ngx-vest-forms 1.4.2 → 1.5.0-beta.1
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 +521 -988
- package/fesm2022/ngx-vest-forms.mjs +984 -409
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +983 -206
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,33 @@
|
|
|
20
20
|
> [!NOTE]
|
|
21
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
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:
|
|
35
|
+
>
|
|
36
|
+
> ```typescript
|
|
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
|
+
|
|
23
50
|
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.
|
|
24
51
|
|
|
25
52
|
> [!TIP]
|
|
@@ -61,7 +88,18 @@ protected readonly suite = myValidationSuite;
|
|
|
61
88
|
- **Angular**: >=18.0.0 (Signals support required)
|
|
62
89
|
- **Vest.js**: >=5.4.6 (Validation engine)
|
|
63
90
|
- **TypeScript**: >=5.8.0 (Modern Angular features)
|
|
64
|
-
- **Node.js**: >=
|
|
91
|
+
- **Node.js**: >=18.19.0 (Required for Angular 18+)
|
|
92
|
+
|
|
93
|
+
### Browser Support
|
|
94
|
+
|
|
95
|
+
ngx-vest-forms supports all modern browsers that Angular 18+ targets:
|
|
96
|
+
|
|
97
|
+
- **Chrome**: 98+ (includes `structuredClone()` support)
|
|
98
|
+
- **Firefox**: 94+ (includes `structuredClone()` support)
|
|
99
|
+
- **Safari**: 15.4+ (includes `structuredClone()` support)
|
|
100
|
+
- **Edge**: 98+ (includes `structuredClone()` support)
|
|
101
|
+
|
|
102
|
+
> **Note**: The library uses native `structuredClone()` for object cloning, which is fully supported in all Angular 18+ target environments. No polyfill is required.
|
|
65
103
|
|
|
66
104
|
### Installation
|
|
67
105
|
|
|
@@ -77,9 +115,9 @@ Create your first ngx-vest-forms component in 3 simple steps:
|
|
|
77
115
|
|
|
78
116
|
```typescript
|
|
79
117
|
import { signal } from '@angular/core';
|
|
80
|
-
import { vestForms,
|
|
118
|
+
import { vestForms, NgxDeepPartial } from 'ngx-vest-forms';
|
|
81
119
|
|
|
82
|
-
type MyFormModel =
|
|
120
|
+
type MyFormModel = NgxDeepPartial<{
|
|
83
121
|
generalInfo: {
|
|
84
122
|
firstName: string;
|
|
85
123
|
lastName: string;
|
|
@@ -92,10 +130,10 @@ type MyFormModel = DeepPartial<{
|
|
|
92
130
|
Use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow:
|
|
93
131
|
|
|
94
132
|
```typescript
|
|
95
|
-
import { vestForms,
|
|
133
|
+
import { vestForms, NgxDeepPartial } from 'ngx-vest-forms';
|
|
96
134
|
|
|
97
135
|
// A form model is always deep partial because angular will create it over time organically
|
|
98
|
-
type MyFormModel =
|
|
136
|
+
type MyFormModel = NgxDeepPartial<{
|
|
99
137
|
generalInfo: {
|
|
100
138
|
firstName: string;
|
|
101
139
|
lastName: string;
|
|
@@ -141,141 +179,78 @@ Your form automatically creates FormGroups and FormControls with type-safe, unid
|
|
|
141
179
|
> [!IMPORTANT]
|
|
142
180
|
> Notice we use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow, and the `?` operator since template-driven forms are `DeepPartial`.
|
|
143
181
|
|
|
144
|
-
|
|
182
|
+
### Minimal Example
|
|
145
183
|
|
|
146
|
-
|
|
184
|
+
Here's the absolute minimum to get started with ngx-vest-forms:
|
|
147
185
|
|
|
148
186
|
```typescript
|
|
149
|
-
import { Component, signal
|
|
150
|
-
import {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// 1. Define your form model (always DeepPartial)
|
|
154
|
-
type UserFormModel = DeepPartial<{
|
|
155
|
-
firstName: string;
|
|
156
|
-
lastName: string;
|
|
187
|
+
import { Component, signal } from '@angular/core';
|
|
188
|
+
import { vestForms, NgxDeepPartial } from 'ngx-vest-forms';
|
|
189
|
+
|
|
190
|
+
type SimpleForm = NgxDeepPartial<{
|
|
157
191
|
email: string;
|
|
192
|
+
name: string;
|
|
158
193
|
}>;
|
|
159
194
|
|
|
160
|
-
// 2. Create a shape for runtime validation (recommended)
|
|
161
|
-
const userFormShape: DeepRequired<UserFormModel> = {
|
|
162
|
-
firstName: '',
|
|
163
|
-
lastName: '',
|
|
164
|
-
email: '',
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// 3. Create a Vest validation suite
|
|
168
|
-
const userValidationSuite = staticSuite(
|
|
169
|
-
(model: UserFormModel, field?: string) => {
|
|
170
|
-
if (field) {
|
|
171
|
-
only(field); // Critical for performance - only validate the active field
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
test('firstName', 'First name is required', () => {
|
|
175
|
-
enforce(model.firstName).isNotBlank();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('lastName', 'Last name is required', () => {
|
|
179
|
-
enforce(model.lastName).isNotBlank();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('email', 'Valid email is required', () => {
|
|
183
|
-
enforce(model.email).isEmail();
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// 4. Create the component
|
|
189
195
|
@Component({
|
|
190
|
-
selector: 'app-
|
|
196
|
+
selector: 'app-simple-form',
|
|
191
197
|
imports: [vestForms],
|
|
192
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
193
198
|
template: `
|
|
194
199
|
<form
|
|
195
200
|
scVestForm
|
|
196
|
-
[suite]="suite"
|
|
197
|
-
[formShape]="shape"
|
|
198
201
|
(formValueChange)="formValue.set($event)"
|
|
199
202
|
(ngSubmit)="onSubmit()"
|
|
200
203
|
>
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
<label>First Name</label>
|
|
204
|
-
<input [ngModel]="formValue().firstName" name="firstName" />
|
|
205
|
-
</div>
|
|
204
|
+
<label for="email">Email</label>
|
|
205
|
+
<input id="email" name="email" [ngModel]="formValue().email" />
|
|
206
206
|
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
<input [ngModel]="formValue().lastName" name="lastName" />
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<div sc-control-wrapper>
|
|
213
|
-
<label>Email</label>
|
|
214
|
-
<input [ngModel]="formValue().email" name="email" type="email" />
|
|
215
|
-
</div>
|
|
207
|
+
<label for="name">Name</label>
|
|
208
|
+
<input id="name" name="name" [ngModel]="formValue().name" />
|
|
216
209
|
|
|
217
210
|
<button type="submit">Submit</button>
|
|
218
211
|
</form>
|
|
219
212
|
`,
|
|
220
213
|
})
|
|
221
|
-
export class
|
|
222
|
-
protected readonly formValue = signal<
|
|
223
|
-
protected readonly suite = userValidationSuite;
|
|
224
|
-
protected readonly shape = userFormShape;
|
|
214
|
+
export class SimpleFormComponent {
|
|
215
|
+
protected readonly formValue = signal<SimpleForm>({});
|
|
225
216
|
|
|
226
|
-
protected onSubmit() {
|
|
217
|
+
protected onSubmit(): void {
|
|
227
218
|
console.log('Form submitted:', this.formValue());
|
|
228
219
|
}
|
|
229
220
|
}
|
|
230
221
|
```
|
|
231
222
|
|
|
232
|
-
That's
|
|
223
|
+
That's all you need! The `scVestForm` directive automatically:
|
|
233
224
|
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
- ✅ Error display with `sc-control-wrapper`
|
|
238
|
-
- ✅ Runtime shape validation (dev mode)
|
|
225
|
+
- Creates FormControls for each input
|
|
226
|
+
- Manages form state with signals
|
|
227
|
+
- Provides type-safe unidirectional data flow
|
|
239
228
|
|
|
240
|
-
##
|
|
229
|
+
## Complete Example
|
|
241
230
|
|
|
242
|
-
|
|
231
|
+
A complete working form with ngx-vest-forms requires just 4 steps:
|
|
243
232
|
|
|
244
|
-
|
|
233
|
+
1. **Define your form model** using `NgxDeepPartial<T>`
|
|
234
|
+
2. **Create a validation suite** with Vest.js using `staticSuite()`
|
|
235
|
+
3. **Set up your component** with a signal for form state
|
|
236
|
+
4. **Build your template** with `scVestForm` directive and `[ngModel]` bindings
|
|
245
237
|
|
|
246
|
-
|
|
247
|
-
formValue = {
|
|
248
|
-
generalInfo: {
|
|
249
|
-
firstName: '',
|
|
250
|
-
lastName: '',
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
```
|
|
238
|
+
The result: A fully functional form with type safety, automatic form control creation, validation on blur/submit, and error display - all with minimal boilerplate.
|
|
254
239
|
|
|
255
|
-
|
|
256
|
-
This does not have anything to do with this package. It's just Angular:
|
|
240
|
+
> **📖 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.
|
|
257
241
|
|
|
258
|
-
|
|
259
|
-
form = {
|
|
260
|
-
controls: {
|
|
261
|
-
generalInformation: { // FormGroup
|
|
262
|
-
controls: {
|
|
263
|
-
firstName: {...}, // FormControl
|
|
264
|
-
lastName: {...} //FormControl
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
```
|
|
242
|
+
## Core Concepts
|
|
270
243
|
|
|
271
|
-
|
|
244
|
+
### Understanding Form State
|
|
245
|
+
|
|
246
|
+
Angular automatically creates FormGroups and FormControls based on your template structure. The `scVestForm` directive provides these outputs:
|
|
272
247
|
|
|
273
|
-
| Output | Description
|
|
274
|
-
| ----------------- |
|
|
275
|
-
| `formValueChange` | Emits when
|
|
276
|
-
| `dirtyChange` | Emits when
|
|
277
|
-
| `validChange` | Emits when
|
|
278
|
-
| `errorsChange` | Emits
|
|
248
|
+
| Output | Description |
|
|
249
|
+
| ----------------- | ----------------------------------------------- |
|
|
250
|
+
| `formValueChange` | Emits when form value changes (debounced) |
|
|
251
|
+
| `dirtyChange` | Emits when dirty state changes |
|
|
252
|
+
| `validChange` | Emits when validation state changes |
|
|
253
|
+
| `errorsChange` | Emits complete list of errors for form/controls |
|
|
279
254
|
|
|
280
255
|
### Public Methods
|
|
281
256
|
|
|
@@ -285,12 +260,12 @@ The `scVestForm` directive offers these outputs:
|
|
|
285
260
|
|
|
286
261
|
### Form Models and Type Safety
|
|
287
262
|
|
|
288
|
-
All form models in ngx-vest-forms use `DeepPartial<T>` because Angular's template-driven forms build up values incrementally:
|
|
263
|
+
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:
|
|
289
264
|
|
|
290
265
|
```typescript
|
|
291
|
-
import {
|
|
266
|
+
import { NgxDeepPartial } from 'ngx-vest-forms';
|
|
292
267
|
|
|
293
|
-
type MyFormModel =
|
|
268
|
+
type MyFormModel = NgxDeepPartial<{
|
|
294
269
|
generalInfo: {
|
|
295
270
|
firstName: string;
|
|
296
271
|
lastName: string;
|
|
@@ -298,12 +273,102 @@ type MyFormModel = DeepPartial<{
|
|
|
298
273
|
}>;
|
|
299
274
|
```
|
|
300
275
|
|
|
276
|
+
> **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.
|
|
277
|
+
|
|
301
278
|
This is why you must use the `?` operator in templates:
|
|
302
279
|
|
|
303
280
|
```html
|
|
304
281
|
<input [ngModel]="formValue().generalInfo?.firstName" name="firstName" />
|
|
305
282
|
```
|
|
306
283
|
|
|
284
|
+
### Validation Suite Type Safety
|
|
285
|
+
|
|
286
|
+
Use `NgxVestSuite<T>` for type-safe validation suites:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { NgxVestSuite, NgxDeepPartial } from 'ngx-vest-forms';
|
|
290
|
+
import { staticSuite, only, test, enforce } from 'vest';
|
|
291
|
+
|
|
292
|
+
type FormModel = NgxDeepPartial<{
|
|
293
|
+
email: string;
|
|
294
|
+
password: string;
|
|
295
|
+
profile: {
|
|
296
|
+
age: number;
|
|
297
|
+
};
|
|
298
|
+
}>;
|
|
299
|
+
|
|
300
|
+
// Create validation suite
|
|
301
|
+
export const validationSuite: NgxVestSuite<FormModel> = staticSuite(
|
|
302
|
+
(model: FormModel, field?: string) => {
|
|
303
|
+
only(field); // CRITICAL: Always call only() unconditionally
|
|
304
|
+
|
|
305
|
+
test('email', 'Email is required', () => {
|
|
306
|
+
enforce(model.email).isNotBlank();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('profile.age', 'Must be 18+', () => {
|
|
310
|
+
enforce(model.profile?.age).greaterThanOrEquals(18);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Component - use directly
|
|
316
|
+
@Component({...})
|
|
317
|
+
class MyFormComponent {
|
|
318
|
+
protected readonly suite = validationSuite;
|
|
319
|
+
protected readonly formValue = signal<FormModel>({});
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Key Benefits:**
|
|
324
|
+
|
|
325
|
+
- **Type Safety**: Full TypeScript support for model and field parameters
|
|
326
|
+
- **Template Compatibility**: Works seamlessly in Angular templates
|
|
327
|
+
- **Simplified API**: Single type for all validation suite needs
|
|
328
|
+
- **No Type Assertions**: No `as any` or `$any()` casts needed
|
|
329
|
+
|
|
330
|
+
### Form State Type and Utilities
|
|
331
|
+
|
|
332
|
+
The `formState` computed signal returns an `NgxFormState<T>` object with the current form state:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { NgxFormState, createEmptyFormState } from 'ngx-vest-forms';
|
|
336
|
+
|
|
337
|
+
// The form state contains:
|
|
338
|
+
interface NgxFormState<TModel> {
|
|
339
|
+
valid: boolean; // Whether the form is valid
|
|
340
|
+
errors: Record<string, string[]>; // Map of field errors by path
|
|
341
|
+
value: TModel | null; // Current form value (includes disabled fields)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Useful for parent components displaying child form state
|
|
345
|
+
@Component({
|
|
346
|
+
template: `
|
|
347
|
+
<app-child-form #childForm />
|
|
348
|
+
<div>Form Valid: {{ formState().valid }}</div>
|
|
349
|
+
<div>Errors: {{ formState().errors | json }}</div>
|
|
350
|
+
`,
|
|
351
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
352
|
+
})
|
|
353
|
+
export class ParentComponent {
|
|
354
|
+
// Modern Angular 20+: Use viewChild() instead of @ViewChild
|
|
355
|
+
private readonly childForm = viewChild<ChildFormComponent>('childForm');
|
|
356
|
+
|
|
357
|
+
// Provide safe fallback when child form isn't initialized yet
|
|
358
|
+
protected readonly formState = computed(
|
|
359
|
+
() => this.childForm()?.vestForm?.formState() ?? createEmptyFormState()
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The `createEmptyFormState()` utility creates a safe default state:
|
|
365
|
+
|
|
366
|
+
- `valid: true`
|
|
367
|
+
- `errors: {}`
|
|
368
|
+
- `value: null`
|
|
369
|
+
|
|
370
|
+
This prevents null reference errors in templates when child forms or form references might be undefined.
|
|
371
|
+
|
|
307
372
|
### Shape Validation: Catching Typos Early
|
|
308
373
|
|
|
309
374
|
Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
|
|
@@ -395,35 +460,23 @@ ngx-vest-forms uses [Vest.js](https://vestjs.dev) for validation - a lightweight
|
|
|
395
460
|
|
|
396
461
|
### Creating Your First Validation Suite
|
|
397
462
|
|
|
398
|
-
|
|
463
|
+
Vest suites use `staticSuite()` with `only()` for performance optimization:
|
|
399
464
|
|
|
400
465
|
```typescript
|
|
401
466
|
import { enforce, only, staticSuite, test } from 'vest';
|
|
402
|
-
import { MyFormModel } from '../models/my-form.model';
|
|
403
|
-
|
|
404
|
-
export const myFormModelSuite = staticSuite(
|
|
405
|
-
(model: MyFormModel, field?: string) => {
|
|
406
|
-
if (field) {
|
|
407
|
-
// Needed to not run every validation every time
|
|
408
|
-
only(field);
|
|
409
|
-
}
|
|
410
467
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
});
|
|
468
|
+
export const myFormSuite = staticSuite((model: MyFormModel, field?) => {
|
|
469
|
+
only(field); // Call unconditionally at top
|
|
414
470
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
);
|
|
471
|
+
test('firstName', 'First name is required', () => {
|
|
472
|
+
enforce(model.firstName).isNotBlank();
|
|
473
|
+
});
|
|
474
|
+
});
|
|
420
475
|
```
|
|
421
476
|
|
|
422
|
-
|
|
477
|
+
**Test parameters**: `test(fieldName, errorMessage, validationFunction)`
|
|
423
478
|
|
|
424
|
-
-
|
|
425
|
-
- **Second parameter**: The validation error message
|
|
426
|
-
- **Third parameter**: The validation assertion
|
|
479
|
+
- Use dot notation for nested fields: `'addresses.billingAddress.street'`
|
|
427
480
|
|
|
428
481
|
### Connecting Validation to Your Form
|
|
429
482
|
|
|
@@ -498,36 +551,26 @@ You can use `sc-control-wrapper` on:
|
|
|
498
551
|
|
|
499
552
|
### Performance Optimization with `only()`
|
|
500
553
|
|
|
501
|
-
|
|
554
|
+
**Critical**: Always call `only()` unconditionally at the top of your validation suite:
|
|
502
555
|
|
|
503
556
|
```typescript
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (field) {
|
|
509
|
-
only(field); // Only validate the specific field during user interaction
|
|
510
|
-
}
|
|
511
|
-
// When field is undefined (e.g., on submit), all validations run
|
|
557
|
+
export const suite = staticSuite((model, field?) => {
|
|
558
|
+
only(field); // ✅ CORRECT - Unconditional call
|
|
559
|
+
// ...tests
|
|
560
|
+
});
|
|
512
561
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
enforce(model.lastName).isNotBlank();
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
);
|
|
562
|
+
// ❌ WRONG - Conditional call
|
|
563
|
+
export const badSuite = staticSuite((model, field?) => {
|
|
564
|
+
if (field) only(field); // Breaks Vest's execution tracking!
|
|
565
|
+
});
|
|
521
566
|
```
|
|
522
567
|
|
|
523
|
-
|
|
568
|
+
**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.
|
|
524
569
|
|
|
525
|
-
- ✅
|
|
526
|
-
- ✅ On form submit: All fields validate (complete validation)
|
|
527
|
-
- ✅ Untouched fields don't show errors prematurely (better UX)
|
|
570
|
+
- ✅ **Fixed**: Proper validation with `omitWhen` and nested fields with `validationConfig`
|
|
528
571
|
|
|
529
572
|
> [!IMPORTANT]
|
|
530
|
-
>
|
|
573
|
+
> **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.
|
|
531
574
|
|
|
532
575
|
### Error Display Control
|
|
533
576
|
|
|
@@ -597,57 +640,70 @@ For example, to debounce validation (useful for API calls):
|
|
|
597
640
|
</form>
|
|
598
641
|
```
|
|
599
642
|
|
|
600
|
-
|
|
643
|
+
#### Configurable Validation Config Debounce
|
|
601
644
|
|
|
602
|
-
|
|
645
|
+
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`:
|
|
603
646
|
|
|
604
|
-
|
|
605
|
-
but since Template driven forms do all the hard work for us, we can simply create a computed signal for that and
|
|
606
|
-
bind that in the template. Having logic in the template is considered a bad practice, so we can do all
|
|
607
|
-
the calculations in our class.
|
|
647
|
+
**Global Configuration** (recommended for consistent behavior):
|
|
608
648
|
|
|
609
|
-
|
|
649
|
+
```typescript
|
|
650
|
+
import { ApplicationConfig } from '@angular/core';
|
|
651
|
+
import { NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN } from 'ngx-vest-forms';
|
|
610
652
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
/>
|
|
653
|
+
export const appConfig: ApplicationConfig = {
|
|
654
|
+
providers: [
|
|
655
|
+
// Set global debounce for validationConfig dependencies
|
|
656
|
+
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 150 },
|
|
657
|
+
],
|
|
658
|
+
};
|
|
659
|
+
```
|
|
619
660
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
661
|
+
**Per-Route Configuration**:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
{
|
|
665
|
+
path: 'checkout',
|
|
666
|
+
component: CheckoutComponent,
|
|
667
|
+
providers: [
|
|
668
|
+
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 50 }
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
**Per-Component Configuration**:
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
@Component({
|
|
677
|
+
providers: [
|
|
678
|
+
// Disable debounce for testing
|
|
679
|
+
{ provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, useValue: 0 },
|
|
680
|
+
],
|
|
681
|
+
})
|
|
682
|
+
export class MyFormComponent {}
|
|
629
683
|
```
|
|
630
684
|
|
|
685
|
+
**Default**: 100ms (maintains backward compatibility)
|
|
686
|
+
|
|
687
|
+
> **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.
|
|
688
|
+
|
|
689
|
+
## Intermediate Topics
|
|
690
|
+
|
|
691
|
+
### Conditional Fields
|
|
692
|
+
|
|
693
|
+
Use computed signals to show/hide fields dynamically. Angular automatically manages FormControl creation/removal:
|
|
694
|
+
|
|
631
695
|
```typescript
|
|
632
696
|
class MyComponent {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
697
|
+
protected readonly showLastName = computed(
|
|
698
|
+
() => !!this.formValue().firstName
|
|
699
|
+
);
|
|
636
700
|
}
|
|
637
701
|
```
|
|
638
702
|
|
|
639
|
-
This will automatically add and remove the form control from our form model.
|
|
640
|
-
This also works for a form group:
|
|
641
|
-
|
|
642
703
|
```html
|
|
643
|
-
@if(
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
<input
|
|
647
|
-
type="text"
|
|
648
|
-
name="firstName"
|
|
649
|
-
[ngModel]="formValue().generalInformation?.firstName"
|
|
650
|
-
/>
|
|
704
|
+
@if(showLastName()) {
|
|
705
|
+
<input name="lastName" [ngModel]="formValue().lastName" />
|
|
706
|
+
}
|
|
651
707
|
|
|
652
708
|
<label>Last name</label>
|
|
653
709
|
<input
|
|
@@ -709,388 +765,228 @@ Check this interesting example below:
|
|
|
709
765
|
- [x] Confirm password is only required when there is a password
|
|
710
766
|
- [x] The passwords should match, but only when they are both filled in
|
|
711
767
|
|
|
712
|
-
|
|
768
|
+
````typescript
|
|
713
769
|
test('passwords.password', 'Password is not filled in', () => {
|
|
714
770
|
enforce(model.passwords?.password).isNotBlank();
|
|
715
771
|
});
|
|
772
|
+
|
|
716
773
|
omitWhen(!model.passwords?.password, () => {
|
|
717
|
-
test('passwords.confirmPassword', 'Confirm password
|
|
774
|
+
test('passwords.confirmPassword', 'Confirm password required', () => {
|
|
718
775
|
enforce(model.passwords?.confirmPassword).isNotBlank();
|
|
719
776
|
});
|
|
777
|
+
|
|
778
|
+
test('passwords', 'Passwords must match', () => {
|
|
779
|
+
enforce(model.passwords?.confirmPassword).equals(model.passwords?.password);
|
|
780
|
+
});
|
|
720
781
|
});
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
() => {
|
|
724
|
-
test('passwords', 'Passwords do not match', () => {
|
|
725
|
-
enforce(model.passwords?.confirmPassword).equals(
|
|
726
|
-
model.passwords?.password
|
|
727
|
-
);
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
);
|
|
731
|
-
```
|
|
782
|
+
|
|
783
|
+
This pattern is testable, reusable across frameworks, and readable.
|
|
732
784
|
|
|
733
785
|
Forget about manually adding, removing validators on reactive forms and not being able to
|
|
734
786
|
re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
|
|
735
787
|
**Oh, it's also pretty readable**
|
|
736
788
|
|
|
737
|
-
|
|
789
|
+
### Dependent Field Validation with Conditional Rendering
|
|
738
790
|
|
|
739
|
-
|
|
791
|
+
When fields that depend on each other are conditionally rendered (using `@if`), you need a **reactive validationConfig** to avoid "control not found" warnings.
|
|
740
792
|
|
|
741
|
-
|
|
793
|
+
> **💡 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.
|
|
742
794
|
|
|
743
795
|
#### The Problem
|
|
744
796
|
|
|
745
797
|
```typescript
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<input name="fieldB" [ngModel]="formValue().fieldB" />
|
|
752
|
-
}
|
|
753
|
-
@else if (procedureType() === 'typeC') {
|
|
754
|
-
<!-- NON-FORM ELEMENT -->
|
|
755
|
-
<p>No additional input required for this procedure type.</p>
|
|
798
|
+
// ❌ This causes warnings when conditional fields don't exist
|
|
799
|
+
class MyComponent {
|
|
800
|
+
protected readonly validationConfig = {
|
|
801
|
+
quantity: ['justification'], // justification only shows when quantity > 5!
|
|
802
|
+
};
|
|
756
803
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
**Issue**: When switching from `typeA` to `typeC`, the input field is removed but no control values change, so validation doesn't update automatically.
|
|
804
|
+
````
|
|
760
805
|
|
|
761
806
|
#### The Solution
|
|
762
807
|
|
|
808
|
+
Use a **computed signal** for `validationConfig`:
|
|
809
|
+
|
|
763
810
|
```typescript
|
|
764
|
-
import { Component, signal,
|
|
811
|
+
import { Component, signal, computed } from '@angular/core';
|
|
765
812
|
|
|
766
813
|
@Component({
|
|
767
814
|
template: `
|
|
768
|
-
<form
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
[ngModel]="formValue().procedureType"
|
|
777
|
-
(ngModelChange)="onProcedureTypeChange($event)"
|
|
778
|
-
>
|
|
779
|
-
<option value="typeA">Type A</option>
|
|
780
|
-
<option value="typeB">Type B</option>
|
|
781
|
-
<option value="typeC">Type C (No input)</option>
|
|
782
|
-
</select>
|
|
783
|
-
|
|
784
|
-
@if (formValue().procedureType === 'typeA') {
|
|
785
|
-
<input name="fieldA" [ngModel]="formValue().fieldA" />
|
|
786
|
-
} @else if (formValue().procedureType === 'typeB') {
|
|
787
|
-
<input name="fieldB" [ngModel]="formValue().fieldB" />
|
|
788
|
-
} @else if (formValue().procedureType === 'typeC') {
|
|
789
|
-
<p>No additional input required.</p>
|
|
815
|
+
<form scVestForm [validationConfig]="validationConfig()" ...>
|
|
816
|
+
<input name="quantity" [ngModel]="formValue().quantity" />
|
|
817
|
+
|
|
818
|
+
@if ((formValue().quantity || 0) > 5) {
|
|
819
|
+
<textarea
|
|
820
|
+
name="justification"
|
|
821
|
+
[ngModel]="formValue().justification"
|
|
822
|
+
></textarea>
|
|
790
823
|
}
|
|
791
824
|
</form>
|
|
792
825
|
`,
|
|
793
826
|
})
|
|
794
|
-
export class
|
|
795
|
-
readonly vestForm =
|
|
796
|
-
viewChild.required<FormDirective<MyFormModel>>('vestForm');
|
|
797
|
-
|
|
827
|
+
export class MyComponent {
|
|
798
828
|
protected readonly formValue = signal<MyFormModel>({});
|
|
799
|
-
protected readonly validationSuite = myValidationSuite;
|
|
800
|
-
|
|
801
|
-
onProcedureTypeChange(newType: string) {
|
|
802
|
-
// Update the form value
|
|
803
|
-
this.formValue.update((current) => ({
|
|
804
|
-
...current,
|
|
805
|
-
procedureType: newType,
|
|
806
|
-
// Clear fields that are no longer relevant
|
|
807
|
-
...(newType !== 'typeA' && { fieldA: undefined }),
|
|
808
|
-
...(newType !== 'typeB' && { fieldB: undefined }),
|
|
809
|
-
}));
|
|
810
|
-
|
|
811
|
-
// ✅ CRITICAL: Trigger validation update after structure change
|
|
812
|
-
this.vestForm.triggerFormValidation();
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
```
|
|
816
829
|
|
|
817
|
-
|
|
830
|
+
// ✅ Computed config only references controls that exist
|
|
831
|
+
protected readonly validationConfig = computed(() => {
|
|
832
|
+
const config: Record<string, string[]> = {};
|
|
818
833
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
- **After switching form modes** - When toggling between different form layouts
|
|
825
|
-
|
|
826
|
-
#### Field Clearing Pattern for Component State Consistency
|
|
834
|
+
// Only add dependency when field is in DOM
|
|
835
|
+
if ((this.formValue().quantity || 0) > 5) {
|
|
836
|
+
config['quantity'] = ['justification'];
|
|
837
|
+
config['justification'] = ['quantity']; // Bidirectional validation
|
|
838
|
+
}
|
|
827
839
|
|
|
828
|
-
|
|
840
|
+
return config;
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
```
|
|
829
844
|
|
|
830
|
-
|
|
845
|
+
**Key Points:**
|
|
831
846
|
|
|
832
|
-
-
|
|
847
|
+
- ✅ **Reactive**: Config updates when field visibility changes
|
|
848
|
+
- ✅ **No Warnings**: Only references controls that exist in DOM
|
|
849
|
+
- ✅ **Bidirectional**: Works for two-way validation dependencies
|
|
850
|
+
- ✅ **Type-Safe**: Full TypeScript support
|
|
833
851
|
|
|
834
|
-
**
|
|
852
|
+
**Template Binding:**
|
|
835
853
|
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
<input name="fieldA" [ngModel]="formValue().fieldA" /> // Form input
|
|
840
|
-
} @else if (procedureType === 'typeB') {
|
|
841
|
-
<input name="fieldB" [ngModel]="formValue().fieldB" /> // Form input
|
|
842
|
-
} @else if (procedureType === 'typeC') {
|
|
843
|
-
<p>No additional input required for this procedure.</p> // NON-form element!
|
|
844
|
-
}
|
|
854
|
+
```html
|
|
855
|
+
<!-- Notice the function call: validationConfig() -->
|
|
856
|
+
<form scVestForm [validationConfig]="validationConfig()" ...>...</form>
|
|
845
857
|
```
|
|
846
858
|
|
|
847
|
-
|
|
859
|
+
## Advanced Topics
|
|
848
860
|
|
|
849
|
-
|
|
850
|
-
2. **Result:** `ngForm.form.value` becomes clean, but `formValue()` signal remains stale
|
|
851
|
-
3. **Problem:** State inconsistency between Angular's form state and your component state
|
|
861
|
+
### Handling Dynamic Forms: Two Common Patterns
|
|
852
862
|
|
|
853
|
-
|
|
854
|
-
// Before switching from typeA to typeC:
|
|
855
|
-
formValue() = { procedureType: 'typeA', fieldA: 'some-value' };
|
|
856
|
-
ngForm.form.value = { procedureType: 'typeA', fieldA: 'some-value' };
|
|
863
|
+
Dynamic forms present two distinct challenges that require different solutions:
|
|
857
864
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
865
|
+
| Challenge | Solution | When to Use |
|
|
866
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
867
|
+
| **Conditional fields with validation dependencies** | [Computed `validationConfig`](#dependent-field-validation-with-conditional-rendering) | Fields depend on each other AND are conditionally rendered |
|
|
868
|
+
| **Structure changes without value changes** | [`triggerFormValidation()`](#handling-form-structure-changes) | Switching between form inputs and non-form content |
|
|
861
869
|
|
|
862
|
-
|
|
863
|
-
formValue() = { procedureType: 'typeC' }; // ✅ Consistent
|
|
864
|
-
ngForm.form.value = { procedureType: 'typeC' }; // ✅ Consistent
|
|
865
|
-
```
|
|
870
|
+
> **💡 Pro Tip**: These solutions are complementary and often used together in complex forms with both conditional validation dependencies and dynamic structure changes.
|
|
866
871
|
|
|
867
|
-
|
|
872
|
+
### Handling Form Structure Changes
|
|
868
873
|
|
|
869
|
-
|
|
874
|
+
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:
|
|
870
875
|
|
|
871
876
|
```typescript
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
877
|
+
@Component({
|
|
878
|
+
template: `
|
|
879
|
+
<form scVestForm [suite]="suite" #vestForm="scVestForm">
|
|
880
|
+
<select
|
|
881
|
+
name="type"
|
|
882
|
+
[ngModel]="formValue().type"
|
|
883
|
+
(ngModelChange)="onTypeChange($event)"
|
|
884
|
+
>
|
|
885
|
+
<option value="typeA">Type A</option>
|
|
886
|
+
<option value="typeC">Type C (no input)</option>
|
|
887
|
+
</select>
|
|
883
888
|
|
|
884
|
-
|
|
889
|
+
@if (formValue().type === 'typeA') {
|
|
890
|
+
<input name="fieldA" [ngModel]="formValue().fieldA" />
|
|
891
|
+
} @else {
|
|
892
|
+
<p>No additional input required.</p>
|
|
893
|
+
}
|
|
894
|
+
</form>
|
|
895
|
+
`,
|
|
896
|
+
})
|
|
897
|
+
class MyComponent {
|
|
898
|
+
protected readonly vestFormRef = viewChild.required('vestForm', {
|
|
899
|
+
read: FormDirective,
|
|
900
|
+
});
|
|
885
901
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
...(newValue !== 'typeB' && { fieldB: undefined }),
|
|
894
|
-
}));
|
|
895
|
-
|
|
896
|
-
// Trigger validation update after structure change
|
|
897
|
-
this.vestFormRef.triggerFormValidation();
|
|
902
|
+
protected onTypeChange(type: string) {
|
|
903
|
+
this.formValue.update((v) => {
|
|
904
|
+
const updated = clearFieldsWhen(v, { fieldA: type !== 'typeA' });
|
|
905
|
+
return { ...updated, type };
|
|
906
|
+
});
|
|
907
|
+
this.vestFormRef().triggerFormValidation();
|
|
908
|
+
}
|
|
898
909
|
}
|
|
899
910
|
```
|
|
900
911
|
|
|
901
|
-
|
|
912
|
+
> **📖 Detailed Guide**: See **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** and **[Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md)** for comprehensive examples.
|
|
902
913
|
|
|
903
|
-
|
|
914
|
+
**When to use `triggerFormValidation()`**: After form structure changes (showing/hiding fields), clearing form sections, or switching between form inputs and non-form content.
|
|
904
915
|
|
|
905
|
-
|
|
906
|
-
import { clearFieldsWhen } from 'ngx-vest-forms';
|
|
916
|
+
> **🔗 See Also**: [Computed `validationConfig`](#dependent-field-validation-with-conditional-rendering) for validation dependencies, and [Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md) for state management.
|
|
907
917
|
|
|
908
|
-
|
|
909
|
-
onStructureChange(newValue: string) {
|
|
910
|
-
this.formValue.update((current) =>
|
|
911
|
-
clearFieldsWhen(current, {
|
|
912
|
-
fieldA: newValue !== 'typeA',
|
|
913
|
-
fieldB: newValue !== 'typeB',
|
|
914
|
-
})
|
|
915
|
-
);
|
|
918
|
+
#### Field Clearing Pattern
|
|
916
919
|
|
|
917
|
-
|
|
918
|
-
this.vestFormRef.triggerFormValidation();
|
|
919
|
-
}
|
|
920
|
-
```
|
|
920
|
+
**When field clearing is required**: When switching between form inputs and non-form elements (e.g., `<input>` ↔ `<p>` tags).
|
|
921
921
|
|
|
922
|
-
**
|
|
922
|
+
**Why**: Angular removes FormControls when switching to non-form content, but component signals retain old values, creating state inconsistency.
|
|
923
923
|
|
|
924
924
|
```typescript
|
|
925
|
-
|
|
925
|
+
// Use clearFieldsWhen to synchronize state
|
|
926
|
+
import { clearFieldsWhen } from 'ngx-vest-forms';
|
|
926
927
|
|
|
927
|
-
|
|
928
|
-
|
|
928
|
+
this.formValue.update((v) =>
|
|
929
|
+
clearFieldsWhen(v, {
|
|
930
|
+
fieldA: procedureType !== 'typeA', // Clear when NOT showing input
|
|
931
|
+
})
|
|
932
|
+
);
|
|
933
|
+
```
|
|
929
934
|
|
|
930
|
-
|
|
931
|
-
const filteredState = keepFieldsWhen(currentFormValue, {
|
|
932
|
-
procedureType: true, // always keep
|
|
933
|
-
fieldA: procedureType === 'typeA',
|
|
934
|
-
fieldB: procedureType === 'typeB',
|
|
935
|
-
});
|
|
936
|
-
```#### Validation Suite Pattern for Conditional Fields
|
|
935
|
+
**When NOT required**: Pure form-to-form conditionals (switching input types with same `name`) – Angular maintains FormControl throughout.
|
|
937
936
|
|
|
938
|
-
|
|
939
|
-
import { staticSuite, test, enforce, omitWhen, only } from 'vest';
|
|
937
|
+
##### When Field Clearing is NOT Required
|
|
940
938
|
|
|
941
|
-
|
|
942
|
-
(model: MyFormModel, field?: string) => {
|
|
943
|
-
if (field) {
|
|
944
|
-
only(field); // Performance optimization
|
|
945
|
-
}
|
|
939
|
+
Pure form-to-form conditionals (switching input types with same `name`) usually don't need field clearing because Angular maintains the FormControl throughout:
|
|
946
940
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
941
|
+
```typescript
|
|
942
|
+
// These switches DON'T require field clearing:
|
|
943
|
+
@if (inputType === 'text') {
|
|
944
|
+
<input name="field" [ngModel]="formValue().field" type="text" />
|
|
945
|
+
} @else {
|
|
946
|
+
<input name="field" [ngModel]="formValue().field" type="number" />
|
|
947
|
+
}
|
|
948
|
+
```
|
|
951
949
|
|
|
952
|
-
|
|
953
|
-
omitWhen(model.procedureType !== 'typeA', () => {
|
|
954
|
-
test('fieldA', 'Field A is required for Type A', () => {
|
|
955
|
-
enforce(model.fieldA).isNotBlank();
|
|
956
|
-
});
|
|
957
|
-
});
|
|
950
|
+
> **📖 Complete Guide**: See **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** for detailed patterns and use cases.
|
|
958
951
|
|
|
959
|
-
|
|
960
|
-
test('fieldB', 'Field B is required for Type B', () => {
|
|
961
|
-
enforce(model.fieldB).isNotBlank();
|
|
962
|
-
});
|
|
963
|
-
});
|
|
952
|
+
### Field State Utilities
|
|
964
953
|
|
|
965
|
-
|
|
966
|
-
}
|
|
967
|
-
);
|
|
968
|
-
```
|
|
954
|
+
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.
|
|
969
955
|
|
|
970
|
-
|
|
956
|
+
**Key Utilities:**
|
|
971
957
|
|
|
972
|
-
|
|
958
|
+
- **`clearFieldsWhen(state, conditions)`** - Conditionally clear fields based on boolean conditions (most common)
|
|
959
|
+
- **`clearFields(state, fieldArray)`** - Unconditionally clear specific fields (for reset operations)
|
|
960
|
+
- **`keepFieldsWhen(state, conditions)`** - Keep only fields that meet conditions (whitelist approach)
|
|
973
961
|
|
|
974
|
-
|
|
962
|
+
**When to Use:**
|
|
975
963
|
|
|
976
|
-
|
|
964
|
+
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.
|
|
977
965
|
|
|
978
|
-
|
|
966
|
+
**Example:**
|
|
979
967
|
|
|
980
968
|
```typescript
|
|
981
969
|
import { clearFieldsWhen } from 'ngx-vest-forms';
|
|
982
970
|
|
|
983
|
-
//
|
|
984
|
-
const updatedState = clearFieldsWhen(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
// When procedureType === 'typeC', both fields are cleared (typeC shows <p> text, not inputs)
|
|
988
|
-
shippingAddress: !useShippingAddress, // Clear when showing "No shipping needed" message
|
|
971
|
+
// Clear shipping address when switching from input to "No shipping needed" message
|
|
972
|
+
const updatedState = clearFieldsWhen(formValue(), {
|
|
973
|
+
'addresses.shippingAddress': !useShippingAddress,
|
|
974
|
+
emergencyContact: age >= 18, // Clear when adult (no emergency contact input shown)
|
|
989
975
|
});
|
|
990
976
|
```
|
|
991
977
|
|
|
992
|
-
|
|
978
|
+
**Important Note:**
|
|
993
979
|
|
|
994
|
-
|
|
980
|
+
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.
|
|
995
981
|
|
|
996
|
-
|
|
997
|
-
import { clearFields } from 'ngx-vest-forms';
|
|
998
|
-
|
|
999
|
-
// Clear specific fields unconditionally
|
|
1000
|
-
const cleanedState = clearFields(currentFormValue, [
|
|
1001
|
-
'temporaryData',
|
|
1002
|
-
'draftSaved',
|
|
1003
|
-
]);
|
|
1004
|
-
```
|
|
982
|
+
> **📖 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.
|
|
1005
983
|
|
|
1006
|
-
###
|
|
984
|
+
### Composable Validations
|
|
1007
985
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
```typescript
|
|
1011
|
-
import { keepFieldsWhen } from 'ngx-vest-forms';
|
|
1012
|
-
|
|
1013
|
-
// Keep only relevant fields
|
|
1014
|
-
const filteredState = keepFieldsWhen(currentFormValue, {
|
|
1015
|
-
basicInfo: true, // always keep
|
|
1016
|
-
addressInfo: needsAddress,
|
|
1017
|
-
paymentInfo: requiresPayment,
|
|
1018
|
-
});
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
### Why These Utilities Are Needed
|
|
1022
|
-
|
|
1023
|
-
The utilities are specifically needed when conditionally switching between **form inputs** and **non-form elements**:
|
|
1024
|
-
|
|
1025
|
-
**The Core Problem:**
|
|
1026
|
-
|
|
1027
|
-
1. **Template structure:** `@if (condition) { <input> } @else { <p>Info text</p> }`
|
|
1028
|
-
2. **Angular's behavior:** Automatically removes FormControls when switching to non-form content
|
|
1029
|
-
3. **Your component signals:** Retain old field values from when input was present
|
|
1030
|
-
4. **Result:** State inconsistency between `ngForm.form.value` (clean) and `formValue()` (stale)
|
|
1031
|
-
|
|
1032
|
-
**The Solution:**
|
|
1033
|
-
|
|
1034
|
-
These utilities synchronize your component state with Angular's form state, ensuring consistency when form structure changes involve non-form content.
|
|
1035
|
-
|
|
1036
|
-
> **Note:** Pure form-to-form conditionals (e.g., switching between different input types with the same `name`) typically don't require these utilities as Angular maintains the FormControl throughout.
|
|
1037
|
-
|
|
1038
|
-
### More Examples
|
|
1039
|
-
|
|
1040
|
-
#### Conditional Fields Example
|
|
1041
|
-
|
|
1042
|
-
```typescript
|
|
1043
|
-
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
|
|
1044
|
-
|
|
1045
|
-
@Component({
|
|
1046
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1047
|
-
template: `
|
|
1048
|
-
<form scVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
|
|
1049
|
-
<!-- Age field -->
|
|
1050
|
-
<div sc-control-wrapper>
|
|
1051
|
-
<label>Age</label>
|
|
1052
|
-
<input [ngModel]="formValue().age" name="age" type="number" />
|
|
1053
|
-
</div>
|
|
1054
|
-
|
|
1055
|
-
<!-- Emergency contact - only required if under 18 -->
|
|
1056
|
-
@if (emergencyContactRequired()) {
|
|
1057
|
-
<div sc-control-wrapper>
|
|
1058
|
-
<label>Emergency Contact</label>
|
|
1059
|
-
<input
|
|
1060
|
-
[ngModel]="formValue().emergencyContact"
|
|
1061
|
-
name="emergencyContact"
|
|
1062
|
-
/>
|
|
1063
|
-
</div>
|
|
1064
|
-
}
|
|
1065
|
-
</form>
|
|
1066
|
-
`,
|
|
1067
|
-
})
|
|
1068
|
-
export class ConditionalFormComponent {
|
|
1069
|
-
protected readonly formValue = signal<ConditionalFormModel>({});
|
|
1070
|
-
|
|
1071
|
-
// Computed signal for conditional logic
|
|
1072
|
-
protected readonly emergencyContactRequired = computed(
|
|
1073
|
-
() => (this.formValue().age || 0) < 18
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
#### Live Examples
|
|
1079
|
-
|
|
1080
|
-
- **[Purchase Form Demo](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples/src/app/components/smart/purchase-form)** - Complex form with nested objects, validation dependencies, and conditional logic
|
|
1081
|
-
- **[Business Hours Demo](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples/src/app/components/smart/business-hours-form)** - Dynamic form arrays with complex validation rules
|
|
1082
|
-
|
|
1083
|
-
> **💡 Pro Tip**: Check out our detailed [Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md) for advanced handling of conditional form scenarios, alternative approaches, and performance considerations.
|
|
1084
|
-
|
|
1085
|
-
### Composable Validations
|
|
1086
|
-
|
|
1087
|
-
We can compose validations suites with sub suites. After all, we want to re-use certain pieces of our
|
|
1088
|
-
validation logic and we don't want one huge unreadable suite.
|
|
1089
|
-
This is quite straightforward with Vest.
|
|
1090
|
-
|
|
1091
|
-
Let's take this simple function that validates an address:
|
|
986
|
+
Break down complex validation logic into reusable functions that can be shared across forms, frameworks, and even frontend/backend:
|
|
1092
987
|
|
|
1093
988
|
```typescript
|
|
989
|
+
// Reusable validation function
|
|
1094
990
|
export function addressValidations(
|
|
1095
991
|
model: AddressModel | undefined,
|
|
1096
992
|
field: string
|
|
@@ -1101,608 +997,231 @@ export function addressValidations(
|
|
|
1101
997
|
test(`${field}.city`, 'City is required', () => {
|
|
1102
998
|
enforce(model?.city).isNotBlank();
|
|
1103
999
|
});
|
|
1104
|
-
|
|
1105
|
-
enforce(model?.zipcode).isNotBlank();
|
|
1106
|
-
});
|
|
1107
|
-
test(`${field}.number`, 'Number is required', () => {
|
|
1108
|
-
enforce(model?.number).isNotBlank();
|
|
1109
|
-
});
|
|
1110
|
-
test(`${field}.country`, 'Country is required', () => {
|
|
1111
|
-
enforce(model?.country).isNotBlank();
|
|
1112
|
-
});
|
|
1000
|
+
// ... more validations
|
|
1113
1001
|
}
|
|
1114
|
-
```
|
|
1115
|
-
|
|
1116
|
-
Our suite would consume it like this:
|
|
1117
1002
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
if (field) {
|
|
1125
|
-
only(field);
|
|
1126
|
-
}
|
|
1127
|
-
addressValidations(
|
|
1128
|
-
model.addresses?.billingAddress,
|
|
1129
|
-
'addresses.billingAddress'
|
|
1130
|
-
);
|
|
1131
|
-
addressValidations(
|
|
1132
|
-
model.addresses?.shippingAddress,
|
|
1133
|
-
'addresses.shippingAddress'
|
|
1134
|
-
);
|
|
1003
|
+
// Use in your suite
|
|
1004
|
+
export const orderSuite: NgxVestSuite<OrderFormModel> = staticSuite(
|
|
1005
|
+
(model, field?) => {
|
|
1006
|
+
only(field);
|
|
1007
|
+
addressValidations(model.billingAddress, 'billingAddress');
|
|
1008
|
+
addressValidations(model.shippingAddress, 'shippingAddress');
|
|
1135
1009
|
}
|
|
1136
1010
|
);
|
|
1137
1011
|
```
|
|
1138
1012
|
|
|
1139
|
-
|
|
1013
|
+
**Benefits:**
|
|
1140
1014
|
|
|
1141
|
-
|
|
1015
|
+
- ✅ **Reusability** - Share validation logic across different forms
|
|
1016
|
+
- ✅ **Maintainability** - Update validation logic in one place
|
|
1017
|
+
- ✅ **Testability** - Test validation functions independently
|
|
1018
|
+
- ✅ **Cross-framework** - Use same logic on frontend/backend, Angular/React
|
|
1142
1019
|
|
|
1143
|
-
|
|
1144
|
-
We have 2 addresses, but the shippingAddress is only required when the `shippingAddressIsDifferentFromBillingAddress`
|
|
1145
|
-
Checkbox is checked. But if it is checked, all fields are required.
|
|
1146
|
-
And if both addresses are filled in, they should be different.
|
|
1020
|
+
> **📖 Detailed Guide**: See **[Composable Validations](./docs/COMPOSABLE-VALIDATIONS.md)** for advanced patterns, conditional composition, organizing validation files, nested compositions, and testing strategies.
|
|
1147
1021
|
|
|
1148
|
-
|
|
1022
|
+
### Custom Control Wrappers
|
|
1149
1023
|
|
|
1150
|
-
|
|
1151
|
-
- [x] The shipping Address field (only required when checkbox is checked)
|
|
1152
|
-
- [x] validation on all the address fields (street, number, etc) on both addresses
|
|
1024
|
+
Create your own error display components using the `FormErrorDisplayDirective`, which provides all the validation state you need:
|
|
1153
1025
|
|
|
1154
1026
|
```typescript
|
|
1155
|
-
addressValidations(model.addresses?.billingAddress, 'addresses.billingAddress');
|
|
1156
|
-
omitWhen(!model.addresses?.shippingAddressDifferentFromBillingAddress, () => {
|
|
1157
|
-
addressValidations(
|
|
1158
|
-
model.addresses?.shippingAddress,
|
|
1159
|
-
'addresses.shippingAddress'
|
|
1160
|
-
);
|
|
1161
|
-
test('addresses', 'The addresses appear to be the same', () => {
|
|
1162
|
-
enforce(JSON.stringify(model.addresses?.billingAddress)).notEquals(
|
|
1163
|
-
JSON.stringify(model.addresses?.shippingAddress)
|
|
1164
|
-
);
|
|
1165
|
-
});
|
|
1166
|
-
});
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
### Creating Custom Control Wrappers
|
|
1170
|
-
|
|
1171
|
-
If the default `sc-control-wrapper` doesn't meet your design requirements, you can easily create your own using the `FormErrorDisplayDirective`. This directive provides all the necessary state and logic for displaying errors, warnings, and pending states.
|
|
1172
|
-
|
|
1173
|
-
##### Basic Custom Wrapper
|
|
1174
|
-
|
|
1175
|
-
```typescript
|
|
1176
|
-
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
1177
|
-
import { FormErrorDisplayDirective } from 'ngx-vest-forms';
|
|
1178
|
-
|
|
1179
1027
|
@Component({
|
|
1180
|
-
selector: 'app-custom-
|
|
1181
|
-
|
|
1182
|
-
hostDirectives: [
|
|
1183
|
-
{
|
|
1184
|
-
directive: FormErrorDisplayDirective,
|
|
1185
|
-
inputs: ['errorDisplayMode'],
|
|
1186
|
-
},
|
|
1187
|
-
],
|
|
1028
|
+
selector: 'app-custom-wrapper',
|
|
1029
|
+
hostDirectives: [FormErrorDisplayDirective],
|
|
1188
1030
|
template: `
|
|
1189
1031
|
<div class="field-wrapper">
|
|
1190
1032
|
<ng-content />
|
|
1191
|
-
|
|
1192
1033
|
@if (errorDisplay.shouldShowErrors()) {
|
|
1193
|
-
<div class="error
|
|
1034
|
+
<div class="error">
|
|
1194
1035
|
@for (error of errorDisplay.errors(); track error) {
|
|
1195
|
-
<span>{{ error
|
|
1036
|
+
<span>{{ error }}</span>
|
|
1196
1037
|
}
|
|
1197
1038
|
</div>
|
|
1198
1039
|
}
|
|
1199
|
-
|
|
1200
1040
|
@if (errorDisplay.isPending()) {
|
|
1201
1041
|
<div class="validating">Validating...</div>
|
|
1202
1042
|
}
|
|
1203
1043
|
</div>
|
|
1204
1044
|
`,
|
|
1205
1045
|
})
|
|
1206
|
-
export class
|
|
1046
|
+
export class CustomWrapperComponent {
|
|
1207
1047
|
protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
|
|
1208
1048
|
self: true,
|
|
1209
1049
|
});
|
|
1210
1050
|
}
|
|
1211
1051
|
```
|
|
1212
1052
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
The `FormErrorDisplayDirective` also exposes warning messages from Vest.js:
|
|
1053
|
+
**When to create custom wrappers:**
|
|
1216
1054
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1055
|
+
- Match your design system (Material, PrimeNG, etc.)
|
|
1056
|
+
- Custom error formatting (tooltips, popovers, inline)
|
|
1057
|
+
- Add UI elements (icons, help text, character counters)
|
|
1058
|
+
- Specific accessibility patterns
|
|
1220
1059
|
|
|
1221
|
-
|
|
1222
|
-
selector: 'app-advanced-wrapper',
|
|
1223
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1224
|
-
hostDirectives: [FormErrorDisplayDirective],
|
|
1225
|
-
template: `
|
|
1226
|
-
<div class="form-field">
|
|
1227
|
-
<ng-content />
|
|
1060
|
+
> **📖 Detailed Guide**: See **[Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)** for Material Design examples, available signals reference, and best practices.
|
|
1228
1061
|
|
|
1229
|
-
|
|
1230
|
-
@if (errorDisplay.shouldShowErrors()) {
|
|
1231
|
-
<div class="errors" role="alert" aria-live="polite">
|
|
1232
|
-
@for (error of errorDisplay.errors(); track error) {
|
|
1233
|
-
<p class="error">{{ error.message || error }}</p>
|
|
1234
|
-
}
|
|
1235
|
-
</div>
|
|
1236
|
-
}
|
|
1062
|
+
### Cross-Field Validation: Three Complementary Features
|
|
1237
1063
|
|
|
1238
|
-
|
|
1239
|
-
@if (errorDisplay.warnings().length > 0) {
|
|
1240
|
-
<div class="warnings" role="status" aria-live="polite">
|
|
1241
|
-
@for (warning of errorDisplay.warnings(); track warning) {
|
|
1242
|
-
<p class="warning">{{ warning }}</p>
|
|
1243
|
-
}
|
|
1244
|
-
</div>
|
|
1245
|
-
}
|
|
1064
|
+
ngx-vest-forms provides three features for handling validation in complex, dynamic forms:
|
|
1246
1065
|
|
|
1247
|
-
|
|
1248
|
-
@if (errorDisplay.isPending()) {
|
|
1249
|
-
<div class="pending" aria-busy="true">
|
|
1250
|
-
<span class="spinner"></span>
|
|
1251
|
-
Validating...
|
|
1252
|
-
</div>
|
|
1253
|
-
}
|
|
1254
|
-
</div>
|
|
1255
|
-
`,
|
|
1256
|
-
})
|
|
1257
|
-
export class AdvancedWrapperComponent {
|
|
1258
|
-
protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
|
|
1259
|
-
self: true,
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
```
|
|
1066
|
+
> **💡 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.
|
|
1263
1067
|
|
|
1264
|
-
|
|
1068
|
+
| Feature | Purpose | Errors Appear At | Use For |
|
|
1069
|
+
| ----------------------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------- |
|
|
1070
|
+
| **`validationConfig`** | **Re-validation trigger** when fields change | **Field level** (`errors.fieldName`) | Fields that need re-validation when others change |
|
|
1071
|
+
| **`validateRootForm`** | Creates **form-level** validations | **Form level** (`errors.rootForm`) | Form-wide business rules |
|
|
1072
|
+
| **`triggerFormValidation()`** | Manual validation trigger for structure changes | N/A (triggers existing validations) | Structure changes without value changes |
|
|
1265
1073
|
|
|
1266
|
-
|
|
1074
|
+
**Key Insight**: These solve different problems and often work together!
|
|
1267
1075
|
|
|
1268
|
-
|
|
1269
|
-
// Error display control
|
|
1270
|
-
shouldShowErrors() // boolean - Whether to show errors based on mode and state
|
|
1271
|
-
errors() // string[] - Filtered errors (empty during pending)
|
|
1272
|
-
warnings() // string[] - Filtered warnings (empty during pending)
|
|
1273
|
-
isPending() // boolean - Whether async validation is running
|
|
1274
|
-
|
|
1275
|
-
// Raw state signals (from FormControlStateDirective)
|
|
1276
|
-
errorMessages() // string[] - All error messages
|
|
1277
|
-
warningMessages() // string[] - All warning messages
|
|
1278
|
-
controlState() // FormControlState - Complete control state
|
|
1279
|
-
isTouched() // boolean - Whether control has been touched
|
|
1280
|
-
isDirty() // boolean - Whether control value has changed
|
|
1281
|
-
isValid() // boolean - Whether control is valid
|
|
1282
|
-
isInvalid() // boolean - Whether control is invalid
|
|
1283
|
-
hasPendingValidation() // boolean - Whether validation is pending
|
|
1284
|
-
updateOn() // string - The ngModelOptions.updateOn value
|
|
1285
|
-
formSubmitted() // boolean - Whether form has been submitted
|
|
1286
|
-
```
|
|
1076
|
+
> **📖 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.
|
|
1287
1077
|
|
|
1288
|
-
|
|
1078
|
+
**Quick Examples:**
|
|
1289
1079
|
|
|
1290
1080
|
```typescript
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
inputs: ['errorDisplayMode'],
|
|
1301
|
-
},
|
|
1302
|
-
],
|
|
1303
|
-
host: {
|
|
1304
|
-
class: 'mat-form-field',
|
|
1305
|
-
'[class.mat-form-field-invalid]': 'errorDisplay.shouldShowErrors()',
|
|
1306
|
-
'[attr.aria-busy]': "errorDisplay.isPending() ? 'true' : null",
|
|
1307
|
-
},
|
|
1308
|
-
template: `
|
|
1309
|
-
<div class="mat-form-field-wrapper">
|
|
1310
|
-
<div class="mat-form-field-flex">
|
|
1311
|
-
<ng-content />
|
|
1312
|
-
</div>
|
|
1313
|
-
|
|
1314
|
-
<div class="mat-form-field-subscript-wrapper">
|
|
1315
|
-
@if (errorDisplay.shouldShowErrors()) {
|
|
1316
|
-
<div class="mat-error" role="alert" aria-live="assertive">
|
|
1317
|
-
@for (error of errorDisplay.errors(); track error) {
|
|
1318
|
-
<span>{{ error.message || error }}</span>
|
|
1319
|
-
}
|
|
1320
|
-
</div>
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
@if (errorDisplay.warnings().length > 0 && !errorDisplay.shouldShowErrors()) {
|
|
1324
|
-
<div class="mat-hint mat-warn" role="status">
|
|
1325
|
-
@for (warning of errorDisplay.warnings(); track warning) {
|
|
1326
|
-
<span>{{ warning }}</span>
|
|
1327
|
-
}
|
|
1328
|
-
</div>
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
@if (errorDisplay.isPending()) {
|
|
1332
|
-
<div class="mat-hint" aria-busy="true">
|
|
1333
|
-
<mat-spinner diameter="16"></mat-spinner>
|
|
1334
|
-
Validating...
|
|
1335
|
-
</div>
|
|
1336
|
-
}
|
|
1337
|
-
</div>
|
|
1338
|
-
</div>
|
|
1339
|
-
`,
|
|
1340
|
-
styles: [`
|
|
1341
|
-
:host {
|
|
1342
|
-
display: block;
|
|
1343
|
-
margin-bottom: 1rem;
|
|
1344
|
-
}
|
|
1081
|
+
// validationConfig: Re-validation trigger (NOT validation logic!)
|
|
1082
|
+
// This says: "When password changes, also re-validate confirmPassword"
|
|
1083
|
+
validationConfig = {
|
|
1084
|
+
password: ['confirmPassword'], // When password changes, revalidate confirmPassword
|
|
1085
|
+
};
|
|
1086
|
+
// The actual validation logic is in your Vest suite:
|
|
1087
|
+
test('confirmPassword', 'Must match', () => {
|
|
1088
|
+
enforce(model.confirmPassword).equals(model.password);
|
|
1089
|
+
});
|
|
1345
1090
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1091
|
+
// validateRootForm: Form-level business rules
|
|
1092
|
+
test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
|
|
1093
|
+
enforce(
|
|
1094
|
+
model.firstName === 'Brecht' &&
|
|
1095
|
+
model.lastName === 'Billiet' &&
|
|
1096
|
+
model.age === 30
|
|
1097
|
+
).isFalsy();
|
|
1098
|
+
});
|
|
1350
1099
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
font-size: 0.875rem;
|
|
1354
|
-
}
|
|
1100
|
+
// triggerFormValidation(): After structure changes
|
|
1101
|
+
protected readonly vestFormRef = viewChild.required('vestForm', { read: FormDirective });
|
|
1355
1102
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
`]
|
|
1360
|
-
})
|
|
1361
|
-
export class MatFieldWrapperComponent {
|
|
1362
|
-
protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
|
|
1363
|
-
self: true,
|
|
1364
|
-
});
|
|
1103
|
+
onTypeChange(type: string) {
|
|
1104
|
+
this.formValue.update(v => ({ ...v, type }));
|
|
1105
|
+
this.vestFormRef().triggerFormValidation(); // ✅ Force validation update
|
|
1365
1106
|
}
|
|
1366
1107
|
```
|
|
1367
1108
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
Once created, use your custom wrapper just like the built-in `sc-control-wrapper`:
|
|
1371
|
-
|
|
1372
|
-
```typescript
|
|
1373
|
-
@Component({
|
|
1374
|
-
imports: [vestForms, CustomControlWrapperComponent],
|
|
1375
|
-
template: `
|
|
1376
|
-
<form scVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
|
|
1377
|
-
<app-custom-control-wrapper>
|
|
1378
|
-
<label>Email</label>
|
|
1379
|
-
<input name="email" [ngModel]="formValue().email" type="email" />
|
|
1380
|
-
</app-custom-control-wrapper>
|
|
1381
|
-
</form>
|
|
1382
|
-
`
|
|
1383
|
-
})
|
|
1384
|
-
```
|
|
1385
|
-
|
|
1386
|
-
#### Best Practices for Custom Wrappers
|
|
1387
|
-
|
|
1388
|
-
1. **Use `hostDirectives`** - Always apply `FormErrorDisplayDirective` as a host directive for automatic state management
|
|
1389
|
-
2. **Respect accessibility** - Use proper ARIA attributes (`role="alert"`, `aria-live`, `aria-busy`)
|
|
1390
|
-
3. **Filter during pending** - The directive's `errors()` and `warnings()` signals automatically filter during validation
|
|
1391
|
-
4. **Leverage computed signals** - All exposed signals are computed, so they update automatically
|
|
1392
|
-
5. **Style based on state** - Use host bindings to apply CSS classes based on error display state
|
|
1393
|
-
|
|
1394
|
-
### Conditional validations
|
|
1109
|
+
### Validations on the Root Form
|
|
1395
1110
|
|
|
1396
|
-
|
|
1397
|
-
Assume we have a form model that has `age` and `emergencyContact`.
|
|
1398
|
-
The `emergencyContact` is required, but only when the person is not of legal age.
|
|
1111
|
+
For form-level validations that span multiple fields, use the `ROOT_FORM` constant with `validateRootForm`:
|
|
1399
1112
|
|
|
1400
|
-
|
|
1401
|
-
will not be done.
|
|
1113
|
+
> **⚠️ Breaking Change (v3)**: Default validation mode changed from `'live'` to `'submit'`. See [Migration Guide](./docs/MIGRATION-V3.md) for details.
|
|
1402
1114
|
|
|
1403
1115
|
```typescript
|
|
1404
|
-
import {
|
|
1405
|
-
|
|
1406
|
-
...
|
|
1407
|
-
omitWhen((model.age || 0) >= 18, () => {
|
|
1408
|
-
test('emergencyContact', 'Emergency contact is required', () => {
|
|
1409
|
-
enforce(model.emergencyContact).isNotBlank();
|
|
1410
|
-
});
|
|
1411
|
-
});
|
|
1412
|
-
```
|
|
1413
|
-
|
|
1414
|
-
You can put those validations on every field that you want. On form group fields and on form control fields.
|
|
1415
|
-
Check this interesting example below:
|
|
1416
|
-
|
|
1417
|
-
- [x] Password is always required
|
|
1418
|
-
- [x] Confirm password is only required when there is a password
|
|
1419
|
-
- [x] The passwords should match, but only when they are both filled in
|
|
1116
|
+
import { ROOT_FORM } from 'ngx-vest-forms';
|
|
1420
1117
|
|
|
1421
|
-
|
|
1422
|
-
test(
|
|
1423
|
-
enforce(model.
|
|
1424
|
-
});
|
|
1425
|
-
omitWhen(!model.passwords?.password, () => {
|
|
1426
|
-
test('passwords.confirmPassword', 'Confirm password is not filled in', () => {
|
|
1427
|
-
enforce(model.passwords?.confirmPassword).isNotBlank();
|
|
1428
|
-
});
|
|
1118
|
+
// In your suite
|
|
1119
|
+
test(ROOT_FORM, 'Passwords must match', () => {
|
|
1120
|
+
enforce(model.confirmPassword).equals(model.password);
|
|
1429
1121
|
});
|
|
1430
|
-
omitWhen(
|
|
1431
|
-
!model.passwords?.password || !model.passwords?.confirmPassword,
|
|
1432
|
-
() => {
|
|
1433
|
-
test('passwords', 'Passwords do not match', () => {
|
|
1434
|
-
enforce(model.passwords?.confirmPassword).equals(
|
|
1435
|
-
model.passwords?.password
|
|
1436
|
-
);
|
|
1437
|
-
});
|
|
1438
|
-
}
|
|
1439
|
-
);
|
|
1440
1122
|
```
|
|
1441
1123
|
|
|
1442
|
-
Forget about manually adding, removing validators on reactive forms and not being able to
|
|
1443
|
-
re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
|
|
1444
|
-
**Oh, it's also pretty readable**
|
|
1445
|
-
|
|
1446
|
-
### Validations on the Root Form
|
|
1447
|
-
|
|
1448
|
-
When we want to validate multiple fields that are depending on each other,
|
|
1449
|
-
it is a best practice to wrap them in a parent form group.
|
|
1450
|
-
If `password` and `confirmPassword` have to be equal the validation should not happen on
|
|
1451
|
-
`password` nor on `confirmPassword`, it should happen on `passwords`:
|
|
1452
|
-
|
|
1453
|
-
```typescript
|
|
1454
|
-
const form = {
|
|
1455
|
-
// validation happens here
|
|
1456
|
-
passwords: {
|
|
1457
|
-
password: '',
|
|
1458
|
-
confirmPassword: '',
|
|
1459
|
-
},
|
|
1460
|
-
};
|
|
1461
|
-
```
|
|
1462
|
-
|
|
1463
|
-
Sometimes we don't have the ability to create a form group for 2 depending fields, or sometimes we just
|
|
1464
|
-
want to create validation rules on portions of the form. For that we can use `validateRootForm`.
|
|
1465
|
-
Use the `errorsChange` output to keep the errors as state in a signal that we can use in the template
|
|
1466
|
-
wherever we want.
|
|
1467
|
-
|
|
1468
1124
|
```html
|
|
1469
|
-
{{ errors()?.['rootForm'] }}
|
|
1470
|
-
<!-- render the errors on the rootForm -->
|
|
1471
|
-
{{ errors() }}
|
|
1472
|
-
<!-- render all the errors -->
|
|
1473
1125
|
<form
|
|
1474
1126
|
scVestForm
|
|
1475
|
-
|
|
1476
|
-
[
|
|
1477
|
-
[formShape]="shape"
|
|
1127
|
+
validateRootForm
|
|
1128
|
+
[validateRootFormMode]="'submit'"
|
|
1478
1129
|
[suite]="suite"
|
|
1479
1130
|
(errorsChange)="errors.set($event)"
|
|
1480
|
-
|
|
1481
|
-
|
|
1131
|
+
>
|
|
1132
|
+
<!-- Display root-level errors -->
|
|
1133
|
+
{{ errors()?.['rootForm'] }}
|
|
1134
|
+
</form>
|
|
1482
1135
|
```
|
|
1483
1136
|
|
|
1484
|
-
|
|
1485
|
-
export class MyformComponent {
|
|
1486
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
1487
|
-
protected readonly suite = myFormModelSuite;
|
|
1488
|
-
// Keep the errors in state
|
|
1489
|
-
protected readonly errors = signal<Record<string, string>>({});
|
|
1490
|
-
}
|
|
1491
|
-
```
|
|
1137
|
+
#### Validation Modes
|
|
1492
1138
|
|
|
1493
|
-
|
|
1494
|
-
also create an ngValidator on root level, that listens to the ROOT_FORM field.
|
|
1139
|
+
Root form validation supports two modes:
|
|
1495
1140
|
|
|
1496
|
-
|
|
1141
|
+
- **`'submit'` (default)**: Validates only after the form is submitted. This is the recommended mode for most use cases as it prevents premature validation errors.
|
|
1142
|
+
- **`'live'`**: Validates on every value change, providing immediate feedback.
|
|
1497
1143
|
|
|
1498
|
-
```
|
|
1499
|
-
|
|
1144
|
+
```html
|
|
1145
|
+
<!-- Submit mode - validates after submit (default) -->
|
|
1146
|
+
<form scVestForm validateRootForm [validateRootFormMode]="'submit'">
|
|
1147
|
+
<!-- form controls -->
|
|
1148
|
+
</form>
|
|
1500
1149
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
model.age === 30
|
|
1506
|
-
).isFalsy();
|
|
1507
|
-
});
|
|
1150
|
+
<!-- Live mode - validates immediately -->
|
|
1151
|
+
<form scVestForm validateRootForm [validateRootFormMode]="'live'">
|
|
1152
|
+
<!-- form controls -->
|
|
1153
|
+
</form>
|
|
1508
1154
|
```
|
|
1509
1155
|
|
|
1510
|
-
### Validation of
|
|
1511
|
-
|
|
1512
|
-
Sometimes, form validations are dependent on the values of other form controls or groups.
|
|
1513
|
-
This scenario is common when a field's validity relies on the input of another field.
|
|
1514
|
-
A typical example is the `confirmPassword` field, which should only be validated if the `password` field is filled in.
|
|
1515
|
-
When the `password` field value changes, it necessitates re-validating the `confirmPassword` field to ensure
|
|
1516
|
-
consistency.
|
|
1517
|
-
|
|
1518
|
-
#### Understanding the Architecture: Why `validationConfig` Is Needed
|
|
1519
|
-
|
|
1520
|
-
Before diving into the implementation, it's important to understand the architectural boundaries between Vest.js and Angular:
|
|
1521
|
-
|
|
1522
|
-
**What Vest.js Handles:**
|
|
1156
|
+
### Validation of Dependent Controls
|
|
1523
1157
|
|
|
1524
|
-
|
|
1525
|
-
- ✅ Conditional validation with `omitWhen()`, `skipWhen()`
|
|
1526
|
-
- ✅ Field-level optimization with `only()`
|
|
1527
|
-
- ✅ Async validations with AbortController
|
|
1528
|
-
- ✅ Cross-field validation logic (e.g., "passwords must match")
|
|
1158
|
+
When field validations depend on other fields (e.g., `confirmPassword` depends on `password`), use `validationConfig` to trigger re-validation:
|
|
1529
1159
|
|
|
1530
|
-
**
|
|
1531
|
-
|
|
1532
|
-
- ❌ Trigger Angular to revalidate a different form control
|
|
1533
|
-
- ❌ Control Angular's form control lifecycle
|
|
1534
|
-
- ❌ Tell Angular "when field X changes, also validate field Y"
|
|
1535
|
-
|
|
1536
|
-
**Angular's Limitation:**
|
|
1537
|
-
Angular template-driven forms do not natively know about cross-field dependencies. When a field changes, only its own validators run automatically.
|
|
1538
|
-
|
|
1539
|
-
**How `validationConfig` Bridges This Gap:**
|
|
1540
|
-
|
|
1541
|
-
The `validationConfig` tells Angular's form system: "when field X changes, also call `updateValueAndValidity()` on field Y". This ensures that:
|
|
1542
|
-
|
|
1543
|
-
- Cross-field validations run at the right time
|
|
1544
|
-
- UI error states update correctly
|
|
1545
|
-
- Form validation state remains consistent
|
|
1546
|
-
|
|
1547
|
-
**Example of the Problem:**
|
|
1160
|
+
> **📖 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.
|
|
1548
1161
|
|
|
1549
1162
|
```typescript
|
|
1550
|
-
// In your Vest
|
|
1551
|
-
|
|
1552
|
-
|
|
1163
|
+
// In your suite - Vest handles the logic
|
|
1164
|
+
omitWhen(!model.password, () => {
|
|
1165
|
+
test('confirmPassword', 'Passwords must match', () => {
|
|
1166
|
+
enforce(model.confirmPassword).equals(model.password);
|
|
1167
|
+
});
|
|
1553
1168
|
});
|
|
1554
1169
|
```
|
|
1555
1170
|
|
|
1556
|
-
Without `validationConfig`: If user changes `password`, the `confirmPassword` field won't be revalidated automatically, even though its validity depends on the password value.
|
|
1557
|
-
|
|
1558
|
-
With `validationConfig`: Angular knows to revalidate `confirmPassword` whenever `password` changes.
|
|
1559
|
-
|
|
1560
|
-
**Architectural Benefits of This Separation:**
|
|
1561
|
-
|
|
1562
|
-
This separation of concerns provides several advantages:
|
|
1563
|
-
|
|
1564
|
-
- **Clarity**: Vest.js focuses on validation logic, `validationConfig` handles Angular orchestration
|
|
1565
|
-
- **Reusability**: Vest suites work across frameworks, while `validationConfig` is Angular-specific
|
|
1566
|
-
- **Maintainability**: Changes to validation logic don't affect dependency management
|
|
1567
|
-
- **Performance**: Only necessary validations run, only necessary controls revalidate
|
|
1568
|
-
- **Testability**: Validation logic can be tested independently from Angular form behavior
|
|
1569
|
-
|
|
1570
|
-
Here's how you can handle validation dependencies with ngx-vest-forms and vest.js:
|
|
1571
|
-
|
|
1572
|
-
Use Vest to create a suite where you define the conditional validations.
|
|
1573
|
-
For example, the `confirmPassword` field should only be validated when the `password` field is not empty.
|
|
1574
|
-
Additionally, you need to ensure that both fields match.
|
|
1575
|
-
|
|
1576
|
-
```typescript
|
|
1577
|
-
import { enforce, omitWhen, staticSuite, test } from 'vest';
|
|
1578
|
-
import { MyFormModel } from '../models/my-form.model';
|
|
1579
|
-
|
|
1580
|
-
import { enforce, omitWhen, only, staticSuite, test } from 'vest';
|
|
1581
|
-
|
|
1582
|
-
test('password', 'Password is required', () => {
|
|
1583
|
-
enforce(model.password).isNotBlank();
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
omitWhen(!model.password, () => {
|
|
1587
|
-
test('confirmPassword', 'Confirm password is required', () => {
|
|
1588
|
-
enforce(model.confirmPassword).isNotBlank();
|
|
1589
|
-
});
|
|
1590
|
-
});
|
|
1591
|
-
|
|
1592
|
-
omitWhen(!model.password || !model.confirmPassword, () => {
|
|
1593
|
-
test('passwords', 'Passwords do not match', () => {
|
|
1594
|
-
enforce(model.confirmPassword).equals(model.password);
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
);
|
|
1599
|
-
```
|
|
1600
|
-
|
|
1601
|
-
Creating a validation config.
|
|
1602
|
-
The `scVestForm` has an input called `validationConfig`, that we can use to let the system know when to retrigger validations.
|
|
1603
|
-
|
|
1604
1171
|
```typescript
|
|
1172
|
+
// In your component - validationConfig handles Angular orchestration
|
|
1605
1173
|
protected validationConfig = {
|
|
1606
|
-
|
|
1607
|
-
}
|
|
1174
|
+
password: ['confirmPassword'] // When password changes, revalidate confirmPassword
|
|
1175
|
+
};
|
|
1608
1176
|
```
|
|
1609
1177
|
|
|
1610
|
-
Here we see that when password changes, it needs to update the field `passwords.confirmPassword`.
|
|
1611
|
-
This validationConfig is completely dynamic, and can also be used for form arrays.
|
|
1612
|
-
|
|
1613
1178
|
```html
|
|
1614
|
-
<form scVestForm
|
|
1615
|
-
<
|
|
1616
|
-
|
|
1617
|
-
<input
|
|
1618
|
-
type="password"
|
|
1619
|
-
name="password"
|
|
1620
|
-
[ngModel]="formValue().passwords?.password"
|
|
1621
|
-
/>
|
|
1622
|
-
|
|
1623
|
-
<label>Confirm Password</label>
|
|
1624
|
-
<input
|
|
1625
|
-
type="password"
|
|
1626
|
-
name="confirmPassword"
|
|
1627
|
-
[ngModel]="formValue().passwords?.confirmPassword"
|
|
1628
|
-
/>
|
|
1629
|
-
</div>
|
|
1179
|
+
<form scVestForm [suite]="suite" [validationConfig]="validationConfig">
|
|
1180
|
+
<input name="password" [ngModel]="formValue().password" />
|
|
1181
|
+
<input name="confirmPassword" [ngModel]="formValue().confirmPassword" />
|
|
1630
1182
|
</form>
|
|
1631
1183
|
```
|
|
1632
1184
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
The `validationConfig` works seamlessly with different state management approaches. By default, most examples show using a single signal for both input and output:
|
|
1636
|
-
|
|
1637
|
-
```typescript
|
|
1638
|
-
// Standard pattern: single signal for both input and output
|
|
1639
|
-
protected readonly formValue = signal<MyFormModel>({});
|
|
1640
|
-
|
|
1641
|
-
// Template
|
|
1642
|
-
<form scVestForm
|
|
1643
|
-
[formValue]="formValue()"
|
|
1644
|
-
(formValueChange)="formValue.set($event)"
|
|
1645
|
-
[validationConfig]="validationConfig">
|
|
1646
|
-
```
|
|
1647
|
-
|
|
1648
|
-
However, you can also use **separate signals** for input and output if your application architecture requires it:
|
|
1649
|
-
|
|
1650
|
-
```typescript
|
|
1651
|
-
// Advanced pattern: separate input and output signals
|
|
1652
|
-
protected readonly inputFormValue = signal<MyFormModel>({});
|
|
1653
|
-
protected readonly outputFormValue = signal<MyFormModel>({});
|
|
1654
|
-
|
|
1655
|
-
// Template
|
|
1656
|
-
<form scVestForm
|
|
1657
|
-
[formValue]="inputFormValue()"
|
|
1658
|
-
(formValueChange)="handleFormChange($event)"
|
|
1659
|
-
[validationConfig]="validationConfig">
|
|
1660
|
-
<input name="password" [ngModel]="outputFormValue().password" />
|
|
1661
|
-
<input name="confirmPassword" [ngModel]="outputFormValue().confirmPassword" />
|
|
1662
|
-
</form>
|
|
1663
|
-
|
|
1664
|
-
// Component
|
|
1665
|
-
handleFormChange(value: MyFormModel) {
|
|
1666
|
-
// Update output signal independently
|
|
1667
|
-
this.outputFormValue.set(value);
|
|
1668
|
-
// Optionally sync input signal or perform other logic
|
|
1669
|
-
}
|
|
1670
|
-
```
|
|
1671
|
-
|
|
1672
|
-
**Why this works**: The validation configuration operates at the **form control level**, listening directly to Angular's form control changes rather than component signals. This makes it independent of your chosen state management pattern.
|
|
1673
|
-
|
|
1674
|
-
This pattern is useful when:
|
|
1675
|
-
|
|
1676
|
-
- You need different processing logic for form inputs vs outputs
|
|
1677
|
-
- You're integrating with state management libraries
|
|
1678
|
-
- You want to maintain separate concerns between form display and form handling
|
|
1185
|
+
**Why `validationConfig` is needed**: Vest.js handles validation logic, but cannot tell Angular to revalidate other form controls. The `validationConfig` bridges this gap by telling Angular's form system which fields should be revalidated when others change.
|
|
1186
|
+
There is also a complex example of form arrays with complex validations in the examples.
|
|
1679
1187
|
|
|
1680
|
-
|
|
1188
|
+
### Child Form Components
|
|
1681
1189
|
|
|
1682
|
-
|
|
1683
|
-
There is also a complex example of form arrays with complex validations in the examples.
|
|
1190
|
+
Large forms are difficult to maintain in a single file. ngx-vest-forms supports splitting forms into reusable child components, which is essential for code organization and component reusability (like address forms used in multiple places).
|
|
1684
1191
|
|
|
1685
|
-
|
|
1192
|
+
**Critical Requirement:**
|
|
1686
1193
|
|
|
1687
|
-
|
|
1688
|
-
For instance an address form can be reused, so we want to create a child component for that.
|
|
1689
|
-
We have to make sure that this child component can access the ngForm.
|
|
1690
|
-
For that we have to use the `vestFormViewProviders` from `ngx-vest-forms`
|
|
1194
|
+
Child components that contain form fields **MUST** use `vestFormsViewProviders` to access the parent form:
|
|
1691
1195
|
|
|
1692
1196
|
```typescript
|
|
1693
|
-
|
|
1694
|
-
import { input } from '@angular/core';
|
|
1197
|
+
import { Component, input } from '@angular/core';
|
|
1695
1198
|
import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
|
|
1696
1199
|
|
|
1697
1200
|
@Component({
|
|
1698
|
-
|
|
1699
|
-
viewProviders: [vestFormsViewProviders]
|
|
1201
|
+
selector: 'app-address',
|
|
1202
|
+
viewProviders: [vestFormsViewProviders], // ⚠️ REQUIRED for child form components
|
|
1203
|
+
template: `
|
|
1204
|
+
<div sc-control-wrapper>
|
|
1205
|
+
<label>Street</label>
|
|
1206
|
+
<input [ngModel]="address().street" name="street" />
|
|
1207
|
+
</div>
|
|
1208
|
+
<!-- More address fields... -->
|
|
1209
|
+
`,
|
|
1700
1210
|
})
|
|
1701
1211
|
export class AddressComponent {
|
|
1702
1212
|
readonly address = input<AddressModel>();
|
|
1703
1213
|
}
|
|
1704
1214
|
```
|
|
1705
1215
|
|
|
1216
|
+
**Key Benefits:**
|
|
1217
|
+
|
|
1218
|
+
- **Component Reusability** - Share address, phone number, or other form sections across multiple forms
|
|
1219
|
+
- **Code Organization** - Keep large forms manageable by splitting them into logical sections
|
|
1220
|
+
- **Type Safety** - Each child component can have its own strongly-typed model
|
|
1221
|
+
- **Dynamic Field Names** - Use inputs to customize field name prefixes (e.g., `billingAddress.street` vs `shippingAddress.street`)
|
|
1222
|
+
|
|
1223
|
+
> **📖 Detailed Guide**: See **[Child Components](./docs/CHILD-COMPONENTS.md)** for complete examples including dynamic field names, nested child components, and troubleshooting common issues.
|
|
1224
|
+
|
|
1706
1225
|
## Features
|
|
1707
1226
|
|
|
1708
1227
|
Now that you've seen how ngx-vest-forms works, here's a complete overview of its capabilities:
|
|
@@ -1710,7 +1229,8 @@ Now that you've seen how ngx-vest-forms works, here's a complete overview of its
|
|
|
1710
1229
|
### Core Features
|
|
1711
1230
|
|
|
1712
1231
|
- **Unidirectional Data Flow** - Predictable state management with Angular signals
|
|
1713
|
-
- **Type Safety** - Full TypeScript support with `
|
|
1232
|
+
- **Type Safety** - Full TypeScript support with `NgxDeepPartial<T>` and `NgxDeepRequired<T>`
|
|
1233
|
+
- **Enhanced Field Path Types** - Template literal autocomplete for field names ([Documentation](./docs/FIELD-PATHS.md))
|
|
1714
1234
|
- **Zero Boilerplate** - Automatic FormControl and FormGroup creation
|
|
1715
1235
|
- **Shape Validation** - Runtime validation against your TypeScript models (dev mode)
|
|
1716
1236
|
|
|
@@ -1746,6 +1266,20 @@ Now that you've seen how ngx-vest-forms works, here's a complete overview of its
|
|
|
1746
1266
|
|
|
1747
1267
|
For comprehensive documentation beyond this README, check out our detailed guides:
|
|
1748
1268
|
|
|
1269
|
+
- **[Field Path Types](./docs/FIELD-PATHS.md)** - Type-safe field references with IDE autocomplete
|
|
1270
|
+
- Template literal types for field paths
|
|
1271
|
+
- `ValidationConfigMap<T>` for type-safe configs
|
|
1272
|
+
- `FormFieldName<T>` for Vest suites
|
|
1273
|
+
- Migration guide and best practices
|
|
1274
|
+
- Performance considerations
|
|
1275
|
+
- **[Utility Types & Functions Reference](./projects/ngx-vest-forms/src/lib/utils/README.md)** - Complete guide to all utility types and functions
|
|
1276
|
+
- Type utilities: `NgxDeepPartial`, `NgxDeepRequired`, `NgxFormCompatibleDeepRequired`
|
|
1277
|
+
- Form utilities: `getAllFormErrors()`, `setValueAtPath()`, `mergeValuesAndRawValues()`
|
|
1278
|
+
- Array/Object conversion: `arrayToObject()`, `deepArrayToObject()`, `objectToArray()`
|
|
1279
|
+
- Field path utilities: `parseFieldPath()`, `stringifyFieldPath()`
|
|
1280
|
+
- Field clearing: `clearFieldsWhen()`, `clearFields()`, `keepFieldsWhen()`
|
|
1281
|
+
- Equality utilities: `shallowEqual()`, `fastDeepEqual()`
|
|
1282
|
+
- Shape validation: `validateShape()`
|
|
1749
1283
|
- **[Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Advanced handling of conditional form scenarios
|
|
1750
1284
|
- Alternative approaches and their trade-offs
|
|
1751
1285
|
- Performance considerations and best practices
|
|
@@ -1856,4 +1390,3 @@ These pioneers laid the groundwork that made ngx-vest-forms possible, combining
|
|
|
1856
1390
|
## License
|
|
1857
1391
|
|
|
1858
1392
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1859
|
-
````
|