Skip to main content

Validation reference

Every Form Nodes node can own validation rules. Fields validate one leaf value; forms, groups, and arrays can validate their aggregate value while also collecting validation state from descendants.

Use validator() to type a reusable synchronous rule and asyncValidator() for Promise- or Observable-based work.

import { asyncValidator, field, form, minLength, required, validator } from '@ngblocks/form-nodes';

const usernameAllowed = validator(({ value }) => {
return value() === 'admin'
? { kind: 'reservedUsername', message: 'This username is reserved.' }
: null;
});

const usernameAvailable = asyncValidator(({ value, abortSignal }) => {
return api.checkUsername(value(), abortSignal).then(available =>
available ? null : { kind: 'usernameTaken' },
);
});

const myForm = form({
username: field('', {
validators: [required, minLength(3), usernameAllowed, usernameAvailable],
}),
});

🧭 API map​

I want to…Start withDetails
Declare rules on a nodevalidatorsThe validators property
Write a reusable synchronous rulevalidator()validator() reference
Run asynchronous validationasyncValidator()asyncValidator() reference
Understand each node typefield(), group(), form(), array()Validation by node type
Read errors and statusNode validation signalsValidation state
Target a child from an aggregate ruletargetNodeError ownership
Replace rules at runtimesetValidators()Replacing validators

βœ… The validators property​

The validators option accepts one validator or a readonly array. Array entries may be null or undefined, which are ignored.

type DeferredValidator = () => any;

type ValidatorSource<TValue> =
| DeferredValidator
| ComposableValidator<TValue>
| readonly [
validator?: DeferredValidator | ComposableValidator<TValue> | null | undefined,
...validators: (DeferredValidator | ComposableValidator<TValue> | null | undefined)[],
];
const myForm = form({
email: field('', {
validators: [required, minLength(3)],
}),
});

Parameterless source callbacks can reference the form being initialized, such as () => equalTo(this.myForm.password()), without return annotations. Their return type is intentionally unchecked, including overloaded functions callable without arguments. This also applies to parameterless callbacks passed to validator() and the callback signature of asyncValidator(). Use context-taking callbacks for checked reusable rules and call overloaded factories such as uniqueItems() when you need value-compatibility checking. The supported runtime results are unchanged. See Self-referencing validators for a complete example.

The shorthand positional validator argument is equivalent:

const name = field('', [required, minLength(3)]);

Use the options form when the node also needs configuration such as disabled, readonly, hidden, debounce, injector ownership, array tracking, or form submission.

β—† Accepted validator entries​

EntryExampleBehavior
Built-in validatorrequired, requiredIf(() => condition)Runs synchronously
Configured built-inminLength(3)Runs synchronously and exposes constraint metadata
Inline callback({ value }) => ...Infers the node value type contextually
validator() resultadultReusable typed synchronous validator
asyncValidator() resultusernameAvailableManaged asynchronous validator
null or undefinedenabled ? required : nullIgnored inside a source array

All standard built-in validator factories accept when in their options object. requiredIf() uses its condition argument for the same purpose:

const myForm = form({
newsletterEmail: field('', [
email({ when: ({ value }) => value() !== '' })
]),
});

Signals read by when are tracked. While it returns false, the validator contributes no errors or constraint metadata.

Async validators must be direct source entries. Do not return an asyncValidator() from a synchronous validator.

πŸ“ Validation by node type​

NodeOwn validator valueDescendant validationImportant distinction
field()Complete leaf value, including objects or arrays stored as one valueNoneOne error and interaction boundary
group()Object assembled from enabled childrenAggregates child state and errorsCan be a root without submission
Object shorthand { ... }Same aggregate object as group()Aggregates child state and errorsNo group-specific options at declaration
form()Object assembled from enabled childrenAggregates child state and errorsAdds submission state and operations
array()Array assembled from enabled item nodesAggregates item state and errorsPaths and ownership follow reconciled items

These node boundaries are unchanged by concise declarations. The declaration shorthand matrix shows which primitive owns validation for every shorthand category.

β—† field()​

A field validator observes the complete field value. If a field stores an object or array, the validator still sees that value as one leaf:

const tags = field<string[]>([], {
validators: ({ value }) => value().length <= 5
? null
: { kind: 'tooManyTags', maximum: 5 },
});

Use array() instead when each item needs independent validation, errors, paths, or state.

β—† group() and object shorthand​

A group validator observes the aggregate object. It is appropriate for cross-field rules:

const confirmation = field('');

const credentials = group({
password: field(''),
confirmation,
}, {
validators: ({ value }) => value().password === value().confirmation
? null
: { kind: 'passwordMismatch', targetNode: confirmation },
});

The object shorthand creates the same kind of aggregate branch, but use explicit group() when that branch needs its own validators or other group options.

β—† form()​

Form validation behaves like aggregate group validation. A form additionally uses its validation state to gate submission and exposes submission operations:

const booking = form({
departure: field<Date>(),
returnDate: field<Date>(),
}, {
validators: ({ value }) => {
const { departure, returnDate } = value();

return departure && returnDate && returnDate < departure
? { kind: 'returnBeforeDeparture' }
: null;
},
onSubmit: async value => saveBooking(value),
});

