field()
For the exported FieldNode model type and its generic counterpart, see the
Node types reference.
field() creates a leaf node for a scalar, object, date, or any other application value. Fields
normally live inside a form() so their parent, path, validation, and state participate in a tree.
Use FormNodeValue<typeof myField> to extract a field's value type.
Not sure whether a structured value should be a field or child nodes? See Choosing a primitive.
field() can be safely created and used outside an Angular injection context. Value operations,
state, synchronous validation, and asynchronous validation all continue to work. When an injector
is available, its DestroyRef provides deterministic cleanup; without one, Form Nodes uses weak
ownership so an unreachable field and its validation watcher can be garbage-collected.
import { field, form, required } from '@ngblocks/form-nodes';
const myForm = form({
name: field('', [required]),
age: field<number>(),
});
Nodes also support Angular WritableSignal<T> utilities. asReadonly() returns a stable live
readonly value signal; use .$api when a child name shadows an operation.
See writable signal interoperability.
Validator input and resultsβ
See the validator argument and result contract for
this primitive's positional and options.validators signatures. Callbacks accept the fully typed
node context and return ValidationResult or ComposableValidationResult<TValue, TNode> at runtime:
no error, messages, errors with string/numeric kinds, or synchronous validator compositions.
The TypeScript callback return is intentionally any so self-referencing declarations compile.
The node and context remain typed. Annotate the return with ValidationResult (or
ComposableValidationResult for composition), or use the checked context-taking validator()
helper when you want result checking. Numeric error kinds are exposed as strings.
π§ API mapβ
| I want to⦠| Start with | Details |
|---|---|---|
| Decide whether a value should be one field | field<T>() | Arrays and objects |
| Choose field nullability | Nullability shortcuts | Nullability |
| Configure validation, debounce, or state | FieldOptions | Options |
| Read value, parent, or path | myField(), parent(), path() | Properties and methods |
| Change or reset its value | set(), update(), reset() | Method reference |
| Inspect errors or constraints | errors(), getError(), required(), min() | Validation properties |
| Manage touched, dirty, or availability | State signals and marker methods | Interaction and availability |
| Connect it to an Angular control | FormNodeDirective, [formNode] | Binding in Angular |
π Fields can hold arrays and objectsβ
field() means βone leaf node,β not βone scalar.β A field can hold an array when the complete
array is edited as one valueβfor example, by a native multi-select or a multi-select component:
const myForm = form({
selectedRoles: field<string[]>([]),
});
<select multiple [formNode]="myForm.selectedRoles">
<option value="admin">Administrator</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
This field has one validation and interaction state for the complete string[]. Use array() only
when items need independent nodes, bindings, errors, paths, or structural operations. See
Array field or array() for a complete
comparison.
When a value appears directly inside an object-node definition, consult the declaration shorthand matrix to see whether it becomes an implicit field or structural group.
Inline validators receive ctx.node() and ctx.field() typed as this primitive, preserving its
value type and any declared children or array items. Inline validator() and asyncValidator()
helpers retain that inference when their generics are omitted. See
Inline node inference.
π Signaturesβ
field();
field(initialValue);
field(initialValue, options);
field(initialValue, validators, options?);
A field with no initial value starts at null.
Literal union suggestionsβ
With an explicit string-literal union, TypeScript IntelliSense suggests its string members when
you open the initial value's quotes. For IborCode below, the suggestions are DAILY and MONTHLY.
The empty string is only an editing state; it is not a valid completed initial value.
import { field, form } from '@ngblocks/form-nodes';
type IborCode = 'DAILY' | 'MONTHLY' | null;
const pricingForm = form({
iborCode: field<IborCode>('DAILY'),
});
pricingForm.iborCode(); // 'DAILY'
if (pricingForm.iborCode() !== 'DAILY') throw new Error('The initial code should be DAILY.');
pricingForm.iborCode.set('MONTHLY');
pricingForm.iborCode(); // 'MONTHLY'
if (pricingForm.iborCode() !== 'MONTHLY') throw new Error('The code should accept MONTHLY.');
pricingForm.iborCode.set(null);
if (pricingForm.iborCode() !== null) throw new Error('The code should accept null.');
The same suggestions work with field.nullable(), field.strict() for non-nullable unions, and
fields from createFormPrimitives(), including calls with validators or options. Invalid strings
still produce a TypeScript error. Explicit undefined initialization retains its existing type.
π Nullabilityβ
field.strict(initialValue, options?);
field.strict(initialValue, validators, options?);
field.nullable(initialValue?, options?);
field.nullable(initialValue, validators, options?);
field() infers nullability from its generic and initial value. It does not add null to a
non-nullish initial value. field.strict() requires a non-nullish value; field.nullable()
always adds null. Both methods also override configured factory policies.
| Declaration | Value type |
|---|---|
field('') or field<string>('') | string |
field<string>(null) | string | null |
field<string>(undefined) | string | undefined |
field<string | null>('') | string | null |
field<string>() | string | null, initially null |
field.nullable('') or field.nullable<string>() | string | null |
field.strict<string>(null) or field.strict<string>() | Type error |
import { array, field, form } from '@ngblocks/form-nodes';
const profile = form({
name: field('Ada'), // string
nickname: field.nullable(''), // string | null
email: field<string>(null), // string | null
code: field<string>(undefined), // string | undefined
age: field<number>(), // number | null, initially null
roles: array({ name: field('') }),
});
profile.name.set('Grace');
profile.nickname.set(null);
profile.email.set('grace@example.com');
profile.code.set('A');
profile.roles.push({ name: 'admin' });
if (profile.name() !== 'Grace' || profile.nickname() !== null
|| profile.email() !== 'grace@example.com' || profile.code() !== 'A'
|| profile.age() !== null || profile.roles[0]?.name() !== 'admin') {
throw new Error('Fields and aggregate values must preserve their declared value contracts.');
}
profile.code.resetToInitial();
profile.name.reset();
if (profile.code() !== undefined || profile.name() !== 'Grace') {
throw new Error('Reset preserves the current value; resetToInitial restores explicit undefined.');
}
The explicit generic remains the domain contract: field<string>(123) is rejected. Only null
and undefined initial values can extend it. Object properties still need to match the generic;
field<{ name: string }>({ name: null }) is rejected. Initializer variables follow TypeScript's
normal control-flow narrowing; use an explicit union or field.nullable() when future null
values must be accepted even though the current value is non-null.
createFormPrimitives() uses the same inference when nullable
is omitted. Set nullable: true to retain the previous nullable-by-default policy.
The inferred type propagates to validators, equality, callbacks, writes, and parent values.
This is a compile-time contract: native controls retain their existing empty-value behavior.
Use nullable number/date fields when clearing their control can produce null.
When the literal initial value is null or undefined, there is no concrete value from which
TypeScript can infer a future type. Form Nodes uses unknown, rather than the unsafe any.
Omitting the initial value starts at null, while an explicit undefined is preserved:
const myForm = form({
unspecified: field(null), // Field<unknown>
deferred: field(undefined), // Field<unknown>; starts at undefined
nickname: field<string>(null), // Field<string | null>
});
myForm.unspecified.set('Marco');
myForm.unspecified.set(42);
With an explicit generic, field<T>(undefined) preserves undefined and produces
FieldNode<T | undefined>. field.nullable<T>(undefined) also adds null.
The same distinction applies to an untyped field() from
createFormPrimitives({ nullable: false }): it returns FieldNode<unknown> initialized to null.
For a known future type without an initial value, use that factory's field.nullable<T>().
import { createFormPrimitives, field } from '../../src/public-api';
const omitted = field<string>();
const explicit = field<string>(undefined);
if (omitted() !== null) throw new Error('An omitted initial value should default to null.');
if (explicit() !== undefined) throw new Error('An explicit undefined value should be preserved.');
explicit.set('Marco');
explicit.reset(undefined);
if (explicit() !== undefined) throw new Error('The field should accept undefined after initialization.');
const configured = createFormPrimitives({ nullable: false });
const profile = configured.form({
omitted: configured.field(),
explicit: configured.field(undefined),
});
if (profile.omitted() !== null) throw new Error('An omitted configured field value should default to null.');
if (profile.explicit() !== undefined) throw new Error('An explicit configured undefined value should be preserved.');
Use an explicit generic when the domain type is known. Although FieldNode<unknown> accepts null,
TypeScript displays it as unknown because unknown | null simplifies to unknown; reads must be
narrowed before use and therefore do not acquire any-like behavior.
field.strict() is the concise form when null is not a valid business value:
const myForm = form({
countryCode: field.strict('CH'),
});
// myForm.countryCode.set(null); // TypeScript error
βοΈ Optionsβ
| Option | Accepted value | Purpose |
|---|---|---|
configure | (api) => void | Configure this instance once with its typed, collision-safe API. |
validators | validator, validator array, or reactive source | Validates the field value |
equal | 'shallow', 'deep', or (previous, next) => boolean | Retains equivalent exposed values; defaults to Object.is |
injector | Angular Injector | Provides this node's preferred lifecycle owner |
adoptBindingInjector | boolean | Temporarily adopts a direct [formNode] host injector; defaults to true |
inheritInjector | boolean | Uses the nearest ancestor injector when no own injector exists; defaults to true |
debounce | number, 'blur', or asynchronous function | Delays control-originated commits |
disabled | boolean, string, or reactive function | Disables the field |
readonly | boolean or reactive function | Makes the field readonly |
hidden | boolean or reactive function | Hides the field |
Start with a single built-in validator, then use an array when the field needs several rules:
const myForm = form({
displayName: field('', [required]),
username: field('', [required, minLength(3)]),
});
The same array can contain configured built-ins, custom callbacks, and asyncValidator() results.
See Validation for the progressively more advanced forms.
State and debounce options inherit from ancestors. A local option can add a state cause or override the inherited debounce.
βοΈ Option referenceβ
configureβ
Signature: configure?: (api: FieldNode<TValue>['$api']) => void
Synchronously configures each new instance with its callable, collision-safe API after its own structure is ready. The callback is untracked; validators installed inside it remain reactive. Fresh template clones run their own callback. Existing instances do not rerun it on reset or edits. Ancestors may not be attached yet. Return values are ignored.
See configuring nodes and sibling rules for an executable example, parent contracts, initialization order, and lifecycle details.
β Value and validationβ
β equalβ
Signature: equal?: 'shallow' | 'deep' | ((previous: TValue, next: TValue) => boolean)
Controls equality of the exposed value returned by the field and its value() signal. When the
comparison returns true, the previous value and reference are retained. Consumers and validators
depending only on that value do not rerun; other state or validator dependencies can still trigger
validation. Internal storage and controls still accept the latest committed write. Public parents
compose the field's exposed value, and update() callbacks receive that same exposed value.
Default equality remains Object.is.
The exposed value is a lazy computed(): its first evaluation publishes the current value without
comparing, and later evaluations compare against the last exposed value. Several writes can be
combined before a read. A comparator exception affects exposed reads after the write has committed;
a later internal value change permits recovery. Identical writes are skipped by internal
Object.is equality, even if the custom comparator would return false.
import { field, form } from '@ngblocks/form-nodes';
const original = { name: 'Marco', preferences: { language: 'en' } };
const profile = form({
person: field(original, { equal: 'deep' }),
location: field({ city: 'Zurich' }, { equal: 'shallow' }),
username: field.strict('Marco', {
equal: (previous, next) => previous.toLowerCase() === next.toLowerCase(),
}),
});
const initial = profile();
profile.person.set({ name: 'Marco', preferences: { language: 'en' } });
profile.person(); // { name: 'Marco', preferences: { language: 'en' } }
if (profile.person() !== original || profile() !== initial) {
throw new Error('Equivalent field values should retain their previous identity and aggregate value.');
}
const location = profile.location();
profile.location.set({ city: 'Zurich' });
if (profile.location() !== location) {
throw new Error('Shallow equality should retain objects whose direct properties are equal.');
}
profile.username.value.control.set('MARCO');
profile.username(); // 'Marco'
profile.username.value.control(); // 'MARCO'
if (profile.username() !== 'Marco' || profile.username.value.control() !== 'MARCO' || !profile.dirty()) {
throw new Error('Equality should preserve equivalent control input and interaction state.');
}
profile.username.reset();
profile.username(); // 'Marco': equality retains the exposed value
profile.username.value.control(); // 'MARCO': reset preserves the latest committed write
if (profile.username() !== 'Marco' || profile.username.value.control() !== 'MARCO' || profile.dirty()) {
throw new Error('Reset should preserve the latest internal value in the control and clear interaction.');
}
profile.username.update(value => `${value}!`);
profile.username(); // 'Marco!': update receives the exposed value
if (profile.username() !== 'Marco!') {
throw new Error('Update should receive the same exposed value that consumers read.');
}
'shallow'compares arrays and plain objects one level deep withObject.is; other objects use identity. Nested objects therefore need the same references.'deep'follows lodashisEqual-style semantics without importing lodash: it compares nested arrays, own enumerable string and symbol object properties, dates, errors, regular expressions, maps, sets, array buffers, data views, typed arrays, and boxed primitives. Circular references are supported.NaNequalsNaN, and0equals-0. Functions and opaque values such as promises, weak collections, and DOM nodes use identity. Class instances also compare their constructors.- Custom comparators receive the actual field value type, including
nullfor nullable fields. They must be pure and describe interchangeable values. Comparison reads are untracked, and the comparator is not called for initial construction.
Map and set comparisons ignore ordering recursively, including arrays nested within them. Following lodash's semantics, map entries are also compared as unordered pairs. Array properties outside indexed elements are ignored; object properties that are inherited or non-enumerable are ignored. Data views compare their offset, length, and complete backing buffers. BigInt primitives compare by value; boxed BigInts and unsupported object kinds compare by identity.
The option is captured at construction, applies only to this field, and is preserved in array
template clones and configured field factories. form(), group(), and array() provide
aggregate value equality, which retains
their exposed snapshot independently of child storage.
For equality that belongs to one consumer, including comparisons of complete forms, groups, or
arrays, use computed() with an equality function.
That derived signal can retain its previous value independently of the node's committed value.
Equality does not suppress control input, dirty, or touched. Control input follows the normal
debounce policy even when it is equivalent to the exposed value. Only input identical to the
current internal value under Object.is cancels earlier work without scheduling another debounce.
reset() clears interaction and validation lifecycle state and restores the latest internally
committed value to the control. reset(value) stores the supplied value even if the exposed value
remains unchanged. Mutating an object in place does not create an old snapshot for deep comparison;
supply a new value when editing structured data.
β validatorsβ
Signature: validators?: ValidatorSource<TValue, FieldNode<TValue>>
Assigns one validator, several validators, or a reactive validator source to this field.
const username = field('', {
validators: [required, minLength(3)],
});
// or
const username = field('', [required, minLength(3)]);
username.invalid(); // true
β debounceβ
Signature: debounce?: number | 'blur' | ((abortSignal: AbortSignal) => void | PromiseLike<void>)
Delays control-originated commits. Programmatic set() and update() calls remain immediate.
const username = field('', {
debounce: 300,
});
β Availabilityβ
β disabledβ
Signature: disabled?: boolean | string | (() => boolean | string)
Sets or reactively derives disabled state. A string also becomes a disabled reason.
const username = field('', {
disabled: 'Profile is locked',
});
username.disabled(); // true
β readonlyβ
Signature: readonly?: boolean | (() => boolean)
Sets or reactively derives readonly state. It defaults to false.
const username = field('', {
readonly: () => profileArchived(),
});
β hiddenβ
Signature: hidden?: boolean | (() => boolean)
Sets or reactively derives hidden state. It defaults to false.
const username = field('', {
hidden: () => !showUsername(),
});
β Injector ownershipβ
β injectorβ
Signature: injector?: Injector
Provides an explicit lifecycle owner for injector-dependent work such as asynchronous validation.
const injector = inject(Injector);
const username = field('', { injector });
β inheritInjectorβ
Signature: inheritInjector?: boolean
Allows an otherwise injector-less field to use the nearest ancestor injector. It defaults to
true; false creates an inheritance boundary.
const username = field('', {
inheritInjector: false,
});
β adoptBindingInjectorβ
Signature: adoptBindingInjector?: boolean
Allows an otherwise injector-less field to adopt a directly bound [formNode] host injector. It
defaults to true.
const username = field('', {
adoptBindingInjector: false,
});
π Properties and methodsβ
A field is a callable committed-value reader with reactive signal properties and the shared node state API. Signal properties must be called to read their current value.
| Member | Description |
|---|---|
| Value and tree | |
myField() | Returns the current committed value. This is the preferred value-reading form. |
value() | Current committed value. Equivalent to calling the field directly. |
value.committed() | Latest committed data before configured equality checks. |
value.committed.set(value) | Complete immediate write, equivalent to set(). |
value.control() | Immediate value received from a bound control; it can differ during debounce. |
value.control.set(value) | Receives a control value, marks dirty, and applies debounce. |
nodeType() | Returns the literal 'field'. |
form() | Nearest explicit form workflow, or null when none owns the field. |
root() | Complete structural root; a standalone field returns itself. |
parent() | Direct parent node, or null at the root or after detachment. |
path() | Property path from the root; array indexes are string segments. |
keyInParent() | Property name or array index in the parent, or null at the root. |
$api | Callable, collision-safe API for generic infrastructure. |
| Value and control | |
set(value) | Immediately assigns a committed value without marking the field dirty. |
update(updater) | Derives and assigns a value from the current committed value. |
reset(value?) | Optionally replaces the value, then clears interaction state and pending input. |
resetToInitial() | Restores captured initial values and clears subtree interaction state. |
debouncing() | Whether a control value is waiting to be committed. |
flush() | Immediately commits a pending control value. |
focus(options?) | Focuses the first bound [formNode] control in DOM order. |
| Validation | |
validators() | Current normalized validators owned by the field. |
setValidators(source) | Replaces the validator source and revalidates. |
errors() | Errors owned directly by this field. |
allErrors() | Same errors as errors() because fields have no descendants. |
getError(kind) | First field-owned error with a kind, or undefined. |
valid() | Whether validation has completed without errors. |
invalid() | Whether the field currently has an error. |
required() | Whether active metadata marks the field as required. |
pending() | Whether asynchronous validation is active. |
validationStatus() | Current 'valid', 'invalid', or 'unknown' phase. |
min() | Strictest active numeric or date minimum, or null. |
max() | Strictest active numeric or date maximum, or null. |
minLength() | Strictest active minimum length, or null. |
maxLength() | Strictest active maximum length, or null. |
pattern() | Every regular expression from active pattern validators. |
| Interaction | |
touched() | Whether the field is touched and currently interactive. |
untouched() | Logical inverse of touched(). |
markAsTouched(options?) | Marks the field touched and flushes pending control input. |
markAsUntouched() | Clears the field's stored touched state. |
dirty() | Whether the field is dirty and currently interactive. |
pristine() | Logical inverse of dirty(). |
markAsDirty() | Marks the field dirty. |
markAsPristine() | Clears the field's stored dirty state. |
| Availability | |
disabled() | Whether the field is disabled locally or by an ancestor. |
disabledReasons() | Active local and inherited disabled causes. |
enabled() | Logical inverse of disabled(). |
disable(message?) | Disables the field and optionally records a reason. |
enable() | Clears the imperative disabled state. |
readonly() | Whether the field is readonly locally or through an ancestor. |
writable() | Logical inverse of readonly(). |
markAsReadonly() | Marks the field readonly. |
markAsWritable() | Clears the imperative readonly state. |
hidden() | Whether the field is hidden locally or through an ancestor. |
visible() | Logical inverse of hidden(). |
hide() | Marks the field hidden. |
show() | Clears the imperative hidden state. |
| Submission | |
submitting() | Whether an ancestor form is running its submission action. |
Programmatic set() and update() do not mark a field dirty. value.control.set() does. A disabled,
readonly, or hidden field reports touched() and dirty() as false without discarding the stored
state; the state is visible again when the field becomes interactive.
π Property referenceβ
Each entry includes its consumer-facing signature, what it represents or returns, and a complete
example. TValue means the field's inferred value type. ParentNode and RootNode represent the
precise parent and root types inferred from where the field is declared.
β Value and tree propertiesβ
β Callable valueβ
Signature: (): TValue
Calls the field as a signal and returns its current committed value. This is the recommended value read.
const username = field('ada');
username(); // 'ada'
β value()β
Signature: value: NodeValueSignal<TValue, TValue>
Contains the exposed committed value. Configured equal checks may retain an earlier equivalent value. For all three views and their setters, see the value views reference.
const username = field('ada');
username.value(); // 'ada'
Prefer the equivalent callable form, username(), for ordinary value reads.
β value.committed()β
Signature: value.committed: Signal<TValue> & { set(value: TValue): void }
Reads the latest committed data, bypassing configured equal checks on this node and its
children. Pending debounce is still respected. Normal signal identity checks still apply.
See the value views reference for an executable example.
β value.committed.set()β
Signature: value.committed.set(value: TValue): void
Equivalent to set(value): commits immediately, cancels pending input, preserves dirty/touched
state, and follows normal validation and parent propagation. Exposed reads still honor equal.
See the setter example.
β value.control()β
Signature: value.control: Signal<TValue>
Contains the immediate value most recently received from a bound UI control.
const username = field('ada', {
debounce: 300,
});
username.value.control(); // 'ada'
During debounce, value.control() contains the pending control value while username() still
contains the last committed value.
β value.control.set()β
Signature: value.control.set(value: TValue): void
Receives a value from a UI control, marks the field dirty, and applies the configured debounce before committing it. Control bindings normally call this method for you.
const username = field('', {
debounce: 'blur',
});
username.value.control.set('ada');
username.value.control(); // 'ada'
username(); // ''
username.dirty(); // true
β nodeType()β
Signature: nodeType(): 'field'
Returns the stable primitive discriminant for this node.
const username = field('ada');
username.nodeType(); // 'field'
β form()β
Signature: form: Signal<FormNode | null>
Returns the nearest explicit form() containing the field, or null when the field belongs only
to a standalone group() or array(), or is itself standalone. A nested explicit form owns its
descendant fields.
const profile = form({
username: field('ada'),
});
profile.username.form() === profile; // true
β root()β
Signature: root: Signal<RootNode>
Returns the complete structural root containing the field. A standalone or detached field returns itself, and this lookup crosses nested form workflow boundaries.
const username = field('ada');
username.root() === username; // true
β parent()β
Signature: parent: Signal<ParentNode | null>
Returns the direct parent node, or null when the field is standalone or has been detached.
const profile = form({
username: field('ada'),
});
profile.username.parent() === profile; // true
β path()β
Signature: path: Signal<readonly string[]>
Returns the property and array-index segments from the root to the field. Array indexes are string segments.
const profile = form({
address: {
city: field('Zurich'),
},
});
profile.address.city.path(); // ['address', 'city']
β keyInParent()β
Signature: keyInParent: Signal<string | number | null>
Returns the property name or array index under which the field is stored, or null at the root.
const profile = form({
username: field('ada'),
});
profile.username.keyInParent(); // 'username'
β $apiβ
Signature: $api: CallableNodeApi<CallableNodeApi<FieldApi<TValue>>>
Exposes the complete callable API through the collision-safe convention shared by every node kind.
const profile = form({
api: field('public-profile-api'),
username: field('ada'),
});
profile.api(); // 'public-profile-api'
profile.$api.valid(); // true
profile.username.$api.valid(); // true
β Validation propertiesβ
β validators()β
Signature: validators: Signal<Validators<TValue>> & { (options: { resolve?: boolean }): Validators<TValue> }
Contains the normalized validators owned directly by the field, in declaration order.
const username = field('', [required, minLength(3)]);
username.validators().length; // 2
β errors()β
Signature: errors: NodeErrorsSignal<TNode>
Reads own errors by default. Pass { descendants: true } to include every descendant, exactly as allErrors() does. { descendants: false }, {}, and no arguments read only own errors. TNode is this concrete node type.
The property remains an Angular Signal. Own reads preserve the concrete targetNode type; descendant reads use AnyNode because errors can belong to different node kinds. See error queries for an executable example.
Contains the current validation errors owned by the field.
const username = field('', [required]);
username.errors()[0]?.kind; // 'required'
β allErrors()β
Signature: allErrors: Signal<readonly ValidationError[]>
Shortcut for errors({ descendants: true }), returning the same cached array.
Contains errors from the field and its descendants. A field has no descendants, so it contains the
same errors as errors().
const username = field('', [required]);
username.allErrors()[0]?.targetNode === username; // true
β valid()β
Signature: valid: Signal<boolean>
Returns whether validation has completed without errors. It is false while validity is unknown.
const username = field('ada', [required]);
username.valid(); // true
β invalid()β
Signature: invalid: Signal<boolean>
Returns whether the field currently has at least one validation error.
const username = field('', [required]);
username.invalid(); // true
β required()β
Signature: required: Signal<boolean>
Returns whether active validation metadata currently marks the field as required.
const username = field('', [required]);
username.required(); // true
β pending()β
Signature: pending: Signal<boolean>
Returns whether one or more asynchronous validation operations are active on the field.
const username = field('', {
validators: asyncValidator(async () => {
await checkUsername();
return null;
}),
});
username.pending(); // true while checkUsername() is running
β validationStatus()β
Signature: validationStatus: Signal<'valid' | 'invalid' | 'unknown'>
Returns the field's current validation phase.
const username = field('', [required]);
username.validationStatus(); // 'invalid'
'unknown' means asynchronous validation is pending and no existing error currently makes the
field invalid. In that phase, both valid() and invalid() are false.
β Constraint metadataβ
Built-in validators expose reactive metadata used by [formNode] to synchronize native control
constraints. See the built-in validator reference for each validator.
β min()β
Signature: min: Signal<NonNullable<TValue> | null>
Contains the strictest minimum contributed by active numeric or date validators.
const age = field(18, [min(16), min(18)]);
age.min(); // 18
β max()β
Signature: max: Signal<NonNullable<TValue> | null>
Contains the strictest maximum contributed by active numeric or date validators.
const age = field(18, [max(120), max(99)]);
age.max(); // 99
β minLength()β
Signature: minLength: Signal<number | null>
Contains the strictest minimum length contributed by active length validators.
const username = field('', [minLength(3), minLength(5)]);
username.minLength(); // 5
β maxLength()β
Signature: maxLength: Signal<number | null>
Contains the strictest maximum length contributed by active length validators.
const username = field('', [maxLength(30), maxLength(20)]);
username.maxLength(); // 20
β pattern()β
Signature: pattern: Signal<readonly RegExp[]>
Contains every regular expression contributed by active pattern validators.
const username = field('', [pattern(/^[a-z]+$/)]);
username.pattern(); // [/^[a-z]+$/]
β Interaction propertiesβ
β touched()β
Signature: touched: Signal<boolean>
Returns whether the field has stored touched state and is currently interactive.
const username = field('ada');
username.markAsTouched();
username.touched(); // true
β untouched()β
Signature: untouched: Signal<boolean>
Returns the logical inverse of touched().
const username = field('ada');
username.untouched(); // true
β dirty()β
Signature: dirty: Signal<boolean>
Returns whether the field has stored user-modified state and is currently interactive.
const username = field('ada');
username.markAsDirty();
username.dirty(); // true
β pristine()β
Signature: pristine: Signal<boolean>
Returns the logical inverse of dirty().
const username = field('ada');
username.pristine(); // true
β Availability propertiesβ
β disabled()β
Signature: disabled: Signal<boolean>
Returns whether the field is effectively disabled by its own state, configuration, or an ancestor.
const username = field('', {
disabled: true,
});
username.disabled(); // true
β disabledReasons()β
Signature: disabledReasons: Signal<readonly DisabledReason[]>
Contains all active local and inherited disabled causes, including each source node and optional message.
const username = field('', {
disabled: 'Profile is locked',
});
username.disabledReasons()[0]?.message; // 'Profile is locked'
β enabled()β
Signature: enabled: Signal<boolean>
Returns the logical inverse of disabled().
const username = field('ada');
username.enabled(); // true
β readonly()β
Signature: readonly: Signal<boolean>
Returns whether the field is effectively readonly through its own state or an ancestor.
const username = field('', {
readonly: true,
});
username.readonly(); // true
β writable()β
Signature: writable: Signal<boolean>
Returns the logical inverse of readonly() and indicates whether a bound control may commit value
changes.
const username = field('ada');
username.writable(); // true
β hidden()β
Signature: hidden: Signal<boolean>
Returns whether the field is effectively hidden through its own state or an ancestor.
const username = field('', {
hidden: true,
});
username.hidden(); // true
β visible()β
Signature: visible: Signal<boolean>
Returns the logical inverse of hidden().
const username = field('ada');
username.visible(); // true
β Control and submission propertiesβ
β debouncing()β
Signature: debouncing: Signal<boolean>
Returns whether a control-originated value is waiting for the field's numeric, blur-based, or asynchronous debounce to complete.
const username = field('', {
debounce: 300,
});
username.debouncing(); // false before a bound control has a pending value
β submitting()β
Signature: submitting: Signal<boolean>
Returns whether an ancestor form is currently running its submission action. A field cannot initiate submission itself.
const profile = form({
username: field('ada'),
}, {
onSubmit: async () => saveProfile(),
});
profile.username.submitting(); // true while saveProfile() is running
π Method referenceβ
Each entry includes its consumer-facing signature, behavior, and return value.
β Update values and control stateβ
β set()β
Signature: set(value: TValue): void
Immediately assigns both the committed and control values, cancels pending debounce, and does not mark the field dirty.
const username = field('ada');
username.set('grace');
username(); // 'grace'
username.dirty(); // false
β update()β
Signature: update(updater: (value: TValue) => TValue): void
Passes the current committed value to a callback and immediately assigns its result without marking the field dirty.
const username = field(' ada ');
username.update(value => value?.trim() ?? null);
username(); // 'ada'
β reset()β
Signatures: reset(): void Β· reset(value: TValue): void
Cancels pending control input and clears touched and dirty state. Without an argument it keeps the current committed value; with a value it assigns that value first.
const username = field('ada');
username.markAsTouched();
username.markAsDirty();
username.reset('grace');
username(); // 'grace'
username.touched(); // false
username.pristine(); // true
api.patch(value) also exists for a uniform node API and is equivalent to set(value). Use
set() directly in ordinary field code. The callable field also carries patch at runtime, but
its public type exposes this operation only through $api.
β flush()β
Signature: flush(): void
Immediately commits a pending value.control() and ends its debounce. It is a no-op when nothing is
pending.
const username = field('', {
debounce: 300,
});
username.value.control.set('ada');
username.flush();
username(); // 'ada'
username.debouncing(); // false
β focus()β
Signature: focus(options?: FocusOptions): void
Focuses the first [formNode] control bound to the field in DOM order. It forwards standard
FocusOptions and does nothing when no control is bound.
import { Component } from '@angular/core';
import { field, FormNodeDirective } from '@ngblocks/form-nodes';
@Component({
imports: [FormNodeDirective],
template: `<input [formNode]="username" />`,
})
export class UsernameComponent {
username = field('');
focusUsername() {
this.username.focus({ preventScroll: true });
}
}
β Validation and interactionβ
β setValidators()β
Signature: setValidators(validators: ValidatorSource<TValue, FieldNode<TValue>>): void
Replaces the field's validator source and immediately validates the current committed value. The source may itself be reactive.
const username = field('ada');
username.setValidators(minLength(5));
username.invalid(); // true
β getError()β
getError() and hasError() suggest built-in error kinds such as 'required', 'minLength',
and 'email', plus kinds registered through ValidationErrorMap.
Custom string literals and dynamic string values are still accepted. Suggestions list known
kinds regardless of installed validators; they do not indicate that an error is currently present.
The same suggestions are available through $api, and getError() keeps its kind-specific
payload and target-node types.
Signature: getError(kind: string): ValidationError | undefined
Returns the first field-owned error with the requested kind. Known built-in kinds preserve their specific inferred error shape.
const username = field('', [required]);
username.getError('required')?.kind; // 'required'
username.getError('minLength'); // undefined
β markAsTouched()β
Signature: markAsTouched(options?: { skipDescendants?: boolean }): void
Marks the field touched and flushes pending control input. skipDescendants is accepted for API
consistency; a field has no descendants.
const username = field('ada');
username.markAsTouched();
username.touched(); // true
β markAsUntouched()β
Signature: markAsUntouched(): void
Clears the field's stored touched state.
const username = field('ada');
username.markAsTouched();
username.markAsUntouched();
username.untouched(); // true
β markAsDirty()β
Signature: markAsDirty(): void
Marks the field's stored state as dirty without changing its value.
const username = field('ada');
username.markAsDirty();
username.dirty(); // true
β markAsPristine()β
Signature: markAsPristine(): void
Clears the field's stored dirty state without changing its value.
const username = field('ada');
username.markAsDirty();
username.markAsPristine();
username.pristine(); // true
β Availabilityβ
β disable()β
Signature: disable(message?: string): void
Disables the field. An optional message records a user-facing reason.
const username = field('ada');
username.disable('Profile is locked');
username.disabled(); // true
username.disabledReasons()[0]?.message; // 'Profile is locked'
β enable()β
Signature: enable(): void
Clears the disabled state created by disable(). A configured or inherited cause can still keep
the field disabled.
const username = field('ada');
username.disable();
username.enable();
username.enabled(); // true
β markAsReadonly()β
Signature: markAsReadonly(): void
Marks the field readonly, preventing bound controls from committing value changes.
const username = field('ada');
username.markAsReadonly();
username.writable(); // false
β markAsWritable()β
Signature: markAsWritable(): void
Clears the state created by markAsReadonly(). Configured or inherited readonly state can still
apply.
const username = field('ada');
username.markAsReadonly();
username.markAsWritable();
username.writable(); // true
β hide()β
Signature: hide(): void
Marks the field hidden without changing its value.
const username = field('ada');
username.hide();
username.visible(); // false
β show()β
Signature: show(): void
Clears the state created by hide(). Configured or inherited hidden state can still apply.
const username = field('ada');
username.hide();
username.show();
username.visible(); // true
π Binding in Angularβ
import { Component } from '@angular/core';
import { field, form, FormNodeDirective } from '@ngblocks/form-nodes';
@Component({
selector: 'app-profile-editor',
imports: [FormNodeDirective],
template: `<input [formNode]="form.name" />`,
})
export class ProfileEditor {
form = form({
name: field(''),
});
}
The binding synchronizes values, interaction state, validation constraints, accessibility state, and debounce. See Control binding and the shared Node API.
π¨ Query errors and registered validatorsβ
hasError(kind: string): boolean checks the node's own current errors(), like
getError(kind) !== undefined. It does not search descendants or allErrors(). Synchronous,
asynchronous, and bound-control errors are included when present in errors().
hasValidator(validator, options?: { resolve?: boolean }): boolean checks the directly registered validator list by function
identity, including async validators. Retain a factory's returned function to query it later.
A registered validator remains present while passing, disabled, or skipped by a condition.
By default, returned compositions are not expanded. With { resolve: true }, this checks the same
leaf references as validators({ resolve: true }), reusing synchronous validation evaluation.
This can execute synchronous validators; async validators are listed without starting their work.
External control validators and descendants are not searched. See
Inspect resolved validators for conditional
branches, successful leaves, interaction-state suppression, and exceptions.
import { computed } from '@angular/core';
import { field, form, minLength, required } from '@ngblocks/form-nodes';
const minimumNameLength = minLength(3);
const profile = form({ name: field('', [required, minimumNameLength]) });
const missingName = computed(() => profile.name.hasError('required'));
profile.name.hasError('required'); // true
profile.hasError('required'); // false: the error belongs to the child
profile.name.hasValidator(required); // true
profile.name.hasValidator(minimumNameLength); // true
profile.name.hasValidator(minLength(3)); // false: a different function instance
if (!missingName() || profile.hasError('required') || !profile.name.hasValidator(required)
|| !profile.name.hasValidator(minimumNameLength) || profile.name.hasValidator(minLength(3))) {
throw new Error('Queries must distinguish own errors from registered validator identities.');
}
profile.name.set('Marco');
missingName(); // false
profile.name.hasValidator(required); // true: still registered, now passing
if (missingName() || !profile.name.hasValidator(required)) {
throw new Error('Passing validation must clear the error without removing the validator.');
}
profile.name.setValidators([]);
profile.name.hasValidator(required); // false
if (profile.name.hasValidator(required)) {
throw new Error('Validator queries must reflect replacement of the registered validators.');
}
Both queries participate in reactive tracking when read inside computed() or effect() and
memoize their boolean result by argument. hasError() follows error changes; hasValidator()
follows setValidators() and, with resolution enabled, dependencies read by synchronous validators.
The default registration query does not execute validators.
For a child named hasError or hasValidator, use the parent's $api to call that operation.
β©οΈ resetToInitial()β
Signature: resetToInitial(): void
Restores captured initial values and clears dirty/touched state in this subtree. It cancels pending
control input, synchronizes rendered controls, and retains current validators and availability
configuration. It does not emit control-originated value outputs. Unlike reset(), it replaces
values; unlike reset(value), it needs no value argument and does not use the last loaded record.
For this field, the baseline is its declaration value, or its effective supplied initialization when created as part of an array item. Supported containers are copied; opaque objects retain references and their in-place mutations cannot be undone.
See Reset and restore initial values for executable examples, server-loaded records, nested arrays, dynamically added fields, snapshot boundaries, validation, and native reset buttons.
Value change callbackβ
onValueChange?(value: TValue, node: FieldNode<TValue>): void;
onValueChange observes committed value changes from both control edits and programmatic writes,
including set(), update(), and resets that change the value.
The binding outputs report only control-originated edits:
(formNodeValueChange) reports the committed value after debounce, while
(formNodeControlValueChange) reports the control value immediately, before debounce.
Use the callback for model changes from either source, or the outputs for edits from a specific control.
Add onValueChange to the options to react synchronously to committed public value changes.
The callback skips initialization, respects equal and control debounce, and receives the typed
node. Aggregate operations notify after their children are updated. It runs without dependency
tracking or an injection-context requirement and does not wait for asynchronous validation.
See value change callbacks for the executable example,
reset and array behavior, callback ordering, and error handling.
Subscribe after creationβ
onValueChange(callback: (value: TValue, node: TNode) => void, options?: { injector?: Injector; debounce?: number }): () => void;
Here TNode is the inferred type of this field instance. Call the instance method to register
independent listeners after construction; use $api.onValueChange() if a child hides the method.
It returns an idempotent cancellation function and emits no initial value. The explicit injector,
otherwise the registration context, owns the listener; node ownership provides a fallback and
also ends the subscription when destroyed. Observation remains available without DI.
Pass { debounce: 300 } to deliver only the latest change after 300 ms of silence for this listener.
Omission or 0 keeps synchronous delivery. Values, validation, and interaction state update normally;
this delay applies to programmatic writes and committed control edits. Unsubscribing or owner
destruction cancels pending delivery. The delay must be finite and non-negative; otherwise
registration throws RangeError. Delayed callback errors are thrown from the timer callback.
See subscription debounce for a component
example, reset behavior, and how it combines with control debounce.
See instance subscriptions for typed examples, automatic cleanup for owner precedence and rebinding, and the callback section above for equality, debounce, and notification timing.