Value flow and debounce
The complete debounce example is compiled and executed with assertions for control value, committed value, dirty, touched, and debouncing state.
Form Nodes distinguishes programmatic model updates from values originating in a bound UI control. That distinction determines debounce and dirty behavior.
Control value eventsâ
Use (formNodeValueChange) on [formNode] to receive control edits after they commit. With debounce,
this waits for its completion or an early flush; without debounce, it emits synchronously.
Use (formNodeControlValueChange) when you need the parsed draft immediately while the committed
value may still be pending. Both carry the node's value type, not a DOM event.
These outputs report edits from the selected control adapter, not programmatic node writes. See the value output reference and complete example for event order, cancellation, native parsing, and custom control contracts.
đ Committed valuesâ
Calling a node is the preferred committed-value read:
const search = field('');
search(); // ''
Synchronous and asynchronous validators, forms, and arrays observe committed values only. See Alternative value access for the equivalent explicit signal paths used by generic infrastructure.
If a consumer needs a custom comparison of committed values, use
computed() with an equality function.
Its retained value belongs to that consumer; it does not change model updates or debounce behavior.
With equal on a field,
form, group, or array, its public value can
retain an earlier equivalent snapshot while internal storage and controls hold newer committed
values. Pending aggregate control input is invalidated by the actual child changes,
including changes hidden by public equality. Validation and submission receive the exposed value.
đĄ Programmatic operationsâ
| Operation | Value effect | Dirty | Touched |
|---|---|---|---|
set(value) | Replaces a complete value | Preserved | Preserved |
update(updater) | Computes and replaces a complete value | Preserved | Preserved |
patch(value) | Updates supplied object branches; supplied arrays reconcile complete collections | Preserved | Preserved |
reset() | Preserves the committed value | Cleared recursively | Cleared recursively |
reset(value) | Replaces the complete value | Cleared recursively | Cleared recursively |
resetToInitial() | Restores captured initial values | Cleared recursively | Cleared recursively |
Programmatic writes are synchronous and never debounced. They cancel pending control work and synchronize the directly bound control representation immediately.
An update() callback runs once, synchronously and untracked, and receives the current exposed
value, including any value retained by equal, rather than a pending control value.
đ Control-originated valuesâ
value.control.set() represents a UI edit:
const myForm = form({
search: field('', { debounce: 300 }),
});
myForm.search.value.control.set('angular');
myForm.search.value.control(); // 'angular' immediately
myForm.search(); // '' until committed
myForm.search.debouncing(); // true
myForm.search.dirty(); // true immediately
It marks the directly bound node dirty even when the reported value equals the existing value. It does not mark the node touched; blur, a custom-control touch event, or markAsTouched() does that.
âąī¸ Debounce strategiesâ
field('', { debounce: 250 });
field('', { debounce: 'blur' });
field('', {
debounce: async abortSignal => {
await waitForIdle(abortSignal);
},
});
- A positive finite number waits that many milliseconds.
'blur'buffers until touch or focus loss.- A function receives an
AbortSignaland commits when its returned promise resolves. - Missing, zero, negative, or non-finite numeric values commit immediately.
- A synchronous function result commits immediately.
Every new control value restarts the strategy and aborts prior custom work. Rejected custom debounce work leaves the committed value unchanged. flush() commits the latest value immediately and aborts outstanding work.
Field input identical to its current internal value under Object.is cancels earlier work without
scheduling replacement work. Custom public equality does not skip a debounce: an equivalent but
internally different value still needs to commit.
Pending timers and custom debounce promises do not, by themselves, keep unused nodes or their parent trees alive. This also applies to cancelled custom work whose promise has not settled yet. Nodes you still retain complete their debounce normally. References held by your own controls, callbacks, values, or injectors keep their normal ownership.
âŠī¸ Touch and reset interactionâ
Marking an interactive node touched commits its pending control value for every debounce strategy. Touching a form or array recursively does the same for descendants unless { skipDescendants: true } is used.
This can change committed values, run validators, and notify onValueChange, even when the nodes
are already touched. skipDescendants skips recursive touch and commit calls; it does not prevent
the current node from committing its own pending input. Noninteractive subtrees are skipped.
This recursive commit is intentional. Angular Signal Forms v22.1.6 also flushes a touched leaf, but its group operation marks descendants internally and only flushes the directly invoked node.
reset() without a value behaves differently: it cancels pending debounce, discards the buffered value, and restores value.control() and rendered controls from the committed model.
When equal retains an older exposed value, reset still restores the latest internally committed
value to controls. It does not replace that value with the older public representative.
âąī¸ Inherited debounceâ
Forms and arrays can establish a default for their subtree:
const profile = form({
name: field(''),
address: {
city: field('', { debounce: 100 }),
},
}, {
debounce: 300,
});
name inherits 300 ms and address.city overrides it with 100 ms. The nearest configured node wins, including an explicit zero that disables an inherited delay. New array items resolve inherited debounce after attachment.
đĄ Aggregate nodesâ
form.debouncing() and array.debouncing() are true while any current descendant has buffered control work. Their flush() recursively commits only their current subtree.
Pending descendant values do not compose into an ancestor's value.control(). Both the aggregate
node call and value.control() keep their last committed representation until descendants commit.
A custom control bound directly to a form or array has its own aggregate control buffer. Its update marks the aggregate node dirty, then distributes or reconciles the complete value when committed; descendants are not individually marked dirty.
Control debounce and asynchronous-validator pending() are independent states.
Restoring initial valuesâ
resetToInitial() discards pending control input and restores captured initial values. It preserves
current object schemas, restores initial array records, and does not emit control-originated value
outputs. Loading server data through reset(value) does not redefine these defaults. See
Reset and restore initial values for the complete contract and examples.
Inspecting committed data before equalityâ
node.value.committed() bypasses custom equality on the node and its descendants while still
respecting debounce. node.value.committed.set(next) performs the same immediate complete write
as node.set(next). node.value.control.set(next) receives control input with dirty tracking
and debounce. See the value views reference for the five individual
entries and a complete executable example combining equality, debounce, and reset.