Skip to main content

Node API

This reference groups the public signals and operations available on fields, forms, and arrays. Exact value and parent types remain inferred from the node tree.

For constructor signatures, options, and primitive-specific examples, see form(), field(), and array().

Calling a node directly—such as profile.name() or profile()—is the preferred committed-value read. Use value.control() only when the immediate, potentially debounced value owned by a bound control is specifically needed. The Values and state page documents the explicit alternative paths for generic infrastructure.

Use direct members for actions and state on every node: name.set(), items.push(), profile.patch(), and profile.valid(). The Tree navigation and API access guide documents .$api only for name collisions and generic infrastructure.

Writable signal compatibility

Nodes support utilities accepting WritableSignal<T> through their existing set() and update() methods. Use .$api when child names collide with set, update, or asReadonly, or when a broad node annotation leaves child names unknown. Nullable node values require nullable utility types.

asReadonly(): Signal<T> returns the same live readonly value signal on every call, shared with node.$api.asReadonly(). It tracks the exposed committed value and configured equality, exposes no writing or form methods, and does not mark the node readonly or freeze its data. See writable utilities and the executable example.

Choosing a node type

See the Node types reference for FieldNode, GroupNode, FormNode, ArrayNode, AnyNode, and DynamicNode, including usage without generic arguments. It documents parameters, component inputs, parent inference, and the distinction between models and bindings.

AnyNode or DynamicNode?

Use AnyNode with $api when child names are unknown. Use DynamicNode for direct common members when the declaration is known not to shadow that surface. Neither type wraps the node or repairs name collisions.

Recognizing nodes with isFormNode()

Use isFormNode(value) to check an unknown value and narrow it to AnyNode. See its dedicated reference for the signature, examples, and package-instance limitations.

🧭 API map

Node concernDetails
Values, reset, parent, and pathShared value and tree API
Errors, validity, constraints, and pending stateValidation API
Touched, dirty, disabled, readonly, and hiddenInteraction and availability API
Form children, patching, debounce, focus, and submissionForm-specific API
Array items, collection helpers, and structural operationsArray-specific API
Concrete rendered controlsBinding API

🌳 Shared value and tree API

MemberDescription
myNode()Preferred read of the current committed value
nodeType()Stable primitive discriminant: 'field', 'group', 'form', or 'array'
value.control()Immediate value of a directly bound control; it may differ during debounce
set(value)Assigns a complete value
update(updater)Computes and assigns a complete value
reset() / reset(value)Clears interaction state, optionally replacing the value
resetToInitial()Restores captured initial values and clears interaction state; see reset and restore
form()Nearest explicit form workflow, or null when none owns the node
root()Complete structural root; every standalone root returns itself
parent()Direct parent or null at the root
path()Reactive string path from the root
keyInParent()Property name, array index, or null

Forms and arrays additionally expose patch(), aggregate flush(), debouncing(), and subtree focus(). Use set() rather than patching a leaf field.

nodeType() returns a precise literal for statically known nodes and the complete union for a generic node. This is useful when generic infrastructure needs to branch by primitive without testing for incidental members:

if (node.nodeType() === 'array') {
// Handle an array node.
}

On forms and groups, a child named nodeType can shadow the direct method. Use myForm.$api.nodeType() when code must be collision-safe.

✅ Validation API

MemberDescription
validators()Current normalized validator collection
setValidators(source)Replaces validators
errors()Errors owned directly by this node
errors({ descendants: true })Own and descendant errors
allErrors()Shortcut for errors({ descendants: true })
getError(kind)First own error of a kind
valid() / invalid()Aggregated validity
pending()Current asynchronous validation state
validationStatus()'valid', 'invalid', or 'unknown' while pending without errors
required()Whether active validators require a value

Fields also expose constraint metadata through min(), max(), minLength(), maxLength(), and pattern().

👆 Interaction and availability API

SignalsOperations
touched() / untouched()markAsTouched(), markAsUntouched()
dirty() / pristine()markAsDirty(), markAsPristine()
disabled() / enabled()disable(message?), enable()
readonly() / writable()markAsReadonly(), markAsWritable()
hidden() / visible()hide(), show()
submitting()Readonly submission state
debouncing()flush()

disabledReasons() lists inherited and local causes with their source nodes.

markAsUntouched() and markAsPristine() clear only the selected node's own marker. A touched or dirty descendant can keep a form, group, or array's aggregate state active. Use reset() to clear interaction state throughout a subtree while preserving its committed values.

interaction-markers.ts
import { field, form, array } from '@ngblocks/form-nodes';

const profile = form({
name: field('Ada'),
contacts: array({
email: field('ada@example.com'),
}, {
initialValue: 1,
}),
});

