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 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**: >=22.0.0 (Required for Angular 18+)
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, DeepPartial } from 'ngx-vest-forms';
118
+ import { vestForms, NgxDeepPartial } from 'ngx-vest-forms';
81
119
 
82
- type MyFormModel = DeepPartial<{
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, DeepPartial } from 'ngx-vest-forms';
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 = DeepPartial<{
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
- ## Complete Example
182
+ ### Minimal Example
145
183
 
146
- Let's see a complete working form with validation to understand how everything fits together:
184
+ Here's the absolute minimum to get started with ngx-vest-forms:
147
185
 
148
186
  ```typescript
149
- import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
150
- import { staticSuite, test, enforce, only } from 'vest';
151
- import { vestForms, DeepPartial, DeepRequired } from 'ngx-vest-forms';
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-user-form',
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
- <!-- Each field wrapped for error display -->
202
- <div sc-control-wrapper>
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
- <div sc-control-wrapper>
208
- <label>Last Name</label>
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 UserFormComponent {
222
- protected readonly formValue = signal<UserFormModel>({});
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 it! You now have a fully functional form with:
223
+ That's all you need! The `scVestForm` directive automatically:
233
224
 
234
- - Type-safe form model
235
- - Automatic form control creation
236
- - Validation on blur and submit
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
- ## Core Concepts
229
+ ## Complete Example
241
230
 
242
- ### Understanding Form State
231
+ A complete working form with ngx-vest-forms requires just 4 steps:
243
232
 
244
- The form value will be automatically populated like this:
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
- ```typescript
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
- The ngForm will contain automatically created FormGroups and FormControls.
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
- ```typescript
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
- The `scVestForm` directive offers these outputs:
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 the form value changes (debounced since template-driven forms are created over time) |
276
- | `dirtyChange` | Emits when the dirty state of the form changes |
277
- | `validChange` | Emits when the form becomes valid or invalid |
278
- | `errorsChange` | Emits the complete list of errors for the form and all its controls |
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 { DeepPartial } from 'ngx-vest-forms';
266
+ import { NgxDeepPartial } from 'ngx-vest-forms';
292
267
 
293
- type MyFormModel = DeepPartial<{
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
- This is how you write a basic Vest suite:
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
- test('firstName', 'First name is required', () => {
412
- enforce(model.firstName).isNotBlank();
413
- });
468
+ export const myFormSuite = staticSuite((model: MyFormModel, field?) => {
469
+ only(field); // Call unconditionally at top
414
470
 
415
- test('lastName', 'Last name is required', () => {
416
- enforce(model.lastName).isNotBlank();
417
- });
418
- }
419
- );
471
+ test('firstName', 'First name is required', () => {
472
+ enforce(model.firstName).isNotBlank();
473
+ });
474
+ });
420
475
  ```
421
476
 
422
- In the `test` function:
477
+ **Test parameters**: `test(fieldName, errorMessage, validationFunction)`
423
478
 
424
- - **First parameter**: The field name (use dot notation for nested fields: `addresses.billingAddress.street`)
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
- ngx-vest-forms automatically optimizes validation performance by running validations only for the field being interacted with:
554
+ **Critical**: Always call `only()` unconditionally at the top of your validation suite:
502
555
 
503
556
  ```typescript
504
- import { enforce, only, staticSuite, test } from 'vest';
505
-
506
- export const myFormModelSuite = staticSuite(
507
- (model: MyFormModel, field?: string) => {
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
- test('firstName', 'First name is required', () => {
514
- enforce(model.firstName).isNotBlank();
515
- });
516
- test('lastName', 'Last name is required', () => {
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
- This pattern ensures:
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
- - ✅ During typing/blur: Only the current field validates (better performance)
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
- > Always include the optional `field?: string` parameter in your suite and use the `only(field)` pattern. The library automatically passes the field name during individual field validation.
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
- ## Intermediate Topics
643
+ #### Configurable Validation Config Debounce
601
644
 
602
- ### Conditional Fields
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
- What if we want to remove a form control or form group? With reactive forms that would require a lot of work
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
- Let's hide `lastName` if `firstName` is not filled in:
649
+ ```typescript
650
+ import { ApplicationConfig } from '@angular/core';
651
+ import { NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN } from 'ngx-vest-forms';
610
652
 
611
- ```html
612
- <div ngModelGroup="generalInfo">
613
- <label>First name</label>
614
- <input
615
- type="text"
616
- name="firstName"
617
- [ngModel]="formValue().generalInformation?.firstName"
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
- @if(lastNameAvailable()){
621
- <label>Last name</label>
622
- <input
623
- type="text"
624
- name="lastName"
625
- [ngModel]="formValue().generalInformation?.lastName"
626
- />
627
- }
628
- </div>
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
- protected readonly lastNameAvailable =
635
- computed(() => !!this.formValue().generalInfo?.firstName);
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(showGeneralInfo()){
644
- <div ngModelGroup="generalInfo">
645
- <label>First name</label>
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
- ```typescript
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 is not filled in', () => {
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
- omitWhen(
722
- !model.passwords?.password || !model.passwords?.confirmPassword,
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
- ## Advanced Topics
789
+ ### Dependent Field Validation with Conditional Rendering
738
790
 
739
- ### Handling Form Structure Changes
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
- When form structure changes dynamically in combination with _NON_ form elements (e.g., conditional fields are shown/hidden), the validation state may not update automatically since no control values change. For these scenarios, use the `triggerFormValidation()` method:
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
- // Form structure changes based on selection
747
- @if (procedureType() === 'typeA') {
748
- <input name="fieldA" [ngModel]="formValue().fieldA" />
749
- }
750
- @else if (procedureType() === 'typeB') {
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, viewChild } from '@angular/core';
811
+ import { Component, signal, computed } from '@angular/core';
765
812
 
766
813
  @Component({
767
814
  template: `
768
- <form
769
- scVestForm
770
- [suite]="validationSuite"
771
- (formValueChange)="formValue.set($event)"
772
- #vestForm="scVestForm"
773
- >
774
- <select
775
- name="procedureType"
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 MyFormComponent {
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
- #### When to Use `triggerFormValidation()`
830
+ // Computed config only references controls that exist
831
+ protected readonly validationConfig = computed(() => {
832
+ const config: Record<string, string[]> = {};
818
833
 
819
- Call this method in these scenarios:
820
-
821
- - **After changing form structure** - When conditional fields are shown/hidden
822
- - **After clearing form sections** - When resetting parts of the form
823
- - **After dynamic field addition/removal** - When programmatically modifying form structure
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
- ##### When Field Clearing is Required
840
+ return config;
841
+ });
842
+ }
843
+ ```
829
844
 
830
- Field clearing utilities are **specifically needed** when conditional logic switches between:
845
+ **Key Points:**
831
846
 
832
- - **Form inputs** (`<input>`, `<select>`, `<textarea>`) **NON-form elements** (`<p>`, `<div>`, informational content)
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
- **Primary Use Case - The Problem Scenario:**
852
+ **Template Binding:**
835
853
 
836
- ```typescript
837
- // This template structure REQUIRES manual field clearing:
838
- @if (procedureType === 'typeA') {
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
- **Why This Creates State Inconsistency:**
859
+ ## Advanced Topics
848
860
 
849
- 1. **Switching FROM form input TO non-form content:** Angular removes FormControl, but component signal retains old value
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
- ```typescript
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
- // After switching (WITHOUT manual clearing):
859
- formValue() = { procedureType: 'typeC', fieldA: 'some-value' }; // ❌ Stale fieldA!
860
- ngForm.form.value = { procedureType: 'typeC' }; // Clean
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
- // After switching (WITH manual clearing):
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
- ##### When Field Clearing is NOT Required
872
+ ### Handling Form Structure Changes
868
873
 
869
- Pure form-to-form conditionals usually don't need manual field clearing:
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
- // This template structure usually DOES NOT require manual field clearing:
873
- @if (inputType === 'text') {
874
- <input name="field" [ngModel]="formValue().field" type="text" /> // Form input
875
- } @else if (inputType === 'number') {
876
- <input name="field" [ngModel]="formValue().field" type="number" /> // Form input
877
- } @else if (inputType === 'email') {
878
- <input name="field" [ngModel]="formValue().field" type="email" /> // Form input
879
- }
880
- ```
881
-
882
- **Why:** All branches contain form inputs with the same `name` attribute, so Angular maintains the FormControl and your component state naturally stays consistent.
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
- **The Pattern:**
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
- ```typescript
887
- onStructureChange(newValue: string) {
888
- this.formValue.update((current) => ({
889
- ...current,
890
- procedureType: newValue,
891
- // Clear fields that are no longer relevant
892
- ...(newValue !== 'typeA' && { fieldA: undefined }),
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
- #### Alternative: Utility Helper
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
- For cleaner code, you can use the built-in utility functions from ngx-vest-forms:
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
- ```typescript
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
- // In your component method
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
- // Trigger validation update after structure change
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
- **Additional Utility Functions:**
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
- import { clearFields, keepFieldsWhen } from 'ngx-vest-forms';
925
+ // Use clearFieldsWhen to synchronize state
926
+ import { clearFieldsWhen } from 'ngx-vest-forms';
926
927
 
927
- // Clear specific fields unconditionally
928
- const cleanedState = clearFields(currentFormValue, ['fieldA', 'fieldB']);
928
+ this.formValue.update((v) =>
929
+ clearFieldsWhen(v, {
930
+ fieldA: procedureType !== 'typeA', // Clear when NOT showing input
931
+ })
932
+ );
933
+ ```
929
934
 
930
- // Keep only fields that meet conditions
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
- ```typescript
939
- import { staticSuite, test, enforce, omitWhen, only } from 'vest';
937
+ ##### When Field Clearing is NOT Required
940
938
 
941
- export const myValidationSuite = staticSuite(
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
- // Always validate procedure type
948
- test('procedureType', 'Procedure type is required', () => {
949
- enforce(model.procedureType).isNotBlank();
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
- // Conditional validations
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
- omitWhen(model.procedureType !== 'typeB', () => {
960
- test('fieldB', 'Field B is required for Type B', () => {
961
- enforce(model.fieldB).isNotBlank();
962
- });
963
- });
952
+ ### Field State Utilities
964
953
 
965
- // Note: No validation needed for typeC as it has no input fields
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
- ### Field State Utilities
956
+ **Key Utilities:**
971
957
 
972
- ngx-vest-forms provides utility functions specifically designed for the scenario where conditional logic switches between **form inputs** and **non-form elements** (like informational text, paragraphs, or other non-input content).
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
- > **Key Use Case:** These utilities are primarily needed when your template conditionally renders form inputs in some branches and non-form content in others. They ensure your component state stays consistent with the actual form structure.
962
+ **When to Use:**
975
963
 
976
- ### `clearFieldsWhen`
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
- Conditionally clears fields when switching from form inputs to non-form content.
966
+ **Example:**
979
967
 
980
968
  ```typescript
981
969
  import { clearFieldsWhen } from 'ngx-vest-forms';
982
970
 
983
- // EXAMPLE: Clear fields when switching to non-form content (typeC shows info text, not input)
984
- const updatedState = clearFieldsWhen(currentFormValue, {
985
- fieldA: procedureType !== 'typeA', // Clear when NOT showing fieldA input
986
- fieldB: procedureType !== 'typeB', // Clear when NOT showing fieldB input
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
- ### `clearFields`
978
+ **Important Note:**
993
979
 
994
- Unconditionally clears specific fields. Useful for form reset operations or cleanup tasks.
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
- ```typescript
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
- ### `keepFieldsWhen`
984
+ ### Composable Validations
1007
985
 
1008
- Creates a new state containing only fields that meet specified conditions. Takes a "whitelist" approach instead of clearing unwanted fields.
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
- test(`${field}.zipcode`, 'Zipcode is required', () => {
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
- ```typescript
1119
- import { enforce, omitWhen, only, staticSuite, test } from 'vest';
1120
- import { PurchaseFormModel } from '../models/purchaseFormModel';
1121
-
1122
- export const mySuite = staticSuite(
1123
- (model: PurchaseFormModel, field?: string) => {
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
- We achieved decoupling, readability and reuse of our addressValidations.
1013
+ **Benefits:**
1140
1014
 
1141
- #### A More Complex Example
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
- Let's combine the conditional part with the reusable part.
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
- This gives us validation on:
1022
+ ### Custom Control Wrappers
1149
1023
 
1150
- - [x] The addresses form field (they can't be equal)
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-control-wrapper',
1181
- changeDetection: ChangeDetectionStrategy.OnPush,
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-message">
1034
+ <div class="error">
1194
1035
  @for (error of errorDisplay.errors(); track error) {
1195
- <span>{{ error.message || error }}</span>
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 CustomControlWrapperComponent {
1046
+ export class CustomWrapperComponent {
1207
1047
  protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
1208
1048
  self: true,
1209
1049
  });
1210
1050
  }
1211
1051
  ```
1212
1052
 
1213
- ##### Advanced Custom Wrapper with Warnings
1214
-
1215
- The `FormErrorDisplayDirective` also exposes warning messages from Vest.js:
1053
+ **When to create custom wrappers:**
1216
1054
 
1217
- ```typescript
1218
- import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
1219
- import { FormErrorDisplayDirective } from 'ngx-vest-forms';
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
- @Component({
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
- <!-- Errors -->
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
- <!-- Warnings (non-blocking feedback) -->
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
- <!-- Pending state -->
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
- ##### Available Signals from FormErrorDisplayDirective
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
- The directive exposes these computed signals for building custom UIs:
1074
+ **Key Insight**: These solve different problems and often work together!
1267
1075
 
1268
- ```typescript
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
- ##### Real-World Example: Material Design Style Wrapper
1078
+ **Quick Examples:**
1289
1079
 
1290
1080
  ```typescript
1291
- import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
1292
- import { FormErrorDisplayDirective } from 'ngx-vest-forms';
1293
-
1294
- @Component({
1295
- selector: 'app-mat-field-wrapper',
1296
- changeDetection: ChangeDetectionStrategy.OnPush,
1297
- hostDirectives: [
1298
- {
1299
- directive: FormErrorDisplayDirective,
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
- .mat-error {
1347
- color: #f44336;
1348
- font-size: 0.875rem;
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
- .mat-hint {
1352
- color: rgba(0, 0, 0, 0.6);
1353
- font-size: 0.875rem;
1354
- }
1100
+ // triggerFormValidation(): After structure changes
1101
+ protected readonly vestFormRef = viewChild.required('vestForm', { read: FormDirective });
1355
1102
 
1356
- .mat-warn {
1357
- color: #ff9800;
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
- ##### Using Your Custom Wrapper
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
- Vest makes it extremely easy to create conditional validations.
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
- We can use the `omitWhen` so that when the person is below 18, the assertion
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 { enforce, omitWhen, only, staticSuite, test } from 'vest';
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
- ```typescript
1422
- test('passwords.password', 'Password is not filled in', () => {
1423
- enforce(model.passwords?.password).isNotBlank();
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
- [formValue]="formValue()"
1476
- [validateRootForm]="true"
1477
- [formShape]="shape"
1127
+ validateRootForm
1128
+ [validateRootFormMode]="'submit'"
1478
1129
  [suite]="suite"
1479
1130
  (errorsChange)="errors.set($event)"
1480
- ...
1481
- ></form>
1131
+ >
1132
+ <!-- Display root-level errors -->
1133
+ {{ errors()?.['rootForm'] }}
1134
+ </form>
1482
1135
  ```
1483
1136
 
1484
- ```typescript
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
- When setting the `[validateRootForm]` directive to true, the form will
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
- To make this work we need to use the field in the vest suite like this:
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
- ```typescript
1499
- import { ROOT_FORM } from 'ngx-vest-forms';
1144
+ ```html
1145
+ <!-- Submit mode - validates after submit (default) -->
1146
+ <form scVestForm validateRootForm [validateRootFormMode]="'submit'">
1147
+ <!-- form controls -->
1148
+ </form>
1500
1149
 
1501
- test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
1502
- enforce(
1503
- model.firstName === 'Brecht' &&
1504
- model.lastName === 'Billiet' &&
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 dependant controls and or groups
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
- - Validation logic and rules
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
- **What Vest.js Cannot Do:**
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 suite
1551
- test('confirmPassword', 'Passwords must match', () => {
1552
- enforce(model.confirmPassword).equals(model.password);
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
- password: ['passwords.confirmPassword']
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 ... [validationConfig]="validationConfig">
1615
- <div ngModelGroup="passwords">
1616
- <label>Password</label>
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
- #### Advanced State Management Patterns
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
- #### Form array validations
1188
+ ### Child Form Components
1681
1189
 
1682
- An example can be found [in this simplified courses article](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)
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
- ### Child form components
1192
+ **Critical Requirement:**
1686
1193
 
1687
- Big forms result in big files. It makes sense to split them up.
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 `DeepPartial<T>` and `DeepRequired<T>`
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
- ````