ngx-vest-forms 2.0.0-beta.1 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -1329
- package/fesm2022/ngx-vest-forms.mjs +228 -59
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +127 -30
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,113 +1,43 @@
|
|
|
1
1
|
<!-- prettier-ignore -->
|
|
2
2
|
<div align="center">
|
|
3
3
|
|
|
4
|
-
<img src="./course.jpeg" alt="ngx-vest-forms" align="center" height="96" />
|
|
5
|
-
|
|
6
4
|
# ngx-vest-forms
|
|
7
5
|
|
|
6
|
+
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.
|
|
7
|
+
|
|
8
8
|
[](https://www.npmjs.com/package/ngx-vest-forms)
|
|
9
9
|
[](https://github.com/ngx-vest-forms/ngx-vest-forms/actions/workflows/cd.yml)
|
|
10
|
-
[%20%E2%80%94%2020%20recommended-dd0031?style=flat-square&logo=angular>)](https://angular.dev)
|
|
11
11
|
[](https://www.typescriptlang.org)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
⭐ If you like this project, star it on GitHub — it helps a lot!
|
|
15
15
|
|
|
16
|
-
[
|
|
16
|
+
[Quick Start](#installation--quick-start) • [Docs](#documentation) • [Key Features](#key-features) • [Migration](#migration) • [FAQ](#faq) • [Resources](#resources)
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
>
|
|
21
|
-
> **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!
|
|
22
|
-
|
|
23
|
-
> [!TIP]
|
|
24
|
-
> **What's New**: Major improvements in the latest release! See **[Release Notes](./docs/PR-60-CHANGES.md)** for complete details.
|
|
25
|
-
>
|
|
26
|
-
> - ✅ **Critical Fix**: Validation timing with `omitWhen` + `validationConfig`
|
|
27
|
-
> - ✅ **New Types**: `NgxDeepPartial`, `NgxDeepRequired`, `NgxVestSuite` (cleaner API)
|
|
28
|
-
> - ✅ **Array Utilities**: Convert arrays to/from objects for template-driven forms
|
|
29
|
-
> - ✅ **Field Path Helpers**: Parse and stringify field paths for Standard Schema
|
|
30
|
-
|
|
31
|
-
> [!WARNING]
|
|
32
|
-
> **Breaking Change**: You must now call `only()` **unconditionally** in validation suites.
|
|
33
|
-
>
|
|
34
|
-
> **Migration**: Remove `if (field)` wrapper around `only()` calls:
|
|
20
|
+
> **New Maintainer**:
|
|
35
21
|
>
|
|
36
|
-
>
|
|
37
|
-
> // ❌ OLD (will break)
|
|
38
|
-
> if (field) {
|
|
39
|
-
> only(field);
|
|
40
|
-
> }
|
|
41
|
-
>
|
|
42
|
-
> // ✅ NEW (required)
|
|
43
|
-
> only(field); // Safe: only(undefined) runs all tests
|
|
44
|
-
> ```
|
|
45
|
-
>
|
|
46
|
-
> **Why**: Conditional `only()` calls corrupt Vest's execution tracking, breaking `omitWhen` + `validationConfig`.
|
|
47
|
-
>
|
|
48
|
-
> **See**: [Performance Optimization with `only()`](#performance-optimization-with-only) section and [Migration Guide](./docs/PR-60-CHANGES.md#migration-guide) for complete details.
|
|
49
|
-
|
|
50
|
-
> [!NOTE]
|
|
51
|
-
> **Selector Prefix Update**: We now recommend using the `ngx-` prefix for all selectors. The legacy `sc-` prefix is **deprecated** and will be removed in v3.0.0.
|
|
52
|
-
>
|
|
53
|
-
> - ✅ **Recommended**: `<ngx-control-wrapper>`, `ngxVestForm`, `ngxValidateRootForm`
|
|
54
|
-
> - ⚠️ **Deprecated**: `<sc-control-wrapper>`, `scVestForm`, `validateRootForm`
|
|
55
|
-
>
|
|
56
|
-
> Both prefixes work in v2.0+, allowing gradual migration. See [Dual Selector Support](./docs/dev/DUAL-SELECTOR-SUPPORT.md) for complete migration guide.
|
|
57
|
-
|
|
58
|
-
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 zero boilerplate.
|
|
59
|
-
|
|
60
|
-
> [!TIP]
|
|
61
|
-
> **For Developers**: This project includes comprehensive instruction files for GitHub Copilot and detailed development guides. See [Developer Resources](#developer-resources) to copy these files to your workspace for enhanced development experience.
|
|
22
|
+
> 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!
|
|
62
23
|
|
|
63
|
-
##
|
|
24
|
+
## Why ngx-vest-forms?
|
|
64
25
|
|
|
65
|
-
|
|
26
|
+
- Unidirectional state with Angular signals
|
|
27
|
+
- Type-safe template-driven forms with runtime shape validation (dev only)
|
|
28
|
+
- Powerful Vest.js validations (sync/async, conditional, composable)
|
|
29
|
+
- Minimal boilerplate: controls and validation wiring are automatic
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
- **Type Safety** - Full TypeScript support with runtime shape validation
|
|
69
|
-
- **Async Validations** - Built-in support for complex, conditional validations
|
|
70
|
-
- **Zero Boilerplate** - Automatic form control creation and validation wiring
|
|
71
|
-
- **Conditional Logic** - Show/hide fields and validation rules dynamically
|
|
72
|
-
- **Reusable Validations** - Share validation suites across frameworks
|
|
31
|
+
See the full guides under [Documentation](#documentation).
|
|
73
32
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Traditional Angular reactive forms require extensive boilerplate for complex scenarios. Template-driven forms are simpler but lack type safety and advanced validation features. **ngx-vest-forms bridges this gap**, giving you the best of both worlds.
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// Before: Complex reactive form setup
|
|
80
|
-
const form = this.fb.group({
|
|
81
|
-
generalInfo: this.fb.group({
|
|
82
|
-
firstName: ['', [Validators.required]],
|
|
83
|
-
lastName: ['', [Validators.required]]
|
|
84
|
-
})
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// After: Simple, type-safe template-driven approach
|
|
88
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
89
|
-
protected readonly suite = myValidationSuite;
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Getting Started
|
|
33
|
+
## Installation & Quick Start
|
|
93
34
|
|
|
94
35
|
### Prerequisites
|
|
95
36
|
|
|
96
|
-
- **Angular**: >=
|
|
37
|
+
- **Angular**: >=19.0.0 minimum, 20.x recommended (all used APIs stable)
|
|
97
38
|
- **Vest.js**: >=5.4.6 (Validation engine)
|
|
98
39
|
- **TypeScript**: >=5.8.0 (Modern Angular features)
|
|
99
|
-
- **Node.js**: >=
|
|
100
|
-
|
|
101
|
-
### Browser Support
|
|
102
|
-
|
|
103
|
-
ngx-vest-forms supports all modern browsers that Angular 18+ targets:
|
|
104
|
-
|
|
105
|
-
- **Chrome**: 98+ (includes `structuredClone()` support)
|
|
106
|
-
- **Firefox**: 94+ (includes `structuredClone()` support)
|
|
107
|
-
- **Safari**: 15.4+ (includes `structuredClone()` support)
|
|
108
|
-
- **Edge**: 98+ (includes `structuredClone()` support)
|
|
109
|
-
|
|
110
|
-
> **Note**: The library uses native `structuredClone()` for object cloning, which is fully supported in all Angular 18+ target environments. No polyfill is required.
|
|
40
|
+
- **Node.js**: >=20 (Maintenance release)
|
|
111
41
|
|
|
112
42
|
### Installation
|
|
113
43
|
|
|
@@ -115,1330 +45,303 @@ ngx-vest-forms supports all modern browsers that Angular 18+ targets:
|
|
|
115
45
|
npm install ngx-vest-forms
|
|
116
46
|
```
|
|
117
47
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
lastName: string;
|
|
132
|
-
};
|
|
133
|
-
}>;
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
#### Step 2: Set up your component
|
|
137
|
-
|
|
138
|
-
Use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import { vestForms, NgxDeepPartial } from 'ngx-vest-forms';
|
|
142
|
-
|
|
143
|
-
// A form model is always deep partial because angular will create it over time organically
|
|
144
|
-
type MyFormModel = NgxDeepPartial<{
|
|
145
|
-
generalInfo: {
|
|
146
|
-
firstName: string;
|
|
147
|
-
lastName: string;
|
|
148
|
-
};
|
|
149
|
-
}>;
|
|
150
|
-
|
|
151
|
-
@Component({
|
|
152
|
-
imports: [vestForms],
|
|
153
|
-
template: `
|
|
154
|
-
<form
|
|
155
|
-
ngxVestForm
|
|
156
|
-
(formValueChange)="formValue.set($event)"
|
|
157
|
-
(ngSubmit)="save()"
|
|
158
|
-
>
|
|
159
|
-
<div ngModelGroup="generalInfo">
|
|
160
|
-
<label>First name</label>
|
|
161
|
-
<input
|
|
162
|
-
type="text"
|
|
163
|
-
name="firstName"
|
|
164
|
-
[ngModel]="formValue().generalInfo?.firstName"
|
|
165
|
-
/>
|
|
166
|
-
|
|
167
|
-
<label>Last name</label>
|
|
168
|
-
<input
|
|
169
|
-
type="text"
|
|
170
|
-
name="lastName"
|
|
171
|
-
[ngModel]="formValue().generalInfo?.lastName"
|
|
172
|
-
/>
|
|
173
|
-
</div>
|
|
174
|
-
</form>
|
|
175
|
-
`,
|
|
176
|
-
})
|
|
177
|
-
export class MyComponent {
|
|
178
|
-
// This signal will hold the state of our form
|
|
179
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
#### Step 3: That's it! 🎉
|
|
184
|
-
|
|
185
|
-
Your form automatically creates FormGroups and FormControls with type-safe, unidirectional data flow.
|
|
186
|
-
|
|
187
|
-
> [!IMPORTANT]
|
|
188
|
-
> Notice we use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow, and the `?` operator since template-driven forms are `DeepPartial`.
|
|
48
|
+
> **v.2.0.0 NOTE:**
|
|
49
|
+
>
|
|
50
|
+
> You must call `only()` **unconditionally** in Vest suites.
|
|
51
|
+
>
|
|
52
|
+
> ```ts
|
|
53
|
+
> // ✅ Correct
|
|
54
|
+
> only(field); // only(undefined) safely runs all tests
|
|
55
|
+
> ```
|
|
56
|
+
>
|
|
57
|
+
> Why: Conditional `only()` corrupts Vest's execution tracking and breaks `omitWhen` + `validationConfig`.
|
|
58
|
+
> See the [Migration Guide](./docs/migration/MIGRATION-v1.x-to-v2.0.0.md#1-unconditional-only-pattern-required-critical).
|
|
59
|
+
>
|
|
60
|
+
> Selector prefix: use `ngx-` (recommended). The legacy `sc-` works in v2.x but is deprecated and will be removed in v3.
|
|
189
61
|
|
|
190
|
-
###
|
|
62
|
+
### Quick Start
|
|
191
63
|
|
|
192
|
-
|
|
64
|
+
Start simple (with validations):
|
|
193
65
|
|
|
194
|
-
```
|
|
66
|
+
```ts
|
|
195
67
|
import { Component, signal } from '@angular/core';
|
|
196
|
-
import {
|
|
197
|
-
|
|
198
|
-
type SimpleForm = NgxDeepPartial<{
|
|
199
|
-
email: string;
|
|
200
|
-
name: string;
|
|
201
|
-
}>;
|
|
202
|
-
|
|
203
|
-
@Component({
|
|
204
|
-
selector: 'app-simple-form',
|
|
205
|
-
imports: [vestForms],
|
|
206
|
-
template: `
|
|
207
|
-
<form
|
|
208
|
-
ngxVestForm
|
|
209
|
-
(formValueChange)="formValue.set($event)"
|
|
210
|
-
(ngSubmit)="save()"
|
|
211
|
-
>
|
|
212
|
-
<label for="email">Email</label>
|
|
213
|
-
<input id="email" name="email" [ngModel]="formValue().email" />
|
|
214
|
-
|
|
215
|
-
<label for="name">Name</label>
|
|
216
|
-
<input id="name" name="name" [ngModel]="formValue().name" />
|
|
217
|
-
|
|
218
|
-
<button type="submit">Submit</button>
|
|
219
|
-
</form>
|
|
220
|
-
`,
|
|
221
|
-
})
|
|
222
|
-
export class SimpleFormComponent {
|
|
223
|
-
protected readonly formValue = signal<SimpleForm>({});
|
|
224
|
-
|
|
225
|
-
protected save(): void {
|
|
226
|
-
console.log('Form submitted:', this.formValue());
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
That's all you need! The `ngxVestForm` directive automatically:
|
|
232
|
-
|
|
233
|
-
- Creates FormControls for each input
|
|
234
|
-
- Manages form state with signals
|
|
235
|
-
- Provides type-safe unidirectional data flow
|
|
236
|
-
|
|
237
|
-
## Complete Example
|
|
238
|
-
|
|
239
|
-
A complete working form with ngx-vest-forms requires just 4 steps:
|
|
240
|
-
|
|
241
|
-
1. **Define your form model** using `NgxDeepPartial<T>`
|
|
242
|
-
2. **Create a validation suite** with Vest.js using `staticSuite()`
|
|
243
|
-
3. **Set up your component** with a signal for form state
|
|
244
|
-
4. **Build your template** with `ngxVestForm` directive and `[ngModel]` bindings
|
|
245
|
-
|
|
246
|
-
The result: A fully functional form with type safety, automatic form control creation, validation on blur/submit, and error display - all with minimal boilerplate.
|
|
247
|
-
|
|
248
|
-
> **📖 Complete Guide**: See **[Complete Example](./docs/COMPLETE-EXAMPLE.md)** for a full working example with detailed explanations of each step, key concepts, and common patterns.
|
|
249
|
-
|
|
250
|
-
## Core Concepts
|
|
251
|
-
|
|
252
|
-
### Understanding Form State
|
|
253
|
-
|
|
254
|
-
Angular automatically creates FormGroups and FormControls based on your template structure. The `ngxVestForm` directive provides these outputs:
|
|
255
|
-
|
|
256
|
-
| Output | Description |
|
|
257
|
-
| ----------------- | ----------------------------------------------- |
|
|
258
|
-
| `formValueChange` | Emits when form value changes (debounced) |
|
|
259
|
-
| `dirtyChange` | Emits when dirty state changes |
|
|
260
|
-
| `validChange` | Emits when validation state changes |
|
|
261
|
-
| `errorsChange` | Emits complete list of errors for form/controls |
|
|
262
|
-
|
|
263
|
-
### Public Methods
|
|
264
|
-
|
|
265
|
-
| Method | Description |
|
|
266
|
-
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
-
| `triggerFormValidation()` | Manually triggers form validation update when form structure changes without value changes (e.g., conditional fields) |
|
|
268
|
-
|
|
269
|
-
### Form Models and Type Safety
|
|
270
|
-
|
|
271
|
-
All form models in ngx-vest-forms use `NgxDeepPartial<T>` (or the legacy alias `DeepPartial<T>`) because Angular's template-driven forms build up values incrementally:
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import { NgxDeepPartial } from 'ngx-vest-forms';
|
|
275
|
-
|
|
276
|
-
type MyFormModel = NgxDeepPartial<{
|
|
277
|
-
generalInfo: {
|
|
278
|
-
firstName: string;
|
|
279
|
-
lastName: string;
|
|
280
|
-
};
|
|
281
|
-
}>;
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
> **Note**: The `Ngx` prefix prevents naming conflicts with other libraries. Both `NgxDeepPartial` and `DeepPartial` work identically; the Ngx-prefixed version is recommended for new code.
|
|
285
|
-
|
|
286
|
-
This is why you must use the `?` operator in templates:
|
|
287
|
-
|
|
288
|
-
```html
|
|
289
|
-
<input [ngModel]="formValue().generalInfo?.firstName" name="firstName" />
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Validation Suite Type Safety
|
|
293
|
-
|
|
294
|
-
Use `NgxVestSuite<T>` for type-safe validation suites:
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
import { NgxVestSuite, NgxDeepPartial } from 'ngx-vest-forms';
|
|
68
|
+
import { NgxVestForms, NgxDeepPartial, NgxVestSuite } from 'ngx-vest-forms';
|
|
298
69
|
import { staticSuite, only, test, enforce } from 'vest';
|
|
299
70
|
|
|
300
|
-
type
|
|
301
|
-
email: string;
|
|
302
|
-
password: string;
|
|
303
|
-
profile: {
|
|
304
|
-
age: number;
|
|
305
|
-
};
|
|
306
|
-
}>;
|
|
307
|
-
|
|
308
|
-
// Create validation suite
|
|
309
|
-
export const validationSuite: NgxVestSuite<FormModel> = staticSuite(
|
|
310
|
-
(model: FormModel, field?: string) => {
|
|
311
|
-
only(field); // CRITICAL: Always call only() unconditionally
|
|
312
|
-
|
|
313
|
-
test('email', 'Email is required', () => {
|
|
314
|
-
enforce(model.email).isNotBlank();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test('profile.age', 'Must be 18+', () => {
|
|
318
|
-
enforce(model.profile?.age).greaterThanOrEquals(18);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
// Component - use directly
|
|
324
|
-
@Component({...})
|
|
325
|
-
class MyFormComponent {
|
|
326
|
-
protected readonly suite = validationSuite;
|
|
327
|
-
protected readonly formValue = signal<FormModel>({});
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
**Key Benefits:**
|
|
332
|
-
|
|
333
|
-
- **Type Safety**: Full TypeScript support for model and field parameters
|
|
334
|
-
- **Template Compatibility**: Works seamlessly in Angular templates
|
|
335
|
-
- **Simplified API**: Single type for all validation suite needs
|
|
336
|
-
- **No Type Assertions**: No `as any` or `$any()` casts needed
|
|
337
|
-
|
|
338
|
-
### Form State Type and Utilities
|
|
339
|
-
|
|
340
|
-
The `formState` computed signal returns an `NgxFormState<T>` object with the current form state:
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
import { NgxFormState, createEmptyFormState } from 'ngx-vest-forms';
|
|
71
|
+
type MyFormModel = NgxDeepPartial<{ email: string; name: string }>;
|
|
344
72
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
73
|
+
// Minimal validation suite (always call only(field) unconditionally)
|
|
74
|
+
const suite: NgxVestSuite<MyFormModel> = staticSuite((model, field?) => {
|
|
75
|
+
only(field);
|
|
76
|
+
test('email', 'Email is required', () => {
|
|
77
|
+
enforce(model.email).isNotBlank();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
351
80
|
|
|
352
|
-
// Useful for parent components displaying child form state
|
|
353
81
|
@Component({
|
|
82
|
+
imports: [NgxVestForms],
|
|
354
83
|
template: `
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
protected readonly formState = computed(
|
|
367
|
-
() => this.childForm()?.vestForm?.formState() ?? createEmptyFormState()
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
The `createEmptyFormState()` utility creates a safe default state:
|
|
373
|
-
|
|
374
|
-
- `valid: true`
|
|
375
|
-
- `errors: {}`
|
|
376
|
-
- `value: null`
|
|
377
|
-
|
|
378
|
-
This prevents null reference errors in templates when child forms or form references might be undefined.
|
|
379
|
-
|
|
380
|
-
### Shape Validation: Catching Typos Early
|
|
381
|
-
|
|
382
|
-
Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
|
|
383
|
-
Making a typo in those can result in a time-consuming endeavor. For this we have introduced shapes.
|
|
384
|
-
A shape is an object where the `scVestForm` can validate to. It is a deep required of the form model:
|
|
385
|
-
|
|
386
|
-
```typescript
|
|
387
|
-
import { DeepPartial, DeepRequired, vestForms } from 'ngx-vest-forms';
|
|
388
|
-
|
|
389
|
-
type MyFormModel = DeepPartial<{
|
|
390
|
-
generalInfo: {
|
|
391
|
-
firstName: string;
|
|
392
|
-
lastName: string;
|
|
393
|
-
};
|
|
394
|
-
}>;
|
|
395
|
-
|
|
396
|
-
export const myFormModelShape: DeepRequired<MyFormModel> = {
|
|
397
|
-
generalInfo: {
|
|
398
|
-
firstName: '',
|
|
399
|
-
lastName: '',
|
|
400
|
-
},
|
|
401
|
-
};
|
|
84
|
+
<form ngxVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
|
|
85
|
+
<ngx-control-wrapper>
|
|
86
|
+
<label for="email">Email</label>
|
|
87
|
+
<input id="email" name="email" [ngModel]="formValue().email" />
|
|
88
|
+
<!-- Errors display automatically below input -->
|
|
89
|
+
</ngx-control-wrapper>
|
|
90
|
+
|
|
91
|
+
<ngx-control-wrapper>
|
|
92
|
+
<label for="name">Name</label>
|
|
93
|
+
<input id="name" name="name" [ngModel]="formValue().name" />
|
|
94
|
+
</ngx-control-wrapper>
|
|
402
95
|
|
|
403
|
-
|
|
404
|
-
imports: [vestForms],
|
|
405
|
-
template: `
|
|
406
|
-
<form
|
|
407
|
-
ngxVestForm
|
|
408
|
-
[formShape]="shape"
|
|
409
|
-
(formValueChange)="formValue.set($event)"
|
|
410
|
-
(ngSubmit)="save()"
|
|
411
|
-
>
|
|
412
|
-
<div ngModelGroup="generalInfo">
|
|
413
|
-
<label>First name</label>
|
|
414
|
-
<input
|
|
415
|
-
type="text"
|
|
416
|
-
name="firstName"
|
|
417
|
-
[ngModel]="formValue().generalInformation?.firstName"
|
|
418
|
-
/>
|
|
419
|
-
|
|
420
|
-
<label>Last name</label>
|
|
421
|
-
<input
|
|
422
|
-
type="text"
|
|
423
|
-
name="lastName"
|
|
424
|
-
[ngModel]="formValue().generalInformation?.lastName"
|
|
425
|
-
/>
|
|
426
|
-
</div>
|
|
96
|
+
<button type="submit">Submit</button>
|
|
427
97
|
</form>
|
|
428
98
|
`,
|
|
429
99
|
})
|
|
430
100
|
export class MyComponent {
|
|
431
101
|
protected readonly formValue = signal<MyFormModel>({});
|
|
432
|
-
protected readonly
|
|
102
|
+
protected readonly suite = suite;
|
|
433
103
|
}
|
|
434
104
|
```
|
|
435
105
|
|
|
436
|
-
|
|
437
|
-
against the form shape every time the form changes, but only when Angular is in devMode.
|
|
438
|
-
|
|
439
|
-
Making a typo in the name attribute or an ngModelGroup attribute would result in runtime errors.
|
|
440
|
-
The console would look like this:
|
|
441
|
-
|
|
442
|
-
```chatinput
|
|
443
|
-
Error: Shape mismatch:
|
|
444
|
-
|
|
445
|
-
[ngModel] Mismatch 'firstame'
|
|
446
|
-
[ngModelGroup] Mismatch: 'addresses.billingddress'
|
|
447
|
-
[ngModel] Mismatch 'addresses.billingddress.steet'
|
|
448
|
-
[ngModel] Mismatch 'addresses.billingddress.number'
|
|
449
|
-
[ngModel] Mismatch 'addresses.billingddress.city'
|
|
450
|
-
[ngModel] Mismatch 'addresses.billingddress.zipcode'
|
|
451
|
-
[ngModel] Mismatch 'addresses.billingddress.country'
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
at validateShape (shape-validation.ts:28:19)
|
|
455
|
-
at Object.next (form.directive.ts:178:17)
|
|
456
|
-
at ConsumerObserver.next (Subscriber.js:91:33)
|
|
457
|
-
at SafeSubscriber._next (Subscriber.js:60:26)
|
|
458
|
-
at SafeSubscriber.next (Subscriber.js:31:18)
|
|
459
|
-
at subscribe.innerSubscriber (switchMap.js:14:144)
|
|
460
|
-
at OperatorSubscriber._next (OperatorSubscriber.js:13:21)
|
|
461
|
-
at OperatorSubscriber.next (Subscriber.js:31:18)
|
|
462
|
-
at map.js:7:24
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
## Validation
|
|
106
|
+
Notes.
|
|
466
107
|
|
|
467
|
-
|
|
108
|
+
- Use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow
|
|
109
|
+
- The `?` operator is required because template-driven forms build values incrementally (`NgxDeepPartial`)
|
|
110
|
+
- The `name` attribute MUST exactly match the property path used in `[ngModel]` — see [Field Paths](./docs/FIELD-PATHS.md)
|
|
468
111
|
|
|
469
|
-
|
|
112
|
+
That's all you need. The directive automatically creates controls, wires validation, and manages state.
|
|
470
113
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
```typescript
|
|
474
|
-
import { enforce, only, staticSuite, test } from 'vest';
|
|
475
|
-
|
|
476
|
-
export const myFormSuite = staticSuite((model: MyFormModel, field?) => {
|
|
477
|
-
only(field); // Call unconditionally at top
|
|
478
|
-
|
|
479
|
-
test('firstName', 'First name is required', () => {
|
|
480
|
-
enforce(model.firstName).isNotBlank();
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
```
|
|
114
|
+
## Key Features
|
|
484
115
|
|
|
485
|
-
**
|
|
116
|
+
- **Unidirectional state with signals** — Models are `NgxDeepPartial<T>` so values build up incrementally
|
|
117
|
+
- **Type-safe with runtime shape validation** — Automatic control creation and validation wiring (dev mode checks)
|
|
118
|
+
- **Vest.js validations** — Sync/async, conditional, composable patterns with `only(field)` optimization
|
|
119
|
+
- **Error display modes** — Control when errors show: `on-blur`, `on-submit`, or `on-blur-or-submit` (default)
|
|
120
|
+
- **Form state tracking** — Access touched, dirty, valid/invalid states for individual fields or entire form
|
|
121
|
+
- **Error display helpers** — `ngx-control-wrapper`, tokens, and `FormErrorDisplayDirective` for consistent UX
|
|
122
|
+
- **Cross-field dependencies** — `validationConfig` for field-to-field triggers, `ROOT_FORM` for form-level rules
|
|
123
|
+
- **Utilities** — Field paths, field clearing, validation config builder
|
|
486
124
|
|
|
487
|
-
|
|
125
|
+
### Error Display Modes
|
|
488
126
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
The biggest pain point ngx-vest-forms solves: **Connecting Vest suites to Angular with zero boilerplate**:
|
|
127
|
+
Control when validation errors are shown to users with three built-in modes:
|
|
492
128
|
|
|
493
129
|
```typescript
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
497
|
-
protected readonly suite = myFormModelSuite;
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
```html
|
|
502
|
-
<!-- Template -->
|
|
503
|
-
<form
|
|
504
|
-
ngxVestForm
|
|
505
|
-
[suite]="suite"
|
|
506
|
-
(formValueChange)="formValue.set($event)"
|
|
507
|
-
(ngSubmit)="save()"
|
|
508
|
-
>
|
|
509
|
-
...
|
|
510
|
-
</form>
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
That's it! Validations are completely wired. Behind the scenes:
|
|
514
|
-
|
|
515
|
-
1. Control gets created, Angular recognizes the `ngModel` directives
|
|
516
|
-
2. These directives implement `AsyncValidator` and connect to the Vest suite
|
|
517
|
-
3. User types into control
|
|
518
|
-
4. The validate function gets called
|
|
519
|
-
5. Vest returns the errors
|
|
520
|
-
6. ngx-vest-forms puts those errors on the Angular form control
|
|
521
|
-
|
|
522
|
-
This means `valid`, `invalid`, `errors`, `statusChanges` all work just like a regular Angular form.
|
|
523
|
-
|
|
524
|
-
### Displaying Validation Errors
|
|
130
|
+
// Global configuration via DI token
|
|
131
|
+
import { NGX_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
|
|
525
132
|
|
|
526
|
-
|
|
133
|
+
providers: [
|
|
134
|
+
{ provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-blur-or-submit' }
|
|
135
|
+
]
|
|
527
136
|
|
|
528
|
-
|
|
529
|
-
<
|
|
530
|
-
<
|
|
531
|
-
|
|
532
|
-
<input
|
|
533
|
-
type="text"
|
|
534
|
-
name="firstName"
|
|
535
|
-
[ngModel]="formValue().generalInfo?.firstName"
|
|
536
|
-
/>
|
|
537
|
-
</ngx-control-wrapper>
|
|
538
|
-
|
|
539
|
-
<ngx-control-wrapper>
|
|
540
|
-
<label>Last name</label>
|
|
541
|
-
<input
|
|
542
|
-
type="text"
|
|
543
|
-
name="lastName"
|
|
544
|
-
[ngModel]="formValue().generalInfo?.lastName"
|
|
545
|
-
/>
|
|
546
|
-
</ngx-control-wrapper>
|
|
547
|
-
</div>
|
|
137
|
+
// Or per-field via control wrapper
|
|
138
|
+
<ngx-control-wrapper [errorDisplayMode]="'on-blur'">
|
|
139
|
+
<input name="email" [ngModel]="formValue().email" />
|
|
140
|
+
</ngx-control-wrapper>
|
|
548
141
|
```
|
|
549
142
|
|
|
550
|
-
|
|
143
|
+
**Available modes:**
|
|
551
144
|
|
|
552
|
-
-
|
|
553
|
-
-
|
|
145
|
+
- **`on-blur-or-submit`** (default) — Show errors after field is touched OR form is submitted
|
|
146
|
+
- **`on-blur`** — Show errors only after field loses focus (touched)
|
|
147
|
+
- **`on-submit`** — Show errors only after form submission
|
|
554
148
|
|
|
555
|
-
|
|
149
|
+
**Tip**: Use `on-blur-or-submit` for best UX — users get immediate feedback on touched fields while preventing overwhelming errors on pristine forms.
|
|
556
150
|
|
|
557
|
-
|
|
558
|
-
- Elements that have an `ngModel` (or form control) inside of them
|
|
151
|
+
📖 **[Complete Guide: Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)**
|
|
559
152
|
|
|
560
|
-
###
|
|
153
|
+
### Form State
|
|
561
154
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
```typescript
|
|
565
|
-
export const suite = staticSuite((model, field?) => {
|
|
566
|
-
only(field); // ✅ CORRECT - Unconditional call
|
|
567
|
-
// ...tests
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
// ❌ WRONG - Conditional call
|
|
571
|
-
export const badSuite = staticSuite((model, field?) => {
|
|
572
|
-
if (field) only(field); // Breaks Vest's execution tracking!
|
|
573
|
-
});
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
**Why**: `only(undefined)` is safe (runs all tests), while `only('fieldName')` optimizes by running only that field's tests. Conditional calls corrupt Vest's internal state and break `omitWhen` + `validationConfig` timing.
|
|
577
|
-
|
|
578
|
-
- ✅ **Fixed**: Proper validation with `omitWhen` and nested fields with `validationConfig`
|
|
579
|
-
|
|
580
|
-
> [!IMPORTANT]
|
|
581
|
-
> **Critical Pattern**: You MUST call `only()` unconditionally. Never wrap it in `if (field)`. This ensures validation uses current form values and prevents timing issues with `omitWhen` and `validationConfig`. Conditional `only()` calls corrupt Vest's internal execution tracking.
|
|
582
|
-
|
|
583
|
-
### Error Display Control
|
|
584
|
-
|
|
585
|
-
The `ngx-control-wrapper` component uses the `FormErrorDisplayDirective` under the hood to manage when and how errors are displayed.
|
|
586
|
-
|
|
587
|
-
#### Error Display Modes
|
|
588
|
-
|
|
589
|
-
ngx-vest-forms supports three error display modes:
|
|
590
|
-
|
|
591
|
-
- **`on-blur-or-submit`** (default) - Show errors after field blur OR form submission
|
|
592
|
-
- **`on-blur`** - Show errors only after field blur
|
|
593
|
-
- **`on-submit`** - Show errors only after form submission
|
|
594
|
-
|
|
595
|
-
#### Configuring Error Display
|
|
596
|
-
|
|
597
|
-
**Global Configuration** - Set the default mode for your entire application:
|
|
598
|
-
|
|
599
|
-
```typescript
|
|
600
|
-
import { ApplicationConfig } from '@angular/core';
|
|
601
|
-
import { NGX_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
|
|
602
|
-
|
|
603
|
-
export const appConfig: ApplicationConfig = {
|
|
604
|
-
providers: [{ provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-submit' }],
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
// Or in a component
|
|
608
|
-
@Component({
|
|
609
|
-
providers: [{ provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-submit' }],
|
|
610
|
-
})
|
|
611
|
-
export class MyComponent {}
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
**Per-Instance Configuration** - Override the mode for specific form fields:
|
|
155
|
+
Access complete form and field state through the `FormErrorDisplayDirective` or `FormControlStateDirective`:
|
|
615
156
|
|
|
616
157
|
```typescript
|
|
617
158
|
@Component({
|
|
618
159
|
template: `
|
|
619
|
-
<ngx-control-wrapper
|
|
160
|
+
<ngx-control-wrapper #wrapper="ngxErrorDisplay">
|
|
620
161
|
<input name="email" [ngModel]="formValue().email" />
|
|
621
|
-
</ngx-control-wrapper>
|
|
622
|
-
`,
|
|
623
|
-
})
|
|
624
|
-
export class MyFormComponent {}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
> **Note**: The `ngx-control-wrapper` component accepts `errorDisplayMode` as an input to override the global setting for specific fields.
|
|
628
|
-
|
|
629
|
-
### Validation Options
|
|
630
|
-
|
|
631
|
-
You can configure additional `validationOptions` at various levels like `form`, `ngModelGroup` or `ngModel`.
|
|
632
|
-
|
|
633
|
-
For example, to debounce validation (useful for API calls):
|
|
634
|
-
|
|
635
|
-
```html
|
|
636
|
-
<form ngxVestForm ... [validationOptions]="{ debounceTime: 0 }">
|
|
637
|
-
...
|
|
638
|
-
<ngx-control-wrapper>
|
|
639
|
-
<label>UserId</label>
|
|
640
|
-
<input
|
|
641
|
-
type="text"
|
|
642
|
-
name="userId"
|
|
643
|
-
[ngModel]="formValue().userId"
|
|
644
|
-
[validationOptions]="{ debounceTime: 300 }"
|
|
645
|
-
/>
|
|
646
|
-
</ngx-control-wrapper>
|
|
647
|
-
...
|
|
648
|
-
</form>
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
#### Configurable Validation Config Debounce
|
|
652
|
-
|
|
653
|
-
The `validationConfig` feature (for triggering dependent field validations) uses a debounce to prevent excessive validation calls. You can configure this debounce globally or per-component using the `NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN`:
|
|
654
|
-
|
|
655
|
-
**Global Configuration** (recommended for consistent behavior):
|
|
656
|
-
|
|
657
|
-
```typescript
|
|
658
|
-
import { ApplicationConfig } from '@angular/core';
|
|
659
|
-
import { NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN } from 'ngx-vest-forms';
|
|
660
|
-
|
|
661
|
-
export const appConfig: ApplicationConfig = {
|
|
662
|
-
providers: [
|
|
663
|
-
// Set global debounce for validationConfig dependencies
|
|
664
|
-
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 150 },
|
|
665
|
-
],
|
|
666
|
-
};
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
**Per-Route Configuration**:
|
|
670
|
-
|
|
671
|
-
```typescript
|
|
672
|
-
{
|
|
673
|
-
path: 'checkout',
|
|
674
|
-
component: CheckoutComponent,
|
|
675
|
-
providers: [
|
|
676
|
-
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 50 }
|
|
677
|
-
]
|
|
678
|
-
}
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
**Per-Component Configuration**:
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
@Component({
|
|
685
|
-
providers: [
|
|
686
|
-
// Disable debounce for testing
|
|
687
|
-
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 0 },
|
|
688
|
-
],
|
|
689
|
-
})
|
|
690
|
-
export class MyFormComponent {}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**Default**: 100ms (maintains backward compatibility)
|
|
694
|
-
|
|
695
|
-
> **Note**: This token controls the debounce for `validationConfig` dependency triggering only. For individual field validation debouncing, use `[validationOptions]="{ debounceTime: 300 }"` on the control as shown above.
|
|
696
|
-
|
|
697
|
-
## Intermediate Topics
|
|
698
|
-
|
|
699
|
-
### Conditional Fields
|
|
700
|
-
|
|
701
|
-
Use computed signals to show/hide fields dynamically. Angular automatically manages FormControl creation/removal:
|
|
702
|
-
|
|
703
|
-
```typescript
|
|
704
|
-
class MyComponent {
|
|
705
|
-
protected readonly showLastName = computed(
|
|
706
|
-
() => !!this.formValue().firstName
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
```html
|
|
712
|
-
@if(showLastName()) {
|
|
713
|
-
<input name="lastName" [ngModel]="formValue().lastName" />
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
<label>Last name</label>
|
|
717
|
-
<input
|
|
718
|
-
type="text"
|
|
719
|
-
name="lastName"
|
|
720
|
-
[ngModel]="formValue().generalInformation?.lastName"
|
|
721
|
-
/>
|
|
722
|
-
</div>
|
|
723
|
-
}
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
### Reactive Disabling
|
|
727
|
-
|
|
728
|
-
To achieve reactive disabling, we just have to take advantage of computed signals as well:
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
class MyComponent {
|
|
732
|
-
protected readonly lastNameDisabled = computed(
|
|
733
|
-
() => !this.formValue().generalInfo?.firstName
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
We can bind the computed signal to the `disabled` directive of Angular.
|
|
739
|
-
|
|
740
|
-
```html
|
|
741
|
-
<input
|
|
742
|
-
type="text"
|
|
743
|
-
name="lastName"
|
|
744
|
-
[disabled]="lastNameDisabled()"
|
|
745
|
-
[ngModel]="formValue().generalInformation?.lastName"
|
|
746
|
-
/>
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
### Conditional Validations
|
|
750
|
-
|
|
751
|
-
Vest makes it extremely easy to create conditional validations.
|
|
752
|
-
Assume we have a form model that has `age` and `emergencyContact`.
|
|
753
|
-
The `emergencyContact` is required, but only when the person is not of legal age.
|
|
754
|
-
|
|
755
|
-
We can use the `omitWhen` so that when the person is below 18, the assertion
|
|
756
|
-
will not be done.
|
|
757
|
-
|
|
758
|
-
```typescript
|
|
759
|
-
import { enforce, omitWhen, only, staticSuite, test } from 'vest';
|
|
760
|
-
|
|
761
|
-
...
|
|
762
|
-
omitWhen((model.age || 0) >= 18, () => {
|
|
763
|
-
test('emergencyContact', 'Emergency contact is required', () => {
|
|
764
|
-
enforce(model.emergencyContact).isNotBlank();
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
You can put those validations on every field that you want. On form group fields and on form control fields.
|
|
770
|
-
Check this interesting example below:
|
|
771
|
-
|
|
772
|
-
- [x] Password is always required
|
|
773
|
-
- [x] Confirm password is only required when there is a password
|
|
774
|
-
- [x] The passwords should match, but only when they are both filled in
|
|
775
|
-
|
|
776
|
-
````typescript
|
|
777
|
-
test('passwords.password', 'Password is not filled in', () => {
|
|
778
|
-
enforce(model.passwords?.password).isNotBlank();
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
omitWhen(!model.passwords?.password, () => {
|
|
782
|
-
test('passwords.confirmPassword', 'Confirm password required', () => {
|
|
783
|
-
enforce(model.passwords?.confirmPassword).isNotBlank();
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
test('passwords', 'Passwords must match', () => {
|
|
787
|
-
enforce(model.passwords?.confirmPassword).equals(model.passwords?.password);
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
This pattern is testable, reusable across frameworks, and readable.
|
|
792
162
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
**Oh, it's also pretty readable**
|
|
796
|
-
|
|
797
|
-
### Dependent Field Validation with Conditional Rendering
|
|
798
|
-
|
|
799
|
-
When fields that depend on each other are conditionally rendered (using `@if`), you need a **reactive validationConfig** to avoid "control not found" warnings.
|
|
800
|
-
|
|
801
|
-
> **💡 Related Pattern**: If you're also switching between form inputs and non-form content (like informational text), you'll need [`triggerFormValidation()`](#handling-form-structure-changes) in addition to computed `validationConfig`. See the [comparison matrix](#handling-dynamic-forms-two-common-patterns) for when to use each.
|
|
802
|
-
|
|
803
|
-
#### The Problem
|
|
804
|
-
|
|
805
|
-
```typescript
|
|
806
|
-
// ❌ This causes warnings when conditional fields don't exist
|
|
807
|
-
class MyComponent {
|
|
808
|
-
protected readonly validationConfig = {
|
|
809
|
-
quantity: ['justification'], // justification only shows when quantity > 5!
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
````
|
|
813
|
-
|
|
814
|
-
#### The Solution
|
|
815
|
-
|
|
816
|
-
Use a **computed signal** for `validationConfig`:
|
|
817
|
-
|
|
818
|
-
```typescript
|
|
819
|
-
import { Component, signal, computed } from '@angular/core';
|
|
820
|
-
|
|
821
|
-
@Component({
|
|
822
|
-
template: `
|
|
823
|
-
<form ngxVestForm [validationConfig]="validationConfig()" ...>
|
|
824
|
-
<input name="quantity" [ngModel]="formValue().quantity" />
|
|
825
|
-
|
|
826
|
-
@if ((formValue().quantity || 0) > 5) {
|
|
827
|
-
<textarea
|
|
828
|
-
name="justification"
|
|
829
|
-
[ngModel]="formValue().justification"
|
|
830
|
-
></textarea>
|
|
163
|
+
@if (wrapper.isTouched()) {
|
|
164
|
+
<span>Field was touched</span>
|
|
831
165
|
}
|
|
832
|
-
|
|
833
|
-
|
|
166
|
+
@if (wrapper.isPending()) {
|
|
167
|
+
<span>Validating...</span>
|
|
168
|
+
}
|
|
169
|
+
</ngx-control-wrapper>
|
|
170
|
+
`
|
|
834
171
|
})
|
|
835
|
-
export class MyComponent {
|
|
836
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
837
|
-
|
|
838
|
-
// ✅ Computed config only references controls that exist
|
|
839
|
-
protected readonly validationConfig = computed(() => {
|
|
840
|
-
const config: Record<string, string[]> = {};
|
|
841
|
-
|
|
842
|
-
// Only add dependency when field is in DOM
|
|
843
|
-
if ((this.formValue().quantity || 0) > 5) {
|
|
844
|
-
config['quantity'] = ['justification'];
|
|
845
|
-
config['justification'] = ['quantity']; // Bidirectional validation
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return config;
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
172
|
```
|
|
852
173
|
|
|
853
|
-
**
|
|
174
|
+
**Available state signals:**
|
|
854
175
|
|
|
855
|
-
-
|
|
856
|
-
-
|
|
857
|
-
-
|
|
858
|
-
-
|
|
176
|
+
- `isTouched()` / `isDirty()` — User interaction state
|
|
177
|
+
- `isValid()` / `isInvalid()` — Validation state
|
|
178
|
+
- `isPending()` — Async validation in progress
|
|
179
|
+
- `errorMessages()` / `warningMessages()` — Current validation messages
|
|
180
|
+
- `shouldShowErrors()` — Computed based on display mode and state
|
|
859
181
|
|
|
860
|
-
**
|
|
182
|
+
**Tip**: For async validations, use `createDebouncedPendingState()` to prevent "Validating..." messages from flashing when validation completes quickly (< 200ms).
|
|
861
183
|
|
|
862
|
-
|
|
863
|
-
<!-- Notice the function call: validationConfig() -->
|
|
864
|
-
<form ngxVestForm [validationConfig]="validationConfig()" ...>...</form>
|
|
865
|
-
```
|
|
184
|
+
📖 **[Complete Guide: Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)**
|
|
866
185
|
|
|
867
|
-
|
|
186
|
+
## Advanced Features
|
|
868
187
|
|
|
869
|
-
|
|
188
|
+
### Validation Config
|
|
870
189
|
|
|
871
|
-
|
|
190
|
+
Automatically re-validate dependent fields when another field changes. Essential when using Vest.js's `omitWhen`/`skipWhen` for conditional validations.
|
|
872
191
|
|
|
873
|
-
**
|
|
192
|
+
**When to use**: Password confirmation, conditional required fields, or any field that depends on another field's value.
|
|
874
193
|
|
|
875
194
|
```typescript
|
|
876
|
-
// ❌ Verbose, error-prone, no type safety
|
|
877
195
|
protected readonly validationConfig = {
|
|
878
|
-
'password': ['confirmPassword'],
|
|
879
|
-
'
|
|
880
|
-
'startDate': ['endDate'],
|
|
881
|
-
'endDate': ['startDate'],
|
|
882
|
-
'country': ['state', 'zipCode'],
|
|
196
|
+
'password': ['confirmPassword'], // When password changes, re-validate confirmPassword
|
|
197
|
+
'age': ['emergencyContact'] // When age changes, re-validate emergencyContact
|
|
883
198
|
};
|
|
884
199
|
```
|
|
885
200
|
|
|
886
|
-
**
|
|
887
|
-
|
|
888
|
-
```typescript
|
|
889
|
-
import { createValidationConfig } from 'ngx-vest-forms';
|
|
890
|
-
|
|
891
|
-
// ✅ Clean, type-safe, self-documenting
|
|
892
|
-
protected readonly validationConfig = createValidationConfig<MyFormModel>()
|
|
893
|
-
.bidirectional('password', 'confirmPassword')
|
|
894
|
-
.bidirectional('startDate', 'endDate')
|
|
895
|
-
.whenChanged('country', ['state', 'zipCode'])
|
|
896
|
-
.build();
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
#### Builder Methods
|
|
900
|
-
|
|
901
|
-
**`whenChanged(trigger, revalidate)`** - One-way dependency
|
|
902
|
-
|
|
903
|
-
```typescript
|
|
904
|
-
// When country changes, revalidate state and zipCode
|
|
905
|
-
.whenChanged('country', ['state', 'zipCode'])
|
|
906
|
-
```
|
|
201
|
+
**Important**: `validationConfig` only triggers re-validation—validation logic is always defined in your Vest suite.
|
|
907
202
|
|
|
908
|
-
|
|
203
|
+
📖 **[Complete Guide: ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)**
|
|
909
204
|
|
|
910
|
-
|
|
911
|
-
// When either password or confirmPassword changes, revalidate the other
|
|
912
|
-
.bidirectional('password', 'confirmPassword')
|
|
913
|
-
```
|
|
205
|
+
### Root-Form Validation
|
|
914
206
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
```typescript
|
|
918
|
-
// When any contact field changes, revalidate all others
|
|
919
|
-
.group(['firstName', 'lastName', 'email'])
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
**`merge(config)`** - Combine configurations
|
|
923
|
-
|
|
924
|
-
```typescript
|
|
925
|
-
// Conditionally merge additional config
|
|
926
|
-
.merge(isInternational() ? { country: ['customsForm'] } : {})
|
|
927
|
-
```
|
|
207
|
+
Form-level validation rules that don't belong to any specific field (e.g., "at least one contact method required").
|
|
928
208
|
|
|
929
|
-
|
|
209
|
+
**When to use**: Business rules that evaluate multiple fields but errors should appear at form level, not on individual fields.
|
|
930
210
|
|
|
931
211
|
```typescript
|
|
932
|
-
import {
|
|
933
|
-
import { createValidationConfig, type DeepPartial } from 'ngx-vest-forms';
|
|
934
|
-
|
|
935
|
-
type OrderFormModel = DeepPartial<{
|
|
936
|
-
// Customer info
|
|
937
|
-
firstName: string;
|
|
938
|
-
lastName: string;
|
|
939
|
-
email: string;
|
|
940
|
-
|
|
941
|
-
// Password
|
|
942
|
-
password: string;
|
|
943
|
-
confirmPassword: string;
|
|
944
|
-
|
|
945
|
-
// Dates
|
|
946
|
-
startDate: Date;
|
|
947
|
-
endDate: Date;
|
|
948
|
-
|
|
949
|
-
// Location
|
|
950
|
-
country: string;
|
|
951
|
-
state: string;
|
|
952
|
-
zipCode: string;
|
|
953
|
-
}>;
|
|
954
|
-
|
|
955
|
-
@Component({
|
|
956
|
-
// ...
|
|
957
|
-
})
|
|
958
|
-
export class OrderFormComponent {
|
|
959
|
-
protected readonly validationConfig = createValidationConfig<OrderFormModel>()
|
|
960
|
-
// Customer info group
|
|
961
|
-
.group(['firstName', 'lastName', 'email'])
|
|
962
|
-
|
|
963
|
-
// Password confirmation
|
|
964
|
-
.bidirectional('password', 'confirmPassword')
|
|
965
|
-
|
|
966
|
-
// Date range
|
|
967
|
-
.bidirectional('startDate', 'endDate')
|
|
968
|
-
|
|
969
|
-
// Location dependencies
|
|
970
|
-
.whenChanged('country', ['state', 'zipCode'])
|
|
971
|
-
|
|
972
|
-
.build();
|
|
973
|
-
}
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
#### Benefits
|
|
977
|
-
|
|
978
|
-
- ✅ **Type Safety**: IDE autocomplete for all field paths
|
|
979
|
-
- ✅ **Readability**: Intent is clear from method names
|
|
980
|
-
- ✅ **Maintainability**: Less boilerplate, easier to understand
|
|
981
|
-
- ✅ **Error Prevention**: Compile-time validation of field names
|
|
982
|
-
- ✅ **Backward Compatible**: Works alongside manual configurations
|
|
983
|
-
|
|
984
|
-
#### Combining with Computed Signals
|
|
985
|
-
|
|
986
|
-
For dynamic configurations, combine the builder with computed signals:
|
|
212
|
+
import { ROOT_FORM } from 'ngx-vest-forms';
|
|
987
213
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
// Conditionally add dependencies
|
|
993
|
-
.merge(
|
|
994
|
-
this.isInternational()
|
|
995
|
-
? { country: ['customsForm', 'taxId'] }
|
|
996
|
-
: {}
|
|
997
|
-
)
|
|
998
|
-
.build()
|
|
999
|
-
);
|
|
214
|
+
// In your Vest suite
|
|
215
|
+
test(ROOT_FORM, 'At least one contact method is required', () => {
|
|
216
|
+
enforce(model.email || model.phone).isTruthy();
|
|
217
|
+
});
|
|
1000
218
|
```
|
|
1001
219
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
Dynamic forms present two distinct challenges that require different solutions:
|
|
1009
|
-
|
|
1010
|
-
| Challenge | Solution | When to Use |
|
|
1011
|
-
| --------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
1012
|
-
| **Conditional fields with validation dependencies** | [Computed `validationConfig`](#dependent-field-validation-with-conditional-rendering) | Fields depend on each other AND are conditionally rendered |
|
|
1013
|
-
| **Structure changes without value changes** | [`triggerFormValidation()`](#handling-form-structure-changes) | Switching between form inputs and non-form content |
|
|
1014
|
-
|
|
1015
|
-
> **💡 Pro Tip**: These solutions are complementary and often used together in complex forms with both conditional validation dependencies and dynamic structure changes.
|
|
1016
|
-
|
|
1017
|
-
### Handling Form Structure Changes
|
|
1018
|
-
|
|
1019
|
-
When form structure changes dynamically (e.g., switching between form inputs and non-form elements like `<p>` tags), use `triggerFormValidation()` to manually update validation state:
|
|
1020
|
-
|
|
1021
|
-
```typescript
|
|
1022
|
-
@Component({
|
|
1023
|
-
template: `
|
|
1024
|
-
<form ngxVestForm [suite]="suite" #vestForm="ngxVestForm">
|
|
1025
|
-
<select
|
|
1026
|
-
name="type"
|
|
1027
|
-
[ngModel]="formValue().type"
|
|
1028
|
-
(ngModelChange)="onTypeChange($event)"
|
|
1029
|
-
>
|
|
1030
|
-
<option value="typeA">Type A</option>
|
|
1031
|
-
<option value="typeC">Type C (no input)</option>
|
|
1032
|
-
</select>
|
|
1033
|
-
|
|
1034
|
-
@if (formValue().type === 'typeA') {
|
|
1035
|
-
<input name="fieldA" [ngModel]="formValue().fieldA" />
|
|
1036
|
-
} @else {
|
|
1037
|
-
<p>No additional input required.</p>
|
|
1038
|
-
}
|
|
1039
|
-
</form>
|
|
1040
|
-
`,
|
|
1041
|
-
})
|
|
1042
|
-
class MyComponent {
|
|
1043
|
-
protected readonly vestFormRef = viewChild.required('vestForm', {
|
|
1044
|
-
read: FormDirective,
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
protected onTypeChange(type: string) {
|
|
1048
|
-
this.formValue.update((v) => {
|
|
1049
|
-
const updated = clearFieldsWhen(v, { fieldA: type !== 'typeA' });
|
|
1050
|
-
return { ...updated, type };
|
|
1051
|
-
});
|
|
1052
|
-
this.vestFormRef().triggerFormValidation();
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
220
|
+
```html
|
|
221
|
+
<!-- In template -->
|
|
222
|
+
<form ngxVestForm ngxValidateRootForm [suite]="suite">
|
|
223
|
+
<!-- Show form-level errors -->
|
|
224
|
+
<div *ngIf="vestForm.errors?.rootForm">{{ vestForm.errors.rootForm }}</div>
|
|
225
|
+
</form>
|
|
1055
226
|
```
|
|
1056
227
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
**When to use `triggerFormValidation()`**: After form structure changes (showing/hiding fields), clearing form sections, or switching between form inputs and non-form content.
|
|
228
|
+
📖 **[Complete Guide: ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)**
|
|
1060
229
|
|
|
1061
|
-
|
|
230
|
+
### Dynamic Form Structure
|
|
1062
231
|
|
|
1063
|
-
|
|
232
|
+
Manually trigger validation when form structure changes between **input fields and non-input content** (like `<p>` tags) without value changes.
|
|
1064
233
|
|
|
1065
|
-
**When
|
|
234
|
+
**When to use**: When switching from form controls to informational text/paragraphs where no control values change.
|
|
1066
235
|
|
|
1067
|
-
**
|
|
236
|
+
**NOT needed when**: Switching between different input fields (value changes trigger validation automatically).
|
|
1068
237
|
|
|
1069
238
|
```typescript
|
|
1070
|
-
//
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
this.formValue.update((v) =>
|
|
1074
|
-
clearFieldsWhen(v, {
|
|
1075
|
-
fieldA: procedureType !== 'typeA', // Clear when NOT showing input
|
|
1076
|
-
})
|
|
1077
|
-
);
|
|
1078
|
-
```
|
|
1079
|
-
|
|
1080
|
-
**When NOT required**: Pure form-to-form conditionals (switching input types with same `name`) – Angular maintains FormControl throughout.
|
|
1081
|
-
|
|
1082
|
-
##### When Field Clearing is NOT Required
|
|
1083
|
-
|
|
1084
|
-
Pure form-to-form conditionals (switching input types with same `name`) usually don't need field clearing because Angular maintains the FormControl throughout:
|
|
1085
|
-
|
|
1086
|
-
```typescript
|
|
1087
|
-
// These switches DON'T require field clearing:
|
|
1088
|
-
@if (inputType === 'text') {
|
|
1089
|
-
<input name="field" [ngModel]="formValue().field" type="text" />
|
|
239
|
+
// Example: Switching from input to paragraph
|
|
240
|
+
@if (type() === 'typeA') {
|
|
241
|
+
<input name="fieldA" [ngModel]="formValue().fieldA" />
|
|
1090
242
|
} @else {
|
|
1091
|
-
<input
|
|
243
|
+
<p>No input required</p> // ← No form control, needs triggerFormValidation()
|
|
1092
244
|
}
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
> **📖 Complete Guide**: See **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** for detailed patterns and use cases.
|
|
1096
|
-
|
|
1097
|
-
### Field State Utilities
|
|
1098
|
-
|
|
1099
|
-
When building dynamic forms, you often need to conditionally show/hide form inputs or switch between form inputs and non-form content (like informational text). ngx-vest-forms provides specialized utilities to keep your component state synchronized with Angular's form state during these transitions.
|
|
1100
|
-
|
|
1101
|
-
**Key Utilities:**
|
|
1102
|
-
|
|
1103
|
-
- **`clearFieldsWhen(state, conditions)`** - Conditionally clear fields based on boolean conditions (most common)
|
|
1104
|
-
- **`clearFields(state, fieldArray)`** - Unconditionally clear specific fields (for reset operations)
|
|
1105
|
-
- **`keepFieldsWhen(state, conditions)`** - Keep only fields that meet conditions (whitelist approach)
|
|
1106
|
-
|
|
1107
|
-
**When to Use:**
|
|
1108
|
-
|
|
1109
|
-
These utilities are primarily needed when your template conditionally renders form inputs in some branches and non-form content (like `<p>` tags) in others. They ensure your component state stays consistent with the actual form structure.
|
|
1110
|
-
|
|
1111
|
-
**Example:**
|
|
1112
|
-
|
|
1113
|
-
```typescript
|
|
1114
|
-
import { clearFieldsWhen } from 'ngx-vest-forms';
|
|
1115
|
-
|
|
1116
|
-
// Clear shipping address when switching from input to "No shipping needed" message
|
|
1117
|
-
const updatedState = clearFieldsWhen(formValue(), {
|
|
1118
|
-
'addresses.shippingAddress': !useShippingAddress,
|
|
1119
|
-
emergencyContact: age >= 18, // Clear when adult (no emergency contact input shown)
|
|
1120
|
-
});
|
|
1121
|
-
```
|
|
1122
|
-
|
|
1123
|
-
**Important Note:**
|
|
1124
|
-
|
|
1125
|
-
Pure form-to-form conditionals (switching between different input types with the same `name`) typically don't require these utilities as Angular maintains the FormControl throughout the transition.
|
|
1126
|
-
|
|
1127
|
-
> **📖 Detailed Guide**: See **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** for comprehensive examples, use cases, and when field clearing is required vs. not required.
|
|
1128
|
-
|
|
1129
|
-
### Composable Validations
|
|
1130
245
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
// Reusable validation function
|
|
1135
|
-
export function addressValidations(
|
|
1136
|
-
model: AddressModel | undefined,
|
|
1137
|
-
field: string
|
|
1138
|
-
): void {
|
|
1139
|
-
test(`${field}.street`, 'Street is required', () => {
|
|
1140
|
-
enforce(model?.street).isNotBlank();
|
|
1141
|
-
});
|
|
1142
|
-
test(`${field}.city`, 'City is required', () => {
|
|
1143
|
-
enforce(model?.city).isNotBlank();
|
|
1144
|
-
});
|
|
1145
|
-
// ... more validations
|
|
246
|
+
onTypeChange(newType: string) {
|
|
247
|
+
this.formValue.update(v => ({ ...v, type: newType }));
|
|
248
|
+
this.vestForm.triggerFormValidation(); // Only needed for structure changes
|
|
1146
249
|
}
|
|
1147
|
-
|
|
1148
|
-
// Use in your suite
|
|
1149
|
-
export const orderSuite: NgxVestSuite<OrderFormModel> = staticSuite(
|
|
1150
|
-
(model, field?) => {
|
|
1151
|
-
only(field);
|
|
1152
|
-
addressValidations(model.billingAddress, 'billingAddress');
|
|
1153
|
-
addressValidations(model.shippingAddress, 'shippingAddress');
|
|
1154
|
-
}
|
|
1155
|
-
);
|
|
1156
250
|
```
|
|
1157
251
|
|
|
1158
|
-
**
|
|
1159
|
-
|
|
1160
|
-
- ✅ **Reusability** - Share validation logic across different forms
|
|
1161
|
-
- ✅ **Maintainability** - Update validation logic in one place
|
|
1162
|
-
- ✅ **Testability** - Test validation functions independently
|
|
1163
|
-
- ✅ **Cross-framework** - Use same logic on frontend/backend, Angular/React
|
|
252
|
+
📖 **[Complete Guide: Structure Change Detection](./docs/STRUCTURE_CHANGE_DETECTION.md)**
|
|
1164
253
|
|
|
1165
|
-
|
|
254
|
+
### Shape Validation (Development Mode)
|
|
1166
255
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
Create your own error display components using the `FormErrorDisplayDirective`, which provides all the validation state you need:
|
|
256
|
+
In development mode, ngx-vest-forms validates that your form's structure matches your TypeScript model, catching common mistakes early:
|
|
1170
257
|
|
|
1171
258
|
```typescript
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
<ng-content />
|
|
1178
|
-
@if (errorDisplay.shouldShowErrors()) {
|
|
1179
|
-
<div class="error">
|
|
1180
|
-
@for (error of errorDisplay.errors(); track error) {
|
|
1181
|
-
<span>{{ error }}</span>
|
|
1182
|
-
}
|
|
1183
|
-
</div>
|
|
1184
|
-
}
|
|
1185
|
-
@if (errorDisplay.isPending()) {
|
|
1186
|
-
<div class="validating">Validating...</div>
|
|
1187
|
-
}
|
|
1188
|
-
</div>
|
|
1189
|
-
`,
|
|
1190
|
-
})
|
|
1191
|
-
export class CustomWrapperComponent {
|
|
1192
|
-
protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
|
|
1193
|
-
self: true,
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
**When to create custom wrappers:**
|
|
1199
|
-
|
|
1200
|
-
- Match your design system (Material, PrimeNG, etc.)
|
|
1201
|
-
- Custom error formatting (tooltips, popovers, inline)
|
|
1202
|
-
- Add UI elements (icons, help text, character counters)
|
|
1203
|
-
- Specific accessibility patterns
|
|
1204
|
-
|
|
1205
|
-
> **📖 Detailed Guide**: See **[Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)** for Material Design examples, available signals reference, and best practices.
|
|
1206
|
-
|
|
1207
|
-
### Cross-Field Validation: Three Complementary Features
|
|
1208
|
-
|
|
1209
|
-
ngx-vest-forms provides three features for handling validation in complex, dynamic forms:
|
|
1210
|
-
|
|
1211
|
-
> **💡 Key Insight**: `validationConfig` is **essential** when using Vest.js's `omitWhen`/`skipWhen` for conditional validations. It ensures Angular re-validates dependent fields when conditions change.
|
|
1212
|
-
|
|
1213
|
-
| Feature | Purpose | Errors Appear At | Use For |
|
|
1214
|
-
| ----------------------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------- |
|
|
1215
|
-
| **`validationConfig`** | **Re-validation trigger** when fields change | **Field level** (`errors.fieldName`) | Fields that need re-validation when others change |
|
|
1216
|
-
| **`validateRootForm`** | Creates **form-level** validations | **Form level** (`errors.rootForm`) | Form-wide business rules |
|
|
1217
|
-
| **`triggerFormValidation()`** | Manual validation trigger for structure changes | N/A (triggers existing validations) | Structure changes without value changes |
|
|
1218
|
-
|
|
1219
|
-
**Key Insight**: These solve different problems and often work together!
|
|
1220
|
-
|
|
1221
|
-
> **📖 Complete Guide**: See **[Validation Features Guide](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)** for detailed comparison, decision trees, use cases, and examples of using all three features together.
|
|
1222
|
-
|
|
1223
|
-
**Quick Examples:**
|
|
259
|
+
// Your model
|
|
260
|
+
type MyFormModel = NgxDeepPartial<{
|
|
261
|
+
email: string;
|
|
262
|
+
address: { street: string; city: string };
|
|
263
|
+
}>;
|
|
1224
264
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
password: ['confirmPassword'], // When password changes, revalidate confirmPassword
|
|
265
|
+
// Define shape for runtime validation
|
|
266
|
+
const shape: NgxDeepRequired<MyFormModel> = {
|
|
267
|
+
email: '',
|
|
268
|
+
address: { street: '', city: '' },
|
|
1230
269
|
};
|
|
1231
|
-
// The actual validation logic is in your Vest suite:
|
|
1232
|
-
test('confirmPassword', 'Must match', () => {
|
|
1233
|
-
enforce(model.confirmPassword).equals(model.password);
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
// validateRootForm: Form-level business rules
|
|
1237
|
-
test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
|
|
1238
|
-
enforce(
|
|
1239
|
-
model.firstName === 'Brecht' &&
|
|
1240
|
-
model.lastName === 'Billiet' &&
|
|
1241
|
-
model.age === 30
|
|
1242
|
-
).isFalsy();
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
// triggerFormValidation(): After structure changes
|
|
1246
|
-
protected readonly vestFormRef = viewChild.required('vestForm', { read: FormDirective });
|
|
1247
|
-
|
|
1248
|
-
onTypeChange(type: string) {
|
|
1249
|
-
this.formValue.update(v => ({ ...v, type }));
|
|
1250
|
-
this.vestFormRef().triggerFormValidation(); // ✅ Force validation update
|
|
1251
|
-
}
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
### Validations on the Root Form
|
|
1255
|
-
|
|
1256
|
-
For form-level validations that span multiple fields, use the `ROOT_FORM` constant with `validateRootForm`:
|
|
1257
|
-
|
|
1258
|
-
> **⚠️ Breaking Change (v3)**: Default validation mode changed from `'live'` to `'submit'`. See [Migration Guide](./docs/MIGRATION-V3.md) for details.
|
|
1259
|
-
|
|
1260
|
-
```typescript
|
|
1261
|
-
import { ROOT_FORM } from 'ngx-vest-forms';
|
|
1262
|
-
|
|
1263
|
-
// In your suite
|
|
1264
|
-
test(ROOT_FORM, 'Passwords must match', () => {
|
|
1265
|
-
enforce(model.confirmPassword).equals(model.password);
|
|
1266
|
-
});
|
|
1267
270
|
```
|
|
1268
271
|
|
|
1269
272
|
```html
|
|
1270
|
-
<form
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
[
|
|
1274
|
-
[suite]="suite"
|
|
1275
|
-
(errorsChange)="errors.set($event)"
|
|
1276
|
-
>
|
|
1277
|
-
<!-- Display root-level errors -->
|
|
1278
|
-
{{ errors()?.['rootForm'] }}
|
|
1279
|
-
</form>
|
|
1280
|
-
```
|
|
1281
|
-
|
|
1282
|
-
#### Validation Modes
|
|
1283
|
-
|
|
1284
|
-
Root form validation supports two modes:
|
|
273
|
+
<form ngxVestForm [suite]="suite" [formShape]="shape">
|
|
274
|
+
<!-- ✅ Correct: matches shape -->
|
|
275
|
+
<input name="email" [ngModel]="formValue().email" />
|
|
276
|
+
<input name="address.street" [ngModel]="formValue().address?.street" />
|
|
1285
277
|
|
|
1286
|
-
|
|
1287
|
-
|
|
278
|
+
<!-- ❌ Error in dev mode: typo detected -->
|
|
279
|
+
<input name="emial" [ngModel]="formValue().email" />
|
|
1288
280
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
<form ngxVestForm ngxValidateRootForm [ngxValidateRootFormMode]="'submit'">
|
|
1292
|
-
<!-- form controls -->
|
|
1293
|
-
</form>
|
|
1294
|
-
|
|
1295
|
-
<!-- Live mode - validates immediately -->
|
|
1296
|
-
<form ngxVestForm ngxValidateRootForm [ngxValidateRootFormMode]="'live'">
|
|
1297
|
-
<!-- form controls -->
|
|
281
|
+
<!-- ❌ Error in dev mode: path doesn't exist in shape -->
|
|
282
|
+
<input name="address.zipcode" [ngModel]="formValue().address?.zipcode" />
|
|
1298
283
|
</form>
|
|
1299
284
|
```
|
|
1300
285
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
When field validations depend on other fields (e.g., `confirmPassword` depends on `password`), use `validationConfig` to trigger re-validation:
|
|
1304
|
-
|
|
1305
|
-
> **📖 When to use `validationConfig` vs `validateRootForm`**: See **[ValidationConfig vs ROOT_FORM Validation](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)** for a complete comparison and decision tree.
|
|
1306
|
-
|
|
1307
|
-
```typescript
|
|
1308
|
-
// In your suite - Vest handles the logic
|
|
1309
|
-
omitWhen(!model.password, () => {
|
|
1310
|
-
test('confirmPassword', 'Passwords must match', () => {
|
|
1311
|
-
enforce(model.confirmPassword).equals(model.password);
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
```typescript
|
|
1317
|
-
// In your component - validationConfig handles Angular orchestration
|
|
1318
|
-
protected validationConfig = {
|
|
1319
|
-
password: ['confirmPassword'] // When password changes, revalidate confirmPassword
|
|
1320
|
-
};
|
|
1321
|
-
```
|
|
1322
|
-
|
|
1323
|
-
```html
|
|
1324
|
-
<form ngxVestForm [suite]="suite" [validationConfig]="validationConfig">
|
|
1325
|
-
<input name="password" [ngModel]="formValue().password" />
|
|
1326
|
-
<input name="confirmPassword" [ngModel]="formValue().confirmPassword" />
|
|
1327
|
-
</form>
|
|
1328
|
-
```
|
|
286
|
+
**Benefits:**
|
|
1329
287
|
|
|
1330
|
-
|
|
1331
|
-
|
|
288
|
+
- Catch typos in `name` attributes immediately during development
|
|
289
|
+
- Ensure template structure matches TypeScript model
|
|
290
|
+
- Zero runtime cost in production (checks disabled automatically)
|
|
291
|
+
- Works with nested objects and arrays
|
|
1332
292
|
|
|
1333
|
-
|
|
293
|
+
**Important**: Shape validation only runs in development mode (`isDevMode()` returns `true`). Production builds have zero overhead.
|
|
1334
294
|
|
|
1335
|
-
|
|
295
|
+
📖 **[Complete Guide: Field Paths](./docs/FIELD-PATHS.md)**
|
|
1336
296
|
|
|
1337
|
-
|
|
297
|
+
## Documentation
|
|
1338
298
|
|
|
1339
|
-
|
|
299
|
+
### Getting Started
|
|
1340
300
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
|
|
301
|
+
- **[Complete Example](./docs/COMPLETE-EXAMPLE.md)** - Step-by-step walkthrough from basic form to advanced patterns
|
|
302
|
+
- **[Composable Validations](./docs/COMPOSABLE-VALIDATIONS.md)** - Break validation logic into reusable, testable functions
|
|
1344
303
|
|
|
1345
|
-
|
|
1346
|
-
selector: 'app-address',
|
|
1347
|
-
viewProviders: [vestFormsViewProviders], // ⚠️ REQUIRED for child form components
|
|
1348
|
-
template: `
|
|
1349
|
-
<ngx-control-wrapper>
|
|
1350
|
-
<label>Street</label>
|
|
1351
|
-
<input [ngModel]="address().street" name="street" />
|
|
1352
|
-
</ngx-control-wrapper>
|
|
1353
|
-
<!-- More address fields... -->
|
|
1354
|
-
`,
|
|
1355
|
-
})
|
|
1356
|
-
export class AddressComponent {
|
|
1357
|
-
readonly address = input<AddressModel>();
|
|
1358
|
-
}
|
|
1359
|
-
```
|
|
304
|
+
### Advanced Patterns
|
|
1360
305
|
|
|
1361
|
-
**
|
|
306
|
+
- **[ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)** - Cross-field dependencies and form-level rules
|
|
307
|
+
- **[Field Path Types](./docs/FIELD-PATHS.md)** - Type-safe dot-notation paths for nested properties
|
|
308
|
+
- **[Structure Change Detection](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Handle dynamic form structure updates
|
|
309
|
+
- **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** - Type-safe utilities for clearing nested form values
|
|
1362
310
|
|
|
1363
|
-
|
|
1364
|
-
- **Code Organization** - Keep large forms manageable by splitting them into logical sections
|
|
1365
|
-
- **Type Safety** - Each child component can have its own strongly-typed model
|
|
1366
|
-
- **Dynamic Field Names** - Use inputs to customize field name prefixes (e.g., `billingAddress.street` vs `shippingAddress.street`)
|
|
311
|
+
### UI & Integration
|
|
1367
312
|
|
|
1368
|
-
|
|
313
|
+
- **[Child Components](./docs/CHILD-COMPONENTS.md)** - Split large forms into smaller, maintainable components
|
|
314
|
+
- **[Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)** - Build consistent error display patterns
|
|
315
|
+
- **[API Tokens](./docs/API-TOKENS.md)** - Configure error display modes and other global settings
|
|
1369
316
|
|
|
1370
|
-
|
|
317
|
+
### Reference
|
|
1371
318
|
|
|
1372
|
-
|
|
319
|
+
- **[Utilities README](./projects/ngx-vest-forms/src/lib/utils/README.md)** - Canonical reference for all utility functions
|
|
1373
320
|
|
|
1374
|
-
###
|
|
321
|
+
### Examples
|
|
1375
322
|
|
|
1376
|
-
- **
|
|
1377
|
-
-
|
|
1378
|
-
-
|
|
1379
|
-
- **Zero Boilerplate** - Automatic FormControl and FormGroup creation
|
|
1380
|
-
- **Shape Validation** - Runtime validation against your TypeScript models (dev mode)
|
|
323
|
+
- **[Examples Project](./projects/examples)** - Working code examples with business hours forms, purchase forms, and validation config demos
|
|
324
|
+
- Run locally: `npm install && npm start`
|
|
325
|
+
- Includes smart components, UI components, and complete validation patterns
|
|
1381
326
|
|
|
1382
|
-
|
|
327
|
+
## Migration
|
|
1383
328
|
|
|
1384
|
-
-
|
|
1385
|
-
-
|
|
1386
|
-
- **Composable Suites** - Reusable validation functions across projects
|
|
1387
|
-
- **Custom Debouncing** - Configure validation timing per field or form
|
|
1388
|
-
- **Warnings Support** - Non-blocking feedback with Vest's `warn()` feature
|
|
1389
|
-
- **Performance Optimization** - Field-level validation with `only()` pattern
|
|
329
|
+
- v1.x → v2.0.0: **[Migration Guide](./docs/migration/MIGRATION-v1.x-to-v2.0.0.md)**
|
|
330
|
+
- Selector prefixes: **[Dual Selector Support](./docs/DUAL-SELECTOR-SUPPORT.md)**
|
|
1390
331
|
|
|
1391
|
-
|
|
332
|
+
Browser support follows Angular 19+ targets (no `structuredClone` polyfill required).
|
|
1392
333
|
|
|
1393
|
-
|
|
1394
|
-
- **Form Arrays** - Dynamic lists with add/remove functionality
|
|
1395
|
-
- **Reactive Disabling** - Disable fields based on computed signals
|
|
1396
|
-
- **State Management** - Preserve field state across conditional rendering
|
|
1397
|
-
- **Structure Change Detection** - Manual trigger for validation updates when form structure changes
|
|
334
|
+
## FAQ
|
|
1398
335
|
|
|
1399
|
-
###
|
|
336
|
+
### Do I need validations to use ngx-vest-forms?
|
|
1400
337
|
|
|
1401
|
-
|
|
1402
|
-
- **Flexible Error Display** - Built-in `ngx-control-wrapper` or create custom wrappers with `FormErrorDisplayDirective`
|
|
1403
|
-
- **Error Display Modes** - Control when errors show: on-blur, on-submit, or both
|
|
1404
|
-
- **Validation Config** - Declare field dependencies for complex scenarios
|
|
1405
|
-
- **Field State Utilities** - Helper functions for managing dynamic form state
|
|
1406
|
-
- **Modern Angular** - Built for Angular 18+ with standalone components and signals
|
|
338
|
+
No—but you’ll almost always want them. Common cases to start without a suite:
|
|
1407
339
|
|
|
1408
|
-
|
|
340
|
+
- Prototyping UI while deferring rules
|
|
341
|
+
- Gradual migration: adopt unidirectional state and type-safe models first
|
|
342
|
+
- Server-driven validation: display backend errors while you add a client suite later
|
|
1409
343
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
For comprehensive documentation beyond this README, check out our detailed guides:
|
|
1413
|
-
|
|
1414
|
-
- **[Accessibility Guide](./docs/ACCESSIBILITY.md)** - WCAG 2.2 AA compliance and best practices
|
|
1415
|
-
- Polite vs assertive ARIA live regions
|
|
1416
|
-
- Field-level and form-level error strategies
|
|
1417
|
-
- Implementation details and testing guidance
|
|
1418
|
-
- **[Field Path Types](./docs/FIELD-PATHS.md)** - Type-safe field references with IDE autocomplete
|
|
1419
|
-
- Template literal types for field paths
|
|
1420
|
-
- `ValidationConfigMap<T>` for type-safe configs
|
|
1421
|
-
- `FormFieldName<T>` for Vest suites
|
|
1422
|
-
- Migration guide and best practices
|
|
1423
|
-
- Performance considerations
|
|
1424
|
-
- **[Utility Types & Functions Reference](./projects/ngx-vest-forms/src/lib/utils/README.md)** - Complete guide to all public utility types and functions
|
|
1425
|
-
- Type utilities: `NgxDeepPartial`, `NgxDeepRequired`, `NgxFormCompatibleDeepRequired`
|
|
1426
|
-
- Form utilities: `setValueAtPath()`, `createEmptyFormState()`
|
|
1427
|
-
- Array/Object conversion: `arrayToObject()`, `deepArrayToObject()`, `objectToArray()`
|
|
1428
|
-
- Field path utilities: `stringifyFieldPath()`
|
|
1429
|
-
- Field clearing: `clearFieldsWhen()`, `clearFields()`, `keepFieldsWhen()`
|
|
1430
|
-
- Validation config builder: `createValidationConfig()`
|
|
1431
|
-
- **[Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Advanced handling of conditional form scenarios
|
|
1432
|
-
- Alternative approaches and their trade-offs
|
|
1433
|
-
- Performance considerations and best practices
|
|
1434
|
-
- Detailed API reference with examples
|
|
1435
|
-
- When and why to use `triggerFormValidation()`
|
|
1436
|
-
|
|
1437
|
-
### Coming Soon
|
|
1438
|
-
|
|
1439
|
-
- **Advanced Form Arrays Guide** - Dynamic lists, nested arrays, and complex scenarios
|
|
1440
|
-
- **Custom Validation Guide** - Building reusable validation suites and complex rules
|
|
1441
|
-
- **Performance Optimization Guide** - Tips and techniques for large-scale forms
|
|
344
|
+
You can add a Vest suite at any time by binding `[suite]` on the form.
|
|
1442
345
|
|
|
1443
346
|
## Resources
|
|
1444
347
|
|
|
@@ -1447,12 +350,9 @@ For comprehensive documentation beyond this README, check out our detailed guide
|
|
|
1447
350
|
- **[Angular Official Documentation](https://angular.dev/guide/forms)** - Template-driven forms guide
|
|
1448
351
|
- **[Vest.js Documentation](https://vestjs.dev)** - Validation framework used by ngx-vest-forms
|
|
1449
352
|
- **[Live Examples Repository](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples)** - Complex form examples and patterns
|
|
1450
|
-
- **[Interactive Stackblitz Demo](https://stackblitz.com/~/github.com/simplifiedcourses/ngx-vest-forms-stackblitz)** - Try it in your browser
|
|
1451
353
|
|
|
1452
354
|
### Running Examples Locally
|
|
1453
355
|
|
|
1454
|
-
Clone this repo and run the examples:
|
|
1455
|
-
|
|
1456
356
|
```bash
|
|
1457
357
|
npm install
|
|
1458
358
|
npm start
|
|
@@ -1460,8 +360,6 @@ npm start
|
|
|
1460
360
|
|
|
1461
361
|
### Learning Resources
|
|
1462
362
|
|
|
1463
|
-
[](https://www.simplified.courses/complex-angular-template-driven-forms)
|
|
1464
|
-
|
|
1465
363
|
**[Complex Angular Template-Driven Forms Course](https://www.simplified.courses/complex-angular-template-driven-forms)** - Master advanced form patterns and become a form expert.
|
|
1466
364
|
|
|
1467
365
|
### Founding Articles by Brecht Billiet
|
|
@@ -1473,12 +371,6 @@ This library was originally created by [Brecht Billiet](https://twitter.com/brec
|
|
|
1473
371
|
- **[Asynchronous Form Validators in Angular with Vest](https://blog.simplified.courses/asynchronous-form-validators-in-angular-with-vest/)** - Advanced async validation patterns
|
|
1474
372
|
- **[Template-Driven Forms with Form Arrays](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)** - Dynamic form arrays implementation
|
|
1475
373
|
|
|
1476
|
-
### Community & Support
|
|
1477
|
-
|
|
1478
|
-
- **[GitHub Issues](https://github.com/ngx-vest-forms/ngx-vest-forms/issues)** - Report bugs or request features
|
|
1479
|
-
- **[GitHub Discussions](https://github.com/ngx-vest-forms/ngx-vest-forms/discussions)** - Ask questions and share ideas
|
|
1480
|
-
- **[npm Package](https://www.npmjs.com/package/ngx-vest-forms)** - Official package page
|
|
1481
|
-
|
|
1482
374
|
## Developer Resources
|
|
1483
375
|
|
|
1484
376
|
### Comprehensive Instruction Files
|
|
@@ -1489,33 +381,6 @@ This project includes detailed instruction files designed to help developers mas
|
|
|
1489
381
|
- **[`.github/instructions/vest.instructions.md`](.github/instructions/vest.instructions.md)** - Comprehensive Vest.js validation patterns and best practices
|
|
1490
382
|
- **[`.github/copilot-instructions.md`](.github/copilot-instructions.md)** - Main GitHub Copilot instructions for this workspace
|
|
1491
383
|
|
|
1492
|
-
### Using Instruction Files in Your Workspace
|
|
1493
|
-
|
|
1494
|
-
For the best development experience with ngx-vest-forms, **copy these instruction files to your own project's `.github/` directory**:
|
|
1495
|
-
|
|
1496
|
-
```bash
|
|
1497
|
-
# Create the directories in your project
|
|
1498
|
-
mkdir -p .github/instructions
|
|
1499
|
-
|
|
1500
|
-
# Copy the instruction files
|
|
1501
|
-
curl -o .github/instructions/ngx-vest-forms.instructions.md \
|
|
1502
|
-
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/instructions/ngx-vest-forms.instructions.md
|
|
1503
|
-
|
|
1504
|
-
curl -o .github/instructions/vest.instructions.md \
|
|
1505
|
-
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/instructions/vest.instructions.md
|
|
1506
|
-
|
|
1507
|
-
# Optionally, adapt the main copilot instructions for your project
|
|
1508
|
-
curl -o .github/copilot-instructions.md \
|
|
1509
|
-
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/copilot-instructions.md
|
|
1510
|
-
```
|
|
1511
|
-
|
|
1512
|
-
**Benefits of copying instruction files:**
|
|
1513
|
-
|
|
1514
|
-
- **GitHub Copilot Integration** - Enhanced code generation aligned with best practices
|
|
1515
|
-
- **Comprehensive Documentation** - Complete patterns and examples at your fingertips
|
|
1516
|
-
- **Consistent Code Quality** - Maintain validation patterns and architectural standards
|
|
1517
|
-
- **Faster Development** - Quick reference for complex scenarios and optimizations
|
|
1518
|
-
|
|
1519
384
|
## Acknowledgments
|
|
1520
385
|
|
|
1521
386
|
🙏 **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.
|