profile.markAsTouched();
profile.markAsUntouched();
profile.touched(); // true
profile.name.touched(); // true
if (!profile.touched() || !profile.name.touched()) {
throw new Error('Clearing a parent marker must preserve touched descendants.');
}

profile.contacts.markAsUntouched();
profile.contacts.touched(); // true
if (!profile.contacts.touched()) {
throw new Error('A touched item must keep its array touched.');
}

profile.reset();
profile.touched(); // false
profile.contacts.touched(); // false
if (profile.touched() || profile.contacts.touched() || profile.name.touched()) {
throw new Error('Reset must clear interaction markers throughout the subtree.');
}

profile.markAsTouched({ skipDescendants: true });
profile.markAsUntouched();
profile.touched(); // false
if (profile.touched()) throw new Error('An own-only marker must be cleared independently.');

🧩 Form-specific API

MemberDescription
childrenStable readonly map of named child nodes
get(key)Reads a child by runtime key, or returns undefined
add(...) / remove(key)Explicitly attaches or detaches runtime children
patch(value)Recursively updates supplied branches
submit()Runs configured submission behavior and returns Promise<boolean>

Initially declared children are also direct properties. Runtime children are deliberately available only through the node returned by add(), get(key), which lets TypeScript and Angular reject misspelled direct properties.

📚 Array-specific API

MemberDescription
items() / length()Reactive item collection and size
[index] / at(index)Reads an item node
push(value?) / insert(index, value?)Creates an item from the template
removeAt(index) / clear()Removes items
moveUp(index) / moveDown(index)Moves one position
move(from, to) / swap(a, b)Reorders nodes without recreating them
patch(values)Reconciles a complete collection, like set()

Arrays are iterable and expose forEach, map, filter, find, findIndex, some, every, includes, and indexOf over item nodes.

🔌 Binding API

A FormNodeDirective<TNode> obtained through viewChild() exposes:

MemberDescription
node()Currently bound typed node
errors()Node errors relevant to this concrete binding
elementHost element
injectorHost injector
focus()Focuses this concrete control
flush()Commits this binding's pending control value
reset()Resets this binding and its current node

Import public APIs only from @ngblocks/form-nodes. _FormNode is exported solely for Angular AOT/linker infrastructure and is not an application API.

For compatibility with Angular model(), ControlValueAccessor, NgControl, and native controls, see Advanced custom controls. For scheduling, detached-node lifetime, multiple bindings, and defensive runtime behavior, see Advanced behavior and edge cases.

Nested value views

Use node.$api.value.committed() to observe the latest committed data before configured equality, and node.$api.value.control() for the node's own pending input. Each has a set() method. See the complete value reference for all five entries and an executable example.

Callable API

Signature: CallableNodeApi<TApi extends { value: Signal<any> }>.

$api() reads the exposed value with the same inferred type and reactive semantics as calling the node. All API signals and operations remain available on that stable function, which is distinct from the node and its value signal. Exposed custom equality still applies; use api.value.committed() for raw committed data. Neither calling the API nor obtaining it changes validation or interaction state.

Child properties cannot overwrite API members. Array APIs keep their real length() signal; normal function members are hidden from IntelliSense on concrete API types. This does not remove JavaScript's function prototype or make unsupported properties part of the public contract.

callable-api.ts
import { computed, isSignal } from '@angular/core';
import { field, form, array, isFormNode } from '@ngblocks/form-nodes';

const profile = form({
submitted: field('draft'),
value: field('child value'),
api: field('child api'),
names: array(field(''), { initialValue: ['Ada'] }),
});
const api = profile.$api;
const snapshot = computed(() => api());

api(); // { submitted: 'draft', value: 'child value', api: 'child api', names: ['Ada'] }
api.submitted(); // false
api.children.submitted(); // 'draft'
api.value(); // { submitted: 'draft', value: 'child value', api: 'child api', names: ['Ada'] }
if (!isSignal(api) || isFormNode(api)) throw new Error('The API must be an Angular signal, not a node declaration.');

await api.submit();
if (!api.submitted() || profile.submitted() !== 'draft') throw new Error('Child names must not overwrite API state.');
api.patch({ value: 'updated' });
if (snapshot().value !== 'updated') throw new Error('Callable API reads must track exposed value changes.');

profile.names.$api.push('Grace');
profile.names.$api(); // ['Ada', 'Grace']
profile.names.$api.length(); // 2
if (profile.names.$api.length() !== 2) throw new Error('The array API must preserve its length signal.');

api.resetToInitial();
if (api.submitted() || snapshot().value !== 'child value') throw new Error('API actions must preserve reset behavior.');

useClosestFormState() exposes this callable API through its reactive formNode property. Read formState.formNode()?.submitted() without child-name collisions.