Invalid or unresolved validation prevents a normal submission run. See Submission for the complete submission lifecycle.

β—† array()​

An array can validate the collection while item nodes validate individual values:

const contacts = array({
email: field('', [required, email]),
}, {
initialValue: [{ email: '' }],
validators: ({ value }) => value().length > 0
? null
: { kind: 'contactRequired' },
});

Array-level errors belong to the array unless targetNode names an item or descendant. Item errors remain owned by their item node after insertions, removals, moves, or reconciliation.

🧩 Error types​

Import error types directly from @ngblocks/form-nodes:

TypeContract
ValidationErrorBase error with kind and optional message
ValidationErrorForKind<TKind>Structured payload for a known kind, with a custom-kind fallback
ValidatorError<TNode>Validator-produced error with an optional target; no binding reference
ValidationErrorWithTargetNode<TNode>Published error with a required target and optional binding reference
ValidationErrorWithOptionalTargetNode<TNode>Error with an optional target and binding reference
ValidationErrorWithoutTargetNodeError without a target or binding reference

The generic target defaults to AnyNode for ValidatorError and unknown for the other target variants. ValidationResult describes accepted callback results, including message strings; ValidationError and its variants describe error objects.

βœ… Synchronous validators​

A synchronous validator receives ValidatorContext<TValue> and may return success, a message, an error object, an array of messages and error objects, or synchronous conditional composition.

type ValidationResult =
| null
| undefined
| void
| string
| ValidatorError
| readonly (string | ValidatorError)[];

Every signal read during execution is a dependency. When it changes, validation is recomputed. Strings become { kind: 'custom', message }, including empty strings; public errors remain objects. See Returning messages. See validator() for the complete callback context and composition rules.

⏳ Asynchronous validators​

asyncValidator() marks a validator for managed scheduling. It can return a Promise-like or Observable-like operation. Form Nodes owns pending state, debounce, cancellation, dependency tracking, error mapping, and stale-result suppression.

const referenceExists = asyncValidator(({ value, abortSignal }) => {
return api.referenceExists(value(), abortSignal).then(exists =>
exists ? null : { kind: 'unknownReference' },
);
}, {
debounce: 250,
});

Synchronous validation runs first. Async validators on the same node do not start while that node has a synchronous error. See asyncValidator() for both signatures and the complete execution lifecycle.

βœ… Validation state​

Every node exposes its validation state directly:

MemberMeaning
errors()Errors owned directly by this node
allErrors()Own errors plus errors owned by descendants
getError(kind)First own error matching kind
validationStatus()'valid', 'invalid', or 'unknown'
valid()No errors or unresolved validation in the subtree
invalid()This node or a descendant currently has an error
pending()Async validation is unresolved on this node or a descendant
debouncing()Async validation is waiting for its delay
validators()Normalized readonly collection of directly registered validators
validators({ resolve: true })Final references reached through synchronous compositions
hasValidator(validator, { resolve: true })Whether that exact reference occurs in the resolved list

unknown means no error currently makes the node invalid, but an asynchronous result is still pending. It does not refer to the TypeScript value type.

β—† Aggregate state​

Forms, groups, and arrays combine their own validation with descendant state:

  • an error on the aggregate or any participating descendant makes it invalid;
  • pending work on the aggregate or a descendant contributes pending state;
  • an existing error takes precedence over pending work for validationStatus();
  • errors() remains local, while allErrors() traverses the subtree.

🚨 Error ownership​

An error returned without targetNode is assigned to the node whose validator produced it. Aggregate validators can target a descendant so the error appears where the user can resolve it:

return {
kind: 'passwordMismatch',
message: 'Passwords must match.',
targetNode: myForm.confirmation,
};

errors() on that descendant includes the targeted error. allErrors() on its ancestors includes it while preserving the descendant as targetNode. The formNode property is reserved for errors created by a concrete rendered control binding.

βœ… When validation is skipped​

Disabled, readonly, and hidden state exclude a node's validation while that state is active. In-flight asynchronous validation is cancelled. When the node returns to an interactive state, validation runs again against its current value.

Disabled children are also omitted from aggregate values. See Interaction and availability for propagation rules.

βœ… Replacing validators​

setValidators(source) replaces the node's complete validator source and immediately revalidates the committed value:

myForm.username.setValidators([required, minLength(5)]);

Replacing rules does not change the current value or mark the node dirty or touched. Read the normalized current collection with validators().

β—† Validator inspection and resolution​

validators remains an Angular Signal of directly registered functions. Its options overload and hasValidator accept { resolve?: boolean }, defaulting to false. Resolved inspection shares synchronous validation evaluation and follows returned functions, preserving order and duplicates. It lists registered async validators without executing their async work. Successful leaves remain present; this is not an active-constraint query. See Inspect resolved validators for complete examples and the behavior of wrappers, disabled nodes, and invalid compositions.

See validation error types for result contracts, structured errors, and ownership, or Public types for individual validator and context declarations.