An open API service indexing awesome lists of open source software.

https://github.com/ngx-vest-forms/ngx-vest-forms

Simple form development for complex and scalable solutions
https://github.com/ngx-vest-forms/ngx-vest-forms

angular template-driven-forms validation vestjs

Last synced: 2 months ago
JSON representation

Simple form development for complex and scalable solutions

Awesome Lists containing this project

README

          

# ngx-vest-forms

A lightweight, type-safe adapter between Angular template-driven forms and [Vest.js](https://vestjs.dev) validation. Build complex forms with unidirectional data flow, sophisticated async validations, and minimal boilerplate.

[![npm version](https://img.shields.io/npm/v/ngx-vest-forms.svg?style=flat-square)](https://www.npmjs.com/package/ngx-vest-forms)
[![Build Status](https://img.shields.io/github/actions/workflow/status/ngx-vest-forms/ngx-vest-forms/cd.yml?branch=master&style=flat-square&label=Build)](https://github.com/ngx-vest-forms/ngx-vest-forms/actions/workflows/cd.yml)
[![Angular]()](https://angular.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](LICENSE)

⭐ If you like this project, star it on GitHub β€” it helps a lot!

[Quick Start](#installation--quick-start) β€’ [Docs](#documentation) β€’ [Key Features](#key-features) β€’ [Migration](#migration) β€’ [FAQ](#faq) β€’ [Resources](#resources)

> **New Maintainer**:
>
> I'm [the-ult](https://bsky.app/profile/the-ult.bsky.social), 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](#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

```bash
npm install ngx-vest-forms
```

> **v.2.0.0 NOTE:**
>
> You must call `only()` **unconditionally** in Vest suites.
>
> ```ts
> // βœ… Correct
> only(field); // only(undefined) safely runs all tests
> ```
>
> Why: Conditional `only()` breaks Vest's change detection mechanism and causes timing issues with `omitWhen` + `validationConfig` in ngx-vest-forms.
> See the [Migration Guide](./docs/migration/MIGRATION-v1.x-to-v2.0.0.md#1-unconditional-only-pattern-required-critical).
>
> Selector prefix: use `ngx-` (recommended). The legacy `sc-` works in v2.x but is deprecated and will be removed in v3.

### Quick Start

Start simple (with validations):

```ts
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 = staticSuite((model, field?) => {
only(field);
test('email', 'Email is required', () => {
enforce(model.email).isNotBlank();
});
});

@Component({
imports: [NgxVestForms],
template: `


Email



Name

Submit

`,
})
export class MyComponent {
protected readonly formValue = signal({});
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 `name` attribute MUST exactly match the property path used in `[ngModel]` β€” see [Field Paths](./docs/FIELD-PATHS.md)

That's all you need. The directive automatically creates controls, wires validation, and manages state.

## Key Features

- **Unidirectional state with signals** β€” Models are `NgxDeepPartial` 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`, or `always`
- **Warning display modes** β€” Control when warnings show: `on-touch`, `on-validated-or-touch` (default), `on-dirty`, or `always`
- **Form state tracking** β€” Access touched, dirty, valid/invalid states for individual fields or entire form
- **Error display helpers** β€” `ngx-control-wrapper` component (recommended) plus directive building blocks for custom wrappers:
- `ngx-form-group-wrapper` component (recommended for `ngModelGroup` containers)
- `FormErrorDisplayDirective` (state + display policy)
- `FormErrorControlDirective` (adds ARIA wiring + stable region IDs)
- **Cross-field dependencies** β€” `validationConfig` for field-to-field triggers, `ROOT_FORM` for form-level rules
- **Utilities** β€” Field paths, field clearing, validation config builder

### Compatibility & Safety Notes (v2.x)

- `ROOT_FORM_CONSTANT` is retained for compatibility but deprecated; prefer `ROOT_FORM`.
- `set` / `cloneDeep` are retained for compatibility; prefer `setValueAtPath` / `structuredClone` in new code.

### Error & Warning Display Modes

Control when validation errors and warnings are shown to users with multiple built-in modes:

#### Error Display Modes

```typescript
// 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

```

| 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

```typescript
// 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

```

| 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

```html
// Group-safe mode (use this on an ngModelGroup container)


Street


City

```

#### ARIA association (advanced)

`` can optionally apply `aria-describedby` / `aria-invalid` to **descendant** controls.
This is controlled by `ariaAssociationMode`:

- `"all-controls"` (default) β€” stamps all descendant `input/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 `` (group-safe by default).

πŸ“– See also:

- [Accessibility Guide](./docs/ACCESSIBILITY.md)
- [`ControlWrapperComponent` docs](./projects/ngx-vest-forms/src/lib/components/control-wrapper/README.md)

> **Styling note**: `ngx-control-wrapper` uses Tailwind CSS utility classes for default styling.
> If your project doesn't use Tailwind, see the [component docs](./projects/ngx-vest-forms/src/lib/components/control-wrapper/README.md#styling-dependency-tailwind-css) for alternatives.

πŸ“– **[Complete Guide: Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)**

### Form State

Access complete form and field state through the `FormErrorDisplayDirective` or `FormControlStateDirective`:

```typescript
@Component({
template: `

@if (wrapper.isTouched()) {
Field was touched
}
@if (wrapper.isPending()) {
Validating...
}

`
})
```

**Available state signals:**

- `isTouched()` / `isDirty()` β€” User interaction state
- `isValid()` / `isInvalid()` β€” Validation state
- `isPending()` β€” Async validation in progress
- `errorMessages()` / `warningMessages()` β€” Current validation messages
- `shouldShowErrors()` / `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.errors` and are cleared on `resetForm()`.
- These messages may appear after `validationConfig` triggers validation, even if the field was not touched yet.
- Use `NGX_WARNING_DISPLAY_MODE_TOKEN` to control when warnings display (see [Warning Display Modes](#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](./docs/CUSTOM-CONTROL-WRAPPERS.md)**

## 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.

```typescript
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](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)**

### 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.

```typescript
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();
});
```

```html


{{ vestForm.errors.rootForm }}

```

πŸ“– **[Complete Guide: ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)**

### Dynamic Form Structure

Manually trigger validation when form structure changes between **input fields and non-input content** (like `

` 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-submit` error display mode, errors are shown automatically when you submit via `(ngSubmit)`. The form automatically calls `markAllAsTouched()` internally. You only need to call `markAllAsTouched()` manually for special cases like multiple forms with one submit button.

```typescript
// Structure change: Re-run validation
@if (type() === 'typeA') {

} @else {

No input required

// ← 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


Submit

// 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](./docs/STRUCTURE_CHANGE_DETECTION.md)**

### Shape Validation (Development Mode)

In development mode, ngx-vest-forms validates that your form's structure matches your TypeScript model, catching common mistakes early:

```typescript
// Your model
type MyFormModel = NgxDeepPartial<{
email: string;
address: { street: string; city: string };
}>;

// Define shape for runtime validation
const shape: NgxDeepRequired = {
email: '',
address: { street: '', city: '' },
};
```

```html





```

**Benefits:**

- Catch typos in `name` attributes 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](./docs/FIELD-PATHS.md)**

## Documentation

### Getting Started

- **[Complete Example](./docs/COMPLETE-EXAMPLE.md)** - Step-by-step walkthrough from basic form to advanced patterns
- **[Composable Validations](./docs/COMPOSABLE-VALIDATIONS.md)** - Break validation logic into reusable, testable functions

### Advanced Patterns

- **[ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)** - Cross-field dependencies and form-level rules
- **[Clear Submitted State](./docs/CLEAR-SUBMITTED-STATE.md)** - End a submit cycle without resetting values or control metadata
- **[Field Path Types](./docs/FIELD-PATHS.md)** - Type-safe dot-notation paths for nested properties
- **[Structure Change Detection](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Handle dynamic form structure updates
- **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** - Type-safe utilities for clearing nested form values

### UI & Integration

- **[Child Components](./docs/CHILD-COMPONENTS.md)** - Split large forms into smaller, maintainable components
- **[Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)** - Build consistent error display patterns
- **[API Tokens](./docs/API-TOKENS.md)** - Configure error display modes and other global settings

### Reference

- **[Utilities README](./projects/ngx-vest-forms/src/lib/utils/README.md)** - Canonical reference for all utility functions

### Examples

- **[Examples Project](./projects/examples)** - 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

## Migration

- v1.x β†’ v2.0.0: **[Migration Guide](./docs/migration/MIGRATION-v1.x-to-v2.0.0.md)**
- Selector prefixes: **[Dual Selector Support](./docs/DUAL-SELECTOR-SUPPORT.md)**

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](https://angular.dev/guide/forms)** - Template-driven forms guide
- **[Vest.js Documentation](https://vestjs.dev)** - Validation framework used by ngx-vest-forms
- **[Live Examples Repository](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples)** - Complex form examples and patterns

### Running Examples Locally

```bash
npm install
npm start
```

### Learning Resources

**[Complex Angular Template-Driven Forms Course](https://www.simplified.courses/complex-angular-template-driven-forms)** - Master advanced form patterns and become a form expert.

### Founding Articles by Brecht Billiet

This library was originally created by [Brecht Billiet](https://twitter.com/brechtbilliet). Here are his foundational blog posts that inspired and guided the development:

- **[Introducing ngx-vest-forms](https://blog.simplified.courses/introducing-ngx-vest-forms/)** - The original introduction and motivation
- **[Making Angular Template-Driven Forms Type-Safe](https://blog.simplified.courses/making-angular-template-driven-forms-typesafe/)** - Deep dive into type safety
- **[Asynchronous Form Validators in Angular with Vest](https://blog.simplified.courses/asynchronous-form-validators-in-angular-with-vest/)** - Advanced async validation patterns
- **[Template-Driven Forms with Form Arrays](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)** - Dynamic form arrays implementation

## Developer Resources

### Agent Skills

This repository ships an installable agent skill for ngx-vest-forms guidance.

Install it from the repository root with the `skills` CLI:

```bash
npx skills add ngx-vest-forms/ngx-vest-forms --skill ngx-vest-forms
```

This repository also ships an installable agent skill for **Vest.js 5.4 guidance**.

Install it from the repository root with the `skills` CLI:

```bash
npx skills add ngx-vest-forms/ngx-vest-forms --skill vestjs
```

See also: **[Vest.js 5.4 Agent Skill Guide](./docs/VESTJS-SKILL.md)**

### 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`](.github/instructions/ngx-vest-forms.instructions.md)** - Complete guide for using ngx-vest-forms library
- **[`.github/instructions/vest.instructions.md`](.github/instructions/vest.instructions.md)** - Comprehensive Vest.js validation patterns and best practices
- **[`.github/copilot-instructions.md`](.github/copilot-instructions.md)** - Main GitHub Copilot instructions for this workspace

## Acknowledgments

πŸ™ **Special thanks to [Brecht Billiet](https://twitter.com/brechtbilliet)** 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](https://twitter.com/evyataral)** - Creator of [Vest.js](https://vestjs.dev/)

- 🎯 **The validation engine** that powers ngx-vest-forms
- πŸŽ™οΈ **Featured on PodRocket**: [Vest with Evyatar Alush](https://dev.to/podrocket/vest-with-evyatar-alush) - Deep dive into the philosophy and architecture of Vest.js

**[Ward Bell](https://twitter.com/wardbell)** - Template-Driven Forms Advocate

- πŸ“’ **Evangelized Template-Driven Forms**: [Prefer Template-Driven Forms](https://devconf.net/talk/prefer-template-driven-forms-ward-bell-ng-conf-2021) (ng-conf 2021)
- πŸŽ₯ **Original Vest.js + Angular Integration**: [Form validation done right](https://www.youtube.com/watch?v=EMUAtQlh9Ko) - The foundational talk that inspired this approach
- πŸ’» **Early Implementation**: [ngc-validate](https://github.com/wardbell/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](LICENSE) file for details.