A lightweight, type-safe adapter between Angular template-driven forms and Vest.js validation. Build complex forms with unidirectional data flow, sophisticated async validations, and minimal boilerplate.
β If you like this project, star it on GitHub β it helps a lot!
Quick Start β’ Docs β’ Key Features β’ Migration β’ FAQ β’ Resources
New Maintainer:
I'm the-ult, now maintaining this project as Brecht Billiet has moved on to other priorities. Huge thanks to Brecht for creating this amazing library and his foundational work on Angular forms!
- Unidirectional state with Angular signals
- Type-safe template-driven forms with runtime shape validation (dev only)
- Powerful Vest.js validations (sync/async, conditional, composable)
- Minimal boilerplate: controls and validation wiring are automatic
See the full guides under Documentation.
- Angular: >=19.0.0 minimum, 20.x recommended (all used APIs stable)
- Vest.js: >=5.4.6 (Validation engine)
- TypeScript: >=5.8.0 (Modern Angular features)
- Node.js: >=20 (Maintenance release)
npm install ngx-vest-formsv.2.0.0 NOTE:
You must call
only()unconditionally in Vest suites.// β Correct only(field); // only(undefined) safely runs all testsWhy: Conditional
only()breaks Vest's change detection mechanism and causes timing issues withomitWhen+validationConfigin ngx-vest-forms. See the Migration Guide.Selector prefix: use
ngx-(recommended). The legacysc-works in v2.x but is deprecated and will be removed in v3.
Start simple (with validations):
import { Component, signal } from '@angular/core';
import { NgxVestForms, NgxDeepPartial, NgxVestSuite } from 'ngx-vest-forms';
import { staticSuite, only, test, enforce } from 'vest';
type MyFormModel = NgxDeepPartial<{ email: string; name: string }>;
// Minimal validation suite (always call only(field) unconditionally)
const suite: NgxVestSuite<MyFormModel> = staticSuite((model, field?) => {
only(field);
test('email', 'Email is required', () => {
enforce(model.email).isNotBlank();
});
});
@Component({
imports: [NgxVestForms],
template: `
<form ngxVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
<ngx-control-wrapper>
<label for="email">Email</label>
<input id="email" name="email" [ngModel]="formValue().email" />
<!-- Errors display automatically below input -->
</ngx-control-wrapper>
<ngx-control-wrapper>
<label for="name">Name</label>
<input id="name" name="name" [ngModel]="formValue().name" />
</ngx-control-wrapper>
<button type="submit">Submit</button>
</form>
`,
})
export class MyComponent {
protected readonly formValue = signal<MyFormModel>({});
protected readonly suite = suite;
}Notes.
- Use
[ngModel](not[(ngModel)]) for unidirectional data flow - The
?operator is required because template-driven forms build values incrementally (NgxDeepPartial) - The
nameattribute MUST exactly match the property path used in[ngModel]β see Field Paths
That's all you need. The directive automatically creates controls, wires validation, and manages state.
- Unidirectional state with signals β Models are
NgxDeepPartial<T>so values build up incrementally - Type-safe with runtime shape validation β Automatic control creation and validation wiring (dev mode checks)
- Vest.js validations β Sync/async, conditional, composable patterns with
only(field)optimization - Error display modes β Control when errors show:
on-blur,on-submit,on-blur-or-submit(default),on-dirty, oralways - Warning display modes β Control when warnings show:
on-touch,on-validated-or-touch(default),on-dirty, oralways - Form state tracking β Access touched, dirty, valid/invalid states for individual fields or entire form
- Error display helpers β
ngx-control-wrappercomponent (recommended) plus directive building blocks for custom wrappers:ngx-form-group-wrappercomponent (recommended forngModelGroupcontainers)FormErrorDisplayDirective(state + display policy)FormErrorControlDirective(adds ARIA wiring + stable region IDs)
- Cross-field dependencies β
validationConfigfor field-to-field triggers,ROOT_FORMfor form-level rules - Utilities β Field paths, field clearing, validation config builder
ROOT_FORM_CONSTANTis retained for compatibility but deprecated; preferROOT_FORM.set/cloneDeepare retained for compatibility; prefersetValueAtPath/structuredClonein new code.
Control when validation errors and warnings are shown to users with multiple built-in modes:
// Global configuration via DI token
import { NGX_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
providers: [
{ provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-dirty' }
]
// Recommended: Use ngx-control-wrapper component
<ngx-control-wrapper [errorDisplayMode]="'on-blur'">
<input name="email" [ngModel]="formValue().email" />
</ngx-control-wrapper>| Mode | Behavior |
|---|---|
'on-blur-or-submit' |
Show after blur OR form submit (default) |
'on-blur' |
Show only after blur/touch |
'on-submit' |
Show only after form submission |
'on-dirty' |
Show as soon as value changes (or after blur/submit) |
'always' |
Show immediately, even on pristine fields |
// Global configuration via DI token
import { NGX_WARNING_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
providers: [
{ provide: NGX_WARNING_DISPLAY_MODE_TOKEN, useValue: 'always' }
]
// Per-instance configuration
<ngx-control-wrapper [warningDisplayMode]="'on-dirty'">
<input name="username" [ngModel]="formValue().username" />
</ngx-control-wrapper>| Mode | Behavior |
|---|---|
'on-validated-or-touch' |
Show after validation runs or touch (default) |
'on-touch' |
Show only after blur/touch |
'on-dirty' |
Show as soon as value changes (or after blur/submit) |
'always' |
Show immediately, even on pristine fields |
// Group-safe mode (use this on an ngModelGroup container)
<ngx-form-group-wrapper ngModelGroup="address">
<ngx-control-wrapper>
<label for="street">Street</label>
<input id="street" name="street" [ngModel]="formValue().address?.street" />
</ngx-control-wrapper>
<ngx-control-wrapper>
<label for="city">City</label>
<input id="city" name="city" [ngModel]="formValue().address?.city" />
</ngx-control-wrapper>
</ngx-form-group-wrapper><ngx-control-wrapper> can optionally apply aria-describedby / aria-invalid to descendant controls.
This is controlled by ariaAssociationMode:
"all-controls"(default) β stamps all descendantinput/select/textarea"single-control"β stamps only if exactly one control exists (useful for input + extra buttons)"none"β never mutates descendant controls (group-safe / manual wiring)
For ngModelGroup containers, prefer using <ngx-form-group-wrapper> (group-safe by default).
π See also:
Styling note:
ngx-control-wrapperuses Tailwind CSS utility classes for default styling. If your project doesn't use Tailwind, see the component docs for alternatives.
π Complete Guide: Custom Control Wrappers
Access complete form and field state through the FormErrorDisplayDirective or FormControlStateDirective:
@Component({
template: `
<ngx-control-wrapper #wrapper="ngxErrorDisplay">
<input name="email" [ngModel]="formValue().email" />
@if (wrapper.isTouched()) {
<span>Field was touched</span>
}
@if (wrapper.isPending()) {
<span>Validating...</span>
}
</ngx-control-wrapper>
`
})Available state signals:
isTouched()/isDirty()β User interaction stateisValid()/isInvalid()β Validation stateisPending()β Async validation in progresserrorMessages()/warningMessages()β Current validation messagesshouldShowErrors()/shouldShowWarnings()β Computed based on display mode and state
Warnings behavior:
- Warnings are non-blocking and do not make a field invalid.
- They are stored separately from
control.errorsand are cleared onresetForm(). - These messages may appear after
validationConfigtriggers validation, even if the field was not touched yet. - Use
NGX_WARNING_DISPLAY_MODE_TOKENto control when warnings display (see Warning Display Modes).
Tip: For async validations, use createDebouncedPendingState() to prevent "Validating..." messages from flashing when validation completes quickly (< 200ms).
π Complete Guide: Custom Control Wrappers
Automatically re-validate dependent fields when another field changes. Essential when using Vest.js's omitWhen/skipWhen for conditional validations.
When to use: Password confirmation, conditional required fields, or any field that depends on another field's value.
protected readonly validationConfig = {
'password': ['confirmPassword'], // When password changes, re-validate confirmPassword
'age': ['emergencyContact'] // When age changes, re-validate emergencyContact
};Important: validationConfig only triggers re-validationβvalidation logic is always defined in your Vest suite.
π Complete Guide: ValidationConfig vs Root-Form
Form-level validation rules that don't belong to any specific field (e.g., "at least one contact method required").
When to use: Business rules that evaluate multiple fields but errors should appear at form level, not on individual fields.
import { ROOT_FORM } from 'ngx-vest-forms';
// In your Vest suite
test(ROOT_FORM, 'At least one contact method is required', () => {
enforce(model.email || model.phone).isTruthy();
});<!-- In template -->
<form ngxVestForm ngxValidateRootForm [suite]="suite">
<!-- Show form-level errors -->
<div *ngIf="vestForm.errors?.rootForm">{{ vestForm.errors.rootForm }}</div>
</form>π Complete Guide: ValidationConfig vs Root-Form
Manually trigger validation when form structure changes between input fields and non-input content (like <p> tags) without value changes.
When to use: When switching from form controls to informational text/paragraphs where no control values change.
NOT needed when: Switching between different input fields (value changes trigger validation automatically).
IMPORTANT: triggerFormValidation() only re-runs validation logicβit does NOT mark fields as touched or show errors.
Note on form submission: With the default
on-blur-or-submiterror display mode, errors are shown automatically when you submit via(ngSubmit). The form automatically callsmarkAllAsTouched()internally. You only need to callmarkAllAsTouched()manually for special cases like multiple forms with one submit button.
// Structure change: Re-run validation
@if (type() === 'typeA') {
<input name="fieldA" [ngModel]="formValue().fieldA" />
} @else {
<p>No input required</p> // β No form control, needs triggerFormValidation()
}
onTypeChange(newType: string) {
this.formValue.update(v => ({ ...v, type: newType }));
this.vestForm.triggerFormValidation(); // Re-runs validation, doesn't show errors
}
// Standard form submission - NO manual call needed!
// Errors shown automatically via (ngSubmit) with default on-blur-or-submit mode
<form ngxVestForm (ngSubmit)="save()">
<!-- ... -->
<button type="submit">Submit</button>
</form>
// Multiple forms with one button - NEED manual markAllAsTouched()
submitBoth() {
this.form1().markAllAsTouched();
this.form2().markAllAsTouched();
if (this.form1().valid && this.form2().valid) {
// Submit logic
}
}π Complete Guide: Structure Change Detection
In development mode, ngx-vest-forms validates that your form's structure matches your TypeScript model, catching common mistakes early:
// Your model
type MyFormModel = NgxDeepPartial<{
email: string;
address: { street: string; city: string };
}>;
// Define shape for runtime validation
const shape: NgxDeepRequired<MyFormModel> = {
email: '',
address: { street: '', city: '' },
};<form ngxVestForm [suite]="suite" [formShape]="shape">
<!-- β
Correct: matches shape -->
<input name="email" [ngModel]="formValue().email" />
<input name="address.street" [ngModel]="formValue().address?.street" />
<!-- β Error in dev mode: typo detected -->
<input name="emial" [ngModel]="formValue().email" />
<!-- β Error in dev mode: path doesn't exist in shape -->
<input name="address.zipcode" [ngModel]="formValue().address?.zipcode" />
</form>Benefits:
- Catch typos in
nameattributes immediately during development - Ensure template structure matches TypeScript model
- Zero runtime cost in production (checks disabled automatically)
- Works with nested objects and arrays
Important: Shape validation only runs in development mode (isDevMode() returns true). Production builds have zero overhead.
π Complete Guide: Field Paths
- Complete Example - Step-by-step walkthrough from basic form to advanced patterns
- Composable Validations - Break validation logic into reusable, testable functions
- ValidationConfig vs Root-Form - Cross-field dependencies and form-level rules
- Field Path Types - Type-safe dot-notation paths for nested properties
- Structure Change Detection - Handle dynamic form structure updates
- Field Clearing Utilities - Type-safe utilities for clearing nested form values
- Child Components - Split large forms into smaller, maintainable components
- Custom Control Wrappers - Build consistent error display patterns
- API Tokens - Configure error display modes and other global settings
- Utilities README - Canonical reference for all utility functions
- Examples Project - Working code examples with business hours forms, purchase forms, and validation config demos
- Run locally:
npm install && npm start - Includes smart components, UI components, and complete validation patterns
- Run locally:
- v1.x β v2.0.0: Migration Guide
- Selector prefixes: Dual Selector Support
Browser support follows Angular 19+ targets (no structuredClone polyfill required).
Noβbut youβll almost always want them. Common cases to start without a suite:
- Prototyping UI while deferring rules
- Gradual migration: adopt unidirectional state and type-safe models first
- Server-driven validation: display backend errors while you add a client suite later
You can add a Vest suite at any time by binding [suite] on the form.
- Angular Official Documentation - Template-driven forms guide
- Vest.js Documentation - Validation framework used by ngx-vest-forms
- Live Examples Repository - Complex form examples and patterns
npm install
npm startComplex Angular Template-Driven Forms Course - Master advanced form patterns and become a form expert.
This library was originally created by Brecht Billiet. Here are his foundational blog posts that inspired and guided the development:
- Introducing ngx-vest-forms - The original introduction and motivation
- Making Angular Template-Driven Forms Type-Safe - Deep dive into type safety
- Asynchronous Form Validators in Angular with Vest - Advanced async validation patterns
- Template-Driven Forms with Form Arrays - Dynamic form arrays implementation
This project includes detailed instruction files designed to help developers master ngx-vest-forms and Vest.js patterns:
.github/instructions/ngx-vest-forms.instructions.md- Complete guide for using ngx-vest-forms library.github/instructions/vest.instructions.md- Comprehensive Vest.js validation patterns and best practices.github/copilot-instructions.md- Main GitHub Copilot instructions for this workspace
π Special thanks to Brecht Billiet for creating the original version of this library and his pioneering work on Angular forms. His vision and expertise laid the foundation for what ngx-vest-forms has become today.
Evyatar Alush - Creator of Vest.js
- π― The validation engine that powers ngx-vest-forms
- ποΈ Featured on PodRocket: Vest with Evyatar Alush - Deep dive into the philosophy and architecture of Vest.js
Ward Bell - Template-Driven Forms Advocate
- π’ Evangelized Template-Driven Forms: Prefer Template-Driven Forms (ng-conf 2021)
- π₯ Original Vest.js + Angular Integration: Form validation done right - The foundational talk that inspired this approach
- π» Early Implementation: ngc-validate - The initial version of template-driven forms with Vest.js
These pioneers laid the groundwork that made ngx-vest-forms possible, combining the power of declarative validation with the elegance of Angular's template-driven approach.
This project is licensed under the MIT License - see the LICENSE file for details.