Skip to main content

Errors and validation status

Form Nodes separates errors owned by one node from errors aggregated across a subtree, and every exposed error identifies its target node.

Use FormNodeErrors to display one error below a field by default, with touch-or-submit visibility and an optional height animation. Pass [node] beside a native input or [state] inside a custom control using useFormNodeState().

Display errors below a native input

Import FormNodeErrors beside FormNodeDirective. The input binds [formNode]="form.email"; the error component observes that same field through [node]="form.email".

contact-form.component.ts
import { Component, signal } from '@angular/core';
import { email, field, form, required, FormNodeErrors, FormNodeDirective } from '@ngblocks/form-nodes';

@Component({
selector: 'app-contact-form',
imports: [FormNodeErrors, FormNodeDirective],
template: `
<form [formNode]="form">
<label for="contact-email">Email</label>
<input id="contact-email" type="email" autocomplete="email" [formNode]="form.email" />
<form-node-errors [node]="form.email" />

<button type="submit">Continue</button>
<button type="reset">Hide errors</button>
<button type="button" (click)="form.resetToInitial()">Start over</button>
</form>

@if (submittedEmail(); as address) {
<p>Submitted email: {{ address }}</p>
}
`,
})
export class ContactForm {
submittedEmail = signal<string | null>(null);

form = form({
email: field('', [required('Enter your email address.'), email('Enter a valid email address.')]),
}, {
onSubmit: (value) => {
this.submittedEmail.set(value.email);
},
});
}

Initially no message is visible. Blur an empty input or submit the form to reveal the required message. An invalid email shows the format message; a valid email removes it. The component shows one message by default and animates its height automatically. The reset button clears interaction history while keeping the value; resetToInitial() also restores the empty value. The separate submitted-email preview belongs to the application and is not reset by the form.

The unique aria-describedby association connects the input to its error container. Keep the container mounted so it can handle visibility and exit animation itself. Custom controls use [state]="state" from useFormNodeState() inside their own template. See display options for multiple messages, submit-only visibility, and disabling animation. Messages use a warm red by default; custom templates and colors let you add icons, render validator metadata, and match your application theme.

🚨 Error shape and ownership

A custom validator returns an error without assigning ownership:

({ value }) => value() === 'blocked'
? { kind: 'blocked', message: 'This value is unavailable.' }
: null

When exposed, the runner adds targetNode:

const error = name.errors()[0];

error.kind; // 'blocked'
error.targetNode === name; // true

targetNode is the node whose validation owns the error. An aggregate validator can explicitly target a descendant for a cross-field rule; otherwise the validated node is assigned automatically. Binding-specific errors, such as a native parse failure, may additionally expose formNode, which identifies the concrete rendered binding that produced it.

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

🚨 Own versus descendant errors

profile.errors();
profile.allErrors();
  • errors() contains only errors owned directly by the current node.
  • allErrors() contains own errors followed by errors from the current subtree.
  • A field has no descendants, so both contain the same node-owned errors.
  • A form-level validator error remains owned by the form rather than being copied to children.

Use errors() to render a group-level rule and allErrors() for summaries or diagnostics.

💡 Typed lookup

getError(kind) returns the first own error of that kind. Built-in kinds infer their complete payload:

const myForm = form({
age: field(16, [min(18)]),
});
const error = myForm.age.getError('min');

error?.min; // number | undefined
error?.actual; // number | undefined
error?.message;
error?.targetNode;

Unknown custom kinds retain a permissive error shape. Reusable packages can augment ValidationErrorMap for precise custom lookup:

declare module '@ngblocks/form-nodes' {
interface ValidationErrorMap {
readonly unavailableUsername: ValidationError & {
readonly kind: 'unavailableUsername';
readonly suggestion: string;
};
}
}

username.getError('unavailableUsername')?.suggestion;

⚡ Status calculation

Situationvalid()invalid()pending()validationStatus()
No errors or pending worktruefalsefalse'valid'
At least one errorfalsetrueMaybe'invalid'
Pending with no completed errorfalsefalsetrue'unknown'
Disabled, readonly, or hiddentruefalsefalse'valid'

An aggregate is invalid when it has an own error or an interactive descendant is invalid. It is pending when it or an interactive descendant is pending, unless an available error already makes the status invalid.

🚨 Error ordering

Validators preserve declaration order. Async results become visible as they complete, but the exposed error array remains in validator order rather than completion order. Replacing validators, changing a when condition, disabling the node, or changing dependencies invalidates stale work.

🚨 Binding-filtered errors

A FormNodeDirective binding's errors() includes:

  • Node errors that are not owned by one concrete control.
  • Binding-owned errors whose formNode is that exact binding.

If two controls bind the same field, a parse error produced by one appears in the field's aggregate errors and that binding's errors, but not in the other binding's errors.

🚨 Suppressed errors

Disabled, readonly, and hidden nodes expose no own errors while non-interactive. The configured validators and stored state are retained, and validation resumes when the node becomes interactive again.

See Async validation for cancellation and pending behavior and Validator messages for message precedence.