ngx-vest-forms
v2.6.0
Published
Opinionated template-driven forms library for Angular with Vest.js integration
Readme
ngx-vest-forms
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!
Why ngx-vest-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.
Installation & Quick Start
Prerequisites
- 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)
Installation
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.
Quick Start
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.
Key Features
- 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
Compatibility & Safety Notes (v2.x)
ROOT_FORM_CONSTANTis retained for compatibility but deprecated; preferROOT_FORM.set/cloneDeepare retained for compatibility; prefersetValueAtPath/structuredClonein new code.
Error & Warning Display Modes
Control when validation errors and warnings are shown to users with multiple built-in modes:
Error Display 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 |
Warning Display Modes
// 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 Example
// 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>ARIA association (advanced)
<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
Form State
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
Advanced Features
Validation Config
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
Root-Form Validation
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
Dynamic Form Structure
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
Shape Validation (Development Mode)
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.
Documentation
Getting Started
- Complete Example - Step-by-step walkthrough from basic form to advanced patterns
- Composable Validations - Break validation logic into reusable, testable functions
Advanced Patterns
- ValidationConfig vs Root-Form - Cross-field dependencies and form-level rules
- Clear Submitted State - End a submit cycle without resetting values or control metadata
- 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
UI & Integration
- 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
Reference
- Utilities README - Canonical reference for all utility functions
Examples
- 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:
Migration
- v1.x → v2.0.0: Migration Guide
- Selector prefixes: Dual Selector Support
Browser support follows Angular 19+ targets (no structuredClone polyfill required).
FAQ
Do I need validations to use ngx-vest-forms?
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.
Resources
Documentation & Tutorials
- 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
Running Examples Locally
npm install
npm startLearning Resources
Complex Angular Template-Driven Forms Course - Master advanced form patterns and become a form expert.
Founding Articles by Brecht Billiet
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
Developer Resources
Comprehensive Instruction Files
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
Acknowledgments
🙏 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.
Core Contributors & Inspirations
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.
License
This project is licensed under the MIT License - see the LICENSE file for details.
