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:
-
displayText
property 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
displayText
property (without triggering render) -
If not while typing: parse displayText and update
value
property -
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.value
property -
Format value to displayText
-
Set
displayText
property (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
-
blur
event fires →_onFieldBlur()
-
Calls
acceptInput(false)
(not while typing) -
Reads DOM via
_readDisplayText()
-
Updates
displayText
property -
Parses displayText to business value
-
Sets
value
property -
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
-
trimText
property 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
_isUpdatingEditorFromRenderer
flag 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
_isUpdatingEditorFromRenderer
with 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: boolean
property -
Initialize flag to
false
in 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
updateDisplayTextOnModify
before 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