Scout Value Field Data Flow: Developer Guide
- 1. What You’ll Learn
- 2. Class Hierarchy
- 3. Three-Layer Data Model
- 4. Key Methods
- 5. Data Flow Diagrams
- 6. Method Call Sequence on Field Blur
- 7. Property Synchronization Table
- 8. Special Behaviors
- 9. Synchronization Guarantees
- 10. Integration Pattern for Custom Fields
- 11. Summary: Key Takeaways for Custom Field Implementation
- 12. References
This guide explains the data flow in Eclipse Scout fields, with focus on implementing custom editor fields that extend BasicField. It provides detailed explanations, implementation patterns, and copy-paste templates for creating robust custom fields like AceField and CodeMirrorField.
1. What You’ll Learn
-
Core Data Flow: Understanding the three-layer model (DOM → displayText → value)
-
Integration Patterns: How to properly implement custom editor fields (AceField, CodeMirrorField, etc.)
-
Guard Patterns: Preventing infinite loops and ensuring correct state management
-
Best Practices: Leveraging Scout’s built-in functionality (debouncing, validation, etc.)
-
Copy-Paste Templates: Ready-to-use code templates for implementing your own custom fields
2. Class Hierarchy
FormField (base)
└── ValueField<TValue>
└── BasicField<TValue>
├── StringField (extends BasicField<string>)
├── AceField (custom, extends BasicField<string>)
└── CodeMirrorField (custom, extends BasicField<string>)
-
ValueField.ts- Defines value/displayText/parsing/formatting logic -
BasicField.ts- Adds DOM field rendering ($field), debouncing, input events -
StringField.ts- Adds string-specific features (trimming, obfuscation) -
ValueFieldAdapter.ts- JSON adapter for server communication -
StringFieldAdapter.ts- String-specific adapter -
Custom Fields:
-
AceField.ts- Wraps Ace Editor (uses guard flag pattern) -
CodeMirrorField.ts- Wraps CodeMirror 6 (uses same guard flag pattern)
-
3. Three-Layer Data Model
3.1. 1. DOM Layer ($field.val())
-
The actual HTML
<input>element’s value -
What the user sees and edits
-
Read by:
_readDisplayText() -
Written by:
_renderDisplayText()
4. Key Methods
4.1. Reading from DOM
_readDisplayText(): string {
return this.$field ? this.$field.val() as string : '';
}
Purpose: Extract current text from the DOM input element
Called by:
-
acceptInput()- to capture user edits -
Selection tracking methods
Returns: Current DOM value as string
4.2. Writing to DOM
_renderDisplayText() {
this.$field.val(this.displayText);
super._renderDisplayText();
}
Purpose: Update DOM element to match displayText property
Called when:
-
displayTextproperty changes (automatically via Scout’s property system) -
After server sends new displayText
-
After formatting a value
Special cases (StringField):
-
Obfuscated fields clear DOM when focused
-
Trimming preserves cursor position
4.3. Accepting User Input
acceptInput(whileTyping?: boolean): void {
let displayText = this._readDisplayText();
if (this._checkDisplayTextChanged(displayText, whileTyping)) {
this._setProperty('displayText', displayText);
if (!whileTyping) {
this.parseAndSetValue(displayText);
}
this._triggerAcceptInput(whileTyping);
}
}
Purpose: Capture user edits from DOM and update model
Parameters:
-
whileTyping=true- Called during typing (ifupdateDisplayTextOnModify=true) -
whileTyping=false- Called on blur (always)
Called by:
-
_onFieldBlur()- Always withwhileTyping=false -
_onDisplayTextModified()- IfupdateDisplayTextOnModify=true, withwhileTyping=true -
Application code - Manual calls (e.g., button clicks)
Steps:
-
Read current DOM value via
_readDisplayText() -
Check if it changed with
_checkDisplayTextChanged() -
Update
displayTextproperty (without triggering render) -
If not while typing: parse displayText and update
valueproperty -
Send to server via
_triggerAcceptInput()
4.4. Setting Value Programmatically
setValue(value: TValue) {
this._callSetProperty('value', value);
}
_setValue(value: TValue) {
this.value = this.validateValue(value);
this._updateDisplayText();
this._valueChanged();
}
_updateDisplayText(value?: TValue) {
let formatted = this.formatValue(value);
this.setDisplayText(formatted);
}
Purpose: Update business value and sync to display
Steps:
-
Validate the new value
-
Set
this.valueproperty -
Format value to displayText
-
Set
displayTextproperty (triggers_renderDisplayText()) -
DOM updated with formatted value
-
Fire change events
5. Data Flow Diagrams
5.1. Downward Flow (Server → Browser)
-
Server sends JSON with new
displayText -
ValueFieldAdapter._syncDisplayText()receives it -
Calls
widget.setDisplayText(displayText) -
Property setter triggers
_renderDisplayText() -
DOM updated via
$field.val(displayText)
5.2. Upward Flow (Browser → Server)
-
User types in field, then field loses focus
-
blurevent fires →_onFieldBlur() -
Calls
acceptInput(false)(not while typing) -
Reads DOM via
_readDisplayText() -
Updates
displayTextproperty -
Parses displayText to business value
-
Sets
valueproperty -
Sends event to server with displayText
6. Method Call Sequence on Field Blur
1. User types in field
2. User clicks outside field (blur)
3. 'blur' event fires
4. _onFieldBlur(event)
5. └─> acceptInput(false)
6. ├─> displayText = _readDisplayText() [read from DOM]
7. ├─> _checkDisplayTextChanged(displayText, false)
8. └─> if changed:
9. ├─> _setProperty('displayText', displayText)
10. ├─> parseAndSetValue(displayText)
11. │ ├─> removeErrorStatus()
12. │ ├─> parseValue(displayText) [uses parser function]
13. │ └─> setValue(parsedValue)
14. │ └─> _setValue(parsedValue)
15. │ ├─> validateValue(parsedValue)
16. │ ├─> this.value = validated
17. │ ├─> _updateDisplayText()
18. │ │ ├─> formatValue(this.value)
19. │ │ └─> setDisplayText(formatted)
20. │ │ └─> _renderDisplayText()
21. │ │ └─> $field.val(formatted)
22. │ └─> _valueChanged()
23. └─> _triggerAcceptInput(false) [event sent to server]
7. Property Synchronization Table
| Direction | Property | Trigger | Method |
|---|---|---|---|
Down |
|
Server sends new value |
|
Down |
|
Server sends new value |
|
Up |
|
User blur or input event |
|
Up |
|
Parsed from displayText on blur |
Sent to server as business value |
8. Special Behaviors
8.1. Obfuscated Fields (Password)
_renderDisplayText() {
if (this.inputObfuscated && this.focused) {
this.$field.val('');
return;
}
super._renderDisplayText();
}
-
When field is focused and obfuscated, DOM is cleared
-
New displayText is NOT shown while focused
-
Restored on blur or when value changes externally
8.2. Text Trimming
-
trimTextproperty controls whitespace removal -
Trims leading/trailing spaces on accept
-
If trimming causes visible change, cursor position is preserved
-
Uses
TRIM_REGEXP = /^\s+|\s+$/g
8.3. updateDisplayTextOnModify
When updateDisplayTextOnModify=true:
-
Triggers
_onDisplayTextModified()on each keystroke -
BasicField debounces calls to
acceptInput(true)(default 250ms delay) -
Sends display text to server while typing
-
Used for preview/suggestion features
-
Final blur still sends
acceptInput(false)to parse value
8.3.1. Important: Use _onDisplayTextModified(), not acceptInput(true) directly
Custom fields should call _onDisplayTextModified() instead of acceptInput(true) directly:
// GOOD - Leverages BasicField's debounce logic
protected _onEditorValueChange() {
if (this._isUpdatingEditorFromRenderer) return;
this._updateHasText();
if (this.updateDisplayTextOnModify) {
this._onDisplayTextModified(); // ✓ Uses debounce
}
}
// BAD - Bypasses debounce, sends every keystroke immediately
protected _onEditorValueChange() {
if (this._isUpdatingEditorFromRenderer) return;
this._updateHasText();
this.acceptInput(true); // ✗ No debounce!
}
Why this matters:
-
_onDisplayTextModified()implements debouncing viaupdateDisplayTextOnModifyDelay -
Prevents server overload during rapid typing
-
Allows configurable delay (default 250ms)
-
Follows the pattern used by BasicField internally
9. Synchronization Guarantees
| Layer | Source of Truth |
|---|---|
User Input |
DOM is always the source of truth - |
Display |
displayText property is the source of truth - |
Business Value |
value property is the source of truth - Comes from parsing displayText |
Timing |
Changes sync on blur or explicit calls - Unless |
10. Integration Pattern for Custom Fields
For editor fields like AceField or CodeMirrorField:
export class AceField extends BasicField<string> {
editor: ace.Editor;
protected _isUpdatingEditorFromRenderer: boolean;
constructor() {
super();
this._isUpdatingEditorFromRenderer = false;
}
override _render() {
// Create container and initialize editor
this.addContainer(this.$parent, 'ace-field');
this.addLabel();
let $field = this.$parent.appendDiv('ace-field-content');
this.addField($field);
this.editor = ace.edit($field.get()[0]);
this.editor.setValue(this.displayText);
// Hook editor change events
this.editor.session.on('change', () => {
this._onEditorValueChange();
});
this.addMandatoryIndicator();
this.addStatus();
}
override _readDisplayText(): string {
// Read from editor instead of DOM input
return this.editor ? this.editor.getValue() : '';
}
override _renderDisplayText() {
// Guard: prevent infinite loop when we set editor value
if (this._isUpdatingEditorFromRenderer) {
return;
}
let displayText = strings.nvl(this.displayText);
let currentEditorValue = strings.nvl(this.editor.getValue());
// Write to editor instead of DOM input
if (this.editor && displayText !== currentEditorValue) {
// Set flag before setValue to prevent loop
this._isUpdatingEditorFromRenderer = true;
try {
this.editor.setValue(displayText);
this._updateHasText();
} finally {
// Always clear flag
this._isUpdatingEditorFromRenderer = false;
}
}
}
protected _onEditorValueChange() {
// Don't handle changes that we triggered ourselves
if (this._isUpdatingEditorFromRenderer) {
return;
}
// Update has-text indicator
this._updateHasText();
// Use Scout's built-in method for while-typing updates
if (this.updateDisplayTextOnModify) {
this._onDisplayTextModified();
}
}
}
Key points:
-
Override
_readDisplayText()to read from editor’s internal state -
Override
_renderDisplayText()to write to editor with guard flag protection -
Use
_isUpdatingEditorFromRendererflag to prevent infinite loops -
Call
_onDisplayTextModified()when users make edits (leverages BasicField’s debounce logic) -
The rest of parsing/formatting/validation works automatically
-
Server communication handled by inherited adapter
10.1. Guard Flag Pattern Explained
The _isUpdatingEditorFromRenderer flag prevents infinite loops:
Without this flag: _renderDisplayText() → editor.setValue() → editor change event → _onDisplayTextModified() → acceptInput() → _renderDisplayText() → infinite loop
With this flag: The change event triggered by programmatic updates is ignored, breaking the loop.
11. Summary: Key Takeaways for Custom Field Implementation
11.1. Must Implement
-
_readDisplayText()- Read current value from your editor -
_renderDisplayText()- Write displayText to your editor with guard flag -
Editor change handler - Hook editor events to
_onEditorValueChange() -
Guard flag - Use
_isUpdatingEditorFromRendererwith try-finally
11.2. Best Practices
-
Use
_onDisplayTextModified()instead ofacceptInput(true)- Gets debouncing for free -
Initialize guard flag in constructor - Prevents undefined behavior
-
Update has-text state - Call
_updateHasText()in both render and change handlers -
Check
updateDisplayTextOnModify- Only call_onDisplayTextModified()when enabled -
Preserve selection - Handle cursor position during trimming
-
Call acceptInput on blur - Let BasicField handle blur events
11.3. Implementation Checklist
When implementing a custom editor field:
-
Extend
BasicField<string> -
Add
_isUpdatingEditorFromRenderer: booleanproperty -
Initialize flag to
falsein constructor -
Override
_render()to create editor and hook change events -
Override
_readDisplayText()to read from editor -
Override
_renderDisplayText()with guard flag and try-finally -
Implement
_onEditorValueChange()with guard check -
Call
_updateHasText()in both directions -
Use
_onDisplayTextModified()for user input -
Check
updateDisplayTextOnModifybefore calling_onDisplayTextModified() -
Add selection preservation logic if needed
-
Test: user typing, blur, server updates, error cases
11.4. Pattern Template (Copy-Paste Ready)
export class MyEditorField extends BasicField<string> {
protected _isUpdatingEditorFromRenderer: boolean;
protected _editor: any;
constructor() {
super();
this._isUpdatingEditorFromRenderer = false;
}
protected override _render() {
this.addContainer(this.$parent, 'my-editor-field');
this.addLabel();
let $field = this.$parent.appendDiv('editor-content');
this.addField($field);
this._editor = initEditor($field);
this._editor.setValue(this.displayText);
this._editor.on('change', () => this._onEditorValueChange());
this.addMandatoryIndicator();
this.addStatus();
}
protected override _readDisplayText(): string {
return this._editor ? this._editor.getValue() : '';
}
protected override _renderDisplayText() {
if (this._isUpdatingEditorFromRenderer) return;
let displayText = strings.nvl(this.displayText);
let currentValue = strings.nvl(this._editor.getValue());
if (this._editor && displayText !== currentValue) {
this._isUpdatingEditorFromRenderer = true;
try {
this._editor.setValue(displayText);
this._updateHasText();
} finally {
this._isUpdatingEditorFromRenderer = false;
}
}
}
protected _onEditorValueChange() {
if (this._isUpdatingEditorFromRenderer) return;
this._updateHasText();
if (this.updateDisplayTextOnModify) {
this._onDisplayTextModified();
}
}
}
12. References
12.1. Eclipse Scout Sources
-
ValueField.ts- Lines 142-586 (acceptInput, setValue, _updateDisplayText) -
BasicField.ts- Lines 75-103 (_renderDisplayText, _onDisplayTextModified) -
StringField.ts- Lines 76-564 (_readDisplayText, _renderDisplayText, obfuscation) -
ValueFieldAdapter.ts- Lines 41-93 (_syncDisplayText, _onWidgetAcceptInput) -
StringFieldAdapter.ts- Additional string-specific adapter logic