ngx-vest-forms 1.4.0 → 1.4.2

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
@@ -13,7 +13,7 @@
13
13
 
14
14
  ⭐ If you like this project, star it on GitHub — it helps a lot!
15
15
 
16
- [Overview](#overview) • [Getting Started](#getting-started) • [Features](#features) • [Basic Usage](#basic-usage) • [Examples](#examples) • [Form Structure Changes](#handling-form-structure-changes) • [Field State Utilities](#field-state-utilities) • [Documentation](#documentation) • [Resources](#resources) • [Developer Resources](#developer-resources) • [Acknowledgments](#acknowledgments)
16
+ [Overview](#overview) • [Getting Started](#getting-started) • [Complete Example](#complete-example) • [Core Concepts](#core-concepts) • [Validation](#validation) • [Intermediate Topics](#intermediate-topics) • [Advanced Topics](#advanced-topics) • [Features](#features) • [Documentation](#documentation) • [Resources](#resources) • [Developer Resources](#developer-resources) • [Acknowledgments](#acknowledgments)
17
17
 
18
18
  </div>
19
19
 
@@ -141,39 +141,105 @@ Your form automatically creates FormGroups and FormControls with type-safe, unid
141
141
  > [!IMPORTANT]
142
142
  > Notice we use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow, and the `?` operator since template-driven forms are `DeepPartial`.
143
143
 
144
- ## Features
144
+ ## Complete Example
145
145
 
146
- ### Core Features
146
+ Let's see a complete working form with validation to understand how everything fits together:
147
147
 
148
- - **Unidirectional Data Flow** - Predictable state management with Angular signals
149
- - **Type Safety** - Full TypeScript support with `DeepPartial<T>` and `DeepRequired<T>`
150
- - **Zero Boilerplate** - Automatic FormControl and FormGroup creation
151
- - **Shape Validation** - Runtime validation against your TypeScript models (dev mode)
148
+ ```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
152
 
153
- ### Advanced Validation
153
+ // 1. Define your form model (always DeepPartial)
154
+ type UserFormModel = DeepPartial<{
155
+ firstName: string;
156
+ lastName: string;
157
+ email: string;
158
+ }>;
154
159
 
155
- - **Async Validations** - Built-in support with AbortController
156
- - **Conditional Logic** - Use `omitWhen()` for conditional validation rules
157
- - **Composable Suites** - Reusable validation functions across projects
158
- - **Custom Debouncing** - Configure validation timing per field or form
160
+ // 2. Create a shape for runtime validation (recommended)
161
+ const userFormShape: DeepRequired<UserFormModel> = {
162
+ firstName: '',
163
+ lastName: '',
164
+ email: '',
165
+ };
159
166
 
160
- ### Dynamic Forms
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
+ }
161
173
 
162
- - **Conditional Fields** - Show/hide fields based on form state
163
- - **Form Arrays** - Dynamic lists with add/remove functionality
164
- - **Reactive Disabling** - Disable fields based on computed signals
165
- - **State Management** - Preserve field state across conditional rendering
166
- - **Structure Change Detection** - Manual trigger for validation updates when form structure changes
174
+ test('firstName', 'First name is required', () => {
175
+ enforce(model.firstName).isNotBlank();
176
+ });
167
177
 
168
- ### Developer Experience
178
+ test('lastName', 'Last name is required', () => {
179
+ enforce(model.lastName).isNotBlank();
180
+ });
169
181
 
170
- - **Runtime Shape Checking** - Catch typos in `name` attributes early
171
- - **Built-in Error Display** - `sc-control-wrapper` component for consistent UX
172
- - **Validation Config** - Declare field dependencies for complex scenarios
173
- - **Field State Utilities** - Helper functions for managing dynamic form state
174
- - **Modern Angular** - Built for Angular 18+ with standalone components
182
+ test('email', 'Valid email is required', () => {
183
+ enforce(model.email).isEmail();
184
+ });
185
+ }
186
+ );
175
187
 
176
- ## Basic Usage
188
+ // 4. Create the component
189
+ @Component({
190
+ selector: 'app-user-form',
191
+ imports: [vestForms],
192
+ changeDetection: ChangeDetectionStrategy.OnPush,
193
+ template: `
194
+ <form
195
+ scVestForm
196
+ [suite]="suite"
197
+ [formShape]="shape"
198
+ (formValueChange)="formValue.set($event)"
199
+ (ngSubmit)="onSubmit()"
200
+ >
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>
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>
216
+
217
+ <button type="submit">Submit</button>
218
+ </form>
219
+ `,
220
+ })
221
+ export class UserFormComponent {
222
+ protected readonly formValue = signal<UserFormModel>({});
223
+ protected readonly suite = userValidationSuite;
224
+ protected readonly shape = userFormShape;
225
+
226
+ protected onSubmit() {
227
+ console.log('Form submitted:', this.formValue());
228
+ }
229
+ }
230
+ ```
231
+
232
+ That's it! You now have a fully functional form with:
233
+
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)
239
+
240
+ ## Core Concepts
241
+
242
+ ### Understanding Form State
177
243
 
178
244
  The form value will be automatically populated like this:
179
245
 
@@ -217,7 +283,28 @@ The `scVestForm` directive offers these outputs:
217
283
  | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
218
284
  | `triggerFormValidation()` | Manually triggers form validation update when form structure changes without value changes (e.g., conditional fields) |
219
285
 
220
- ### Avoiding typo's
286
+ ### Form Models and Type Safety
287
+
288
+ All form models in ngx-vest-forms use `DeepPartial<T>` because Angular's template-driven forms build up values incrementally:
289
+
290
+ ```typescript
291
+ import { DeepPartial } from 'ngx-vest-forms';
292
+
293
+ type MyFormModel = DeepPartial<{
294
+ generalInfo: {
295
+ firstName: string;
296
+ lastName: string;
297
+ };
298
+ }>;
299
+ ```
300
+
301
+ This is why you must use the `?` operator in templates:
302
+
303
+ ```html
304
+ <input [ngModel]="formValue().generalInfo?.firstName" name="firstName" />
305
+ ```
306
+
307
+ ### Shape Validation: Catching Typos Early
221
308
 
222
309
  Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
223
310
  Making a typo in those can result in a time-consuming endeavor. For this we have introduced shapes.
@@ -302,7 +389,217 @@ Error: Shape mismatch:
302
389
  at map.js:7:24
303
390
  ```
304
391
 
305
- ### Conditional fields
392
+ ## Validation
393
+
394
+ ngx-vest-forms uses [Vest.js](https://vestjs.dev) for validation - a lightweight, flexible validation framework that works across any JavaScript environment.
395
+
396
+ ### Creating Your First Validation Suite
397
+
398
+ This is how you write a basic Vest suite:
399
+
400
+ ```typescript
401
+ 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
+
411
+ test('firstName', 'First name is required', () => {
412
+ enforce(model.firstName).isNotBlank();
413
+ });
414
+
415
+ test('lastName', 'Last name is required', () => {
416
+ enforce(model.lastName).isNotBlank();
417
+ });
418
+ }
419
+ );
420
+ ```
421
+
422
+ In the `test` function:
423
+
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
427
+
428
+ ### Connecting Validation to Your Form
429
+
430
+ The biggest pain point ngx-vest-forms solves: **Connecting Vest suites to Angular with zero boilerplate**:
431
+
432
+ ```typescript
433
+ // Component
434
+ class MyComponent {
435
+ protected readonly formValue = signal<MyFormModel>({});
436
+ protected readonly suite = myFormModelSuite;
437
+ }
438
+ ```
439
+
440
+ ```html
441
+ <!-- Template -->
442
+ <form
443
+ scVestForm
444
+ [suite]="suite"
445
+ (formValueChange)="formValue.set($event)"
446
+ (ngSubmit)="onSubmit()"
447
+ >
448
+ ...
449
+ </form>
450
+ ```
451
+
452
+ That's it! Validations are completely wired. Behind the scenes:
453
+
454
+ 1. Control gets created, Angular recognizes the `ngModel` directives
455
+ 2. These directives implement `AsyncValidator` and connect to the Vest suite
456
+ 3. User types into control
457
+ 4. The validate function gets called
458
+ 5. Vest returns the errors
459
+ 6. ngx-vest-forms puts those errors on the Angular form control
460
+
461
+ This means `valid`, `invalid`, `errors`, `statusChanges` all work just like a regular Angular form.
462
+
463
+ ### Displaying Validation Errors
464
+
465
+ Use the `sc-control-wrapper` component to show validation errors consistently:
466
+
467
+ ```html
468
+ <div ngModelGroup="generalInfo" sc-control-wrapper>
469
+ <div sc-control-wrapper>
470
+ <label>First name</label>
471
+ <input
472
+ type="text"
473
+ name="firstName"
474
+ [ngModel]="formValue().generalInfo?.firstName"
475
+ />
476
+ </div>
477
+
478
+ <div sc-control-wrapper>
479
+ <label>Last name</label>
480
+ <input
481
+ type="text"
482
+ name="lastName"
483
+ [ngModel]="formValue().generalInfo?.lastName"
484
+ />
485
+ </div>
486
+ </div>
487
+ ```
488
+
489
+ Errors show automatically:
490
+
491
+ - ✅ On blur
492
+ - ✅ On submit
493
+
494
+ You can use `sc-control-wrapper` on:
495
+
496
+ - Elements that hold `ngModelGroup`
497
+ - Elements that have an `ngModel` (or form control) inside of them
498
+
499
+ ### Performance Optimization with `only()`
500
+
501
+ ngx-vest-forms automatically optimizes validation performance by running validations only for the field being interacted with:
502
+
503
+ ```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
512
+
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
+ );
521
+ ```
522
+
523
+ This pattern ensures:
524
+
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)
528
+
529
+ > [!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.
531
+
532
+ ### Error Display Control
533
+
534
+ The `sc-control-wrapper` component uses the `FormErrorDisplayDirective` under the hood to manage when and how errors are displayed.
535
+
536
+ #### Error Display Modes
537
+
538
+ ngx-vest-forms supports three error display modes:
539
+
540
+ - **`on-blur-or-submit`** (default) - Show errors after field blur OR form submission
541
+ - **`on-blur`** - Show errors only after field blur
542
+ - **`on-submit`** - Show errors only after form submission
543
+
544
+ #### Configuring Error Display
545
+
546
+ **Global Configuration** - Set the default mode for your entire application:
547
+
548
+ ```typescript
549
+ import { ApplicationConfig } from '@angular/core';
550
+ import { SC_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
551
+
552
+ export const appConfig: ApplicationConfig = {
553
+ providers: [{ provide: SC_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-submit' }],
554
+ };
555
+
556
+ // Or in a component
557
+ @Component({
558
+ providers: [{ provide: SC_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-submit' }],
559
+ })
560
+ export class MyComponent {}
561
+ ```
562
+
563
+ **Per-Instance Configuration** - Override the mode for specific form fields:
564
+
565
+ ```typescript
566
+ @Component({
567
+ template: `
568
+ <div sc-control-wrapper [errorDisplayMode]="'on-blur'">
569
+ <input name="email" [ngModel]="formValue().email" />
570
+ </div>
571
+ `,
572
+ })
573
+ export class MyFormComponent {}
574
+ ```
575
+
576
+ > **Note**: The `sc-control-wrapper` component accepts `errorDisplayMode` as an input to override the global setting for specific fields.
577
+
578
+ ### Validation Options
579
+
580
+ You can configure additional `validationOptions` at various levels like `form`, `ngModelGroup` or `ngModel`.
581
+
582
+ For example, to debounce validation (useful for API calls):
583
+
584
+ ```html
585
+ <form scVestForm ... [validationOptions]="{ debounceTime: 0 }">
586
+ ...
587
+ <div sc-control-wrapper>
588
+ <label>UserId</label>
589
+ <input
590
+ type="text"
591
+ name="userId"
592
+ [ngModel]="formValue().userId"
593
+ [validationOptions]="{ debounceTime: 300 }"
594
+ />
595
+ </div>
596
+ ...
597
+ </form>
598
+ ```
599
+
600
+ ## Intermediate Topics
601
+
602
+ ### Conditional Fields
306
603
 
307
604
  What if we want to remove a form control or form group? With reactive forms that would require a lot of work
308
605
  but since Template driven forms do all the hard work for us, we can simply create a computed signal for that and
@@ -362,7 +659,7 @@ This also works for a form group:
362
659
  }
363
660
  ```
364
661
 
365
- ### Reactive disabling
662
+ ### Reactive Disabling
366
663
 
367
664
  To achieve reactive disabling, we just have to take advantage of computed signals as well:
368
665
 
@@ -374,16 +671,70 @@ class MyComponent {
374
671
  }
375
672
  ```
376
673
 
377
- We can bind the computed signal to the `disabled` directive of Angular.
674
+ We can bind the computed signal to the `disabled` directive of Angular.
675
+
676
+ ```html
677
+ <input
678
+ type="text"
679
+ name="lastName"
680
+ [disabled]="lastNameDisabled()"
681
+ [ngModel]="formValue().generalInformation?.lastName"
682
+ />
683
+ ```
684
+
685
+ ### Conditional Validations
686
+
687
+ Vest makes it extremely easy to create conditional validations.
688
+ Assume we have a form model that has `age` and `emergencyContact`.
689
+ The `emergencyContact` is required, but only when the person is not of legal age.
690
+
691
+ We can use the `omitWhen` so that when the person is below 18, the assertion
692
+ will not be done.
693
+
694
+ ```typescript
695
+ import { enforce, omitWhen, only, staticSuite, test } from 'vest';
696
+
697
+ ...
698
+ omitWhen((model.age || 0) >= 18, () => {
699
+ test('emergencyContact', 'Emergency contact is required', () => {
700
+ enforce(model.emergencyContact).isNotBlank();
701
+ });
702
+ });
703
+ ```
704
+
705
+ You can put those validations on every field that you want. On form group fields and on form control fields.
706
+ Check this interesting example below:
707
+
708
+ - [x] Password is always required
709
+ - [x] Confirm password is only required when there is a password
710
+ - [x] The passwords should match, but only when they are both filled in
711
+
712
+ ```typescript
713
+ test('passwords.password', 'Password is not filled in', () => {
714
+ enforce(model.passwords?.password).isNotBlank();
715
+ });
716
+ omitWhen(!model.passwords?.password, () => {
717
+ test('passwords.confirmPassword', 'Confirm password is not filled in', () => {
718
+ enforce(model.passwords?.confirmPassword).isNotBlank();
719
+ });
720
+ });
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
+ ```
732
+
733
+ Forget about manually adding, removing validators on reactive forms and not being able to
734
+ re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
735
+ **Oh, it's also pretty readable**
378
736
 
379
- ```html
380
- <input
381
- type="text"
382
- name="lastName"
383
- [disabled]="lastNameDisabled()"
384
- [ngModel]="formValue().generalInformation?.lastName"
385
- />
386
- ```
737
+ ## Advanced Topics
387
738
 
388
739
  ### Handling Form Structure Changes
389
740
 
@@ -410,6 +761,8 @@ When form structure changes dynamically in combination with _NON_ form elements
410
761
  #### The Solution
411
762
 
412
763
  ```typescript
764
+ import { Component, signal, viewChild } from '@angular/core';
765
+
413
766
  @Component({
414
767
  template: `
415
768
  <form
@@ -439,7 +792,8 @@ When form structure changes dynamically in combination with _NON_ form elements
439
792
  `,
440
793
  })
441
794
  export class MyFormComponent {
442
- @ViewChild('vestForm') vestForm!: FormDirective<MyFormModel>;
795
+ readonly vestForm =
796
+ viewChild.required<FormDirective<MyFormModel>>('vestForm');
443
797
 
444
798
  protected readonly formValue = signal<MyFormModel>({});
445
799
  protected readonly validationSuite = myValidationSuite;
@@ -567,7 +921,7 @@ onStructureChange(newValue: string) {
567
921
 
568
922
  **Additional Utility Functions:**
569
923
 
570
- ````typescript
924
+ ```typescript
571
925
  import { clearFields, keepFieldsWhen } from 'ngx-vest-forms';
572
926
 
573
927
  // Clear specific fields unconditionally
@@ -613,7 +967,7 @@ export const myValidationSuite = staticSuite(
613
967
  );
614
968
  ```
615
969
 
616
- ## Field State Utilities
970
+ ### Field State Utilities
617
971
 
618
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).
619
973
 
@@ -664,366 +1018,71 @@ const filteredState = keepFieldsWhen(currentFormValue, {
664
1018
  });
665
1019
  ```
666
1020
 
667
- ### Why These Utilities Are Needed
668
-
669
- The utilities are specifically needed when conditionally switching between **form inputs** and **non-form elements**:
670
-
671
- **The Core Problem:**
672
-
673
- 1. **Template structure:** `@if (condition) { <input> } @else { <p>Info text</p> }`
674
- 2. **Angular's behavior:** Automatically removes FormControls when switching to non-form content
675
- 3. **Your component signals:** Retain old field values from when input was present
676
- 4. **Result:** State inconsistency between `ngForm.form.value` (clean) and `formValue()` (stale)
677
-
678
- **The Solution:**
679
-
680
- These utilities synchronize your component state with Angular's form state, ensuring consistency when form structure changes involve non-form content.
681
-
682
- > **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.
683
-
684
- ## Examples
685
-
686
- ### Simple Form with Validation
687
-
688
- Here's a complete example showing form setup, validation, and error display:
689
-
690
- ```typescript
691
- import { Component, signal } from '@angular/core';
692
- import { staticSuite, test, enforce } from 'vest';
693
- import { vestForms, DeepPartial, DeepRequired } from 'ngx-vest-forms';
694
-
695
- // 1. Define your form model
696
- type UserFormModel = DeepPartial<{
697
- firstName: string;
698
- lastName: string;
699
- email: string;
700
- }>;
701
-
702
- // 2. Create a shape for runtime validation (recommended)
703
- const userFormShape: DeepRequired<UserFormModel> = {
704
- firstName: '',
705
- lastName: '',
706
- email: '',
707
- };
708
-
709
- // 3. Create a Vest validation suite
710
- const userValidationSuite = staticSuite(
711
- (model: UserFormModel, field?: string) => {
712
- if (field) {
713
- only(field); // Critical for performance - only validate the active field
714
- }
715
-
716
- test('firstName', 'First name is required', () => {
717
- enforce(model.firstName).isNotBlank();
718
- });
719
-
720
- test('lastName', 'Last name is required', () => {
721
- enforce(model.lastName).isNotBlank();
722
- });
723
-
724
- test('email', 'Valid email is required', () => {
725
- enforce(model.email).isEmail();
726
- });
727
- }
728
- );
729
-
730
- @Component({
731
- selector: 'app-user-form',
732
- imports: [vestForms],
733
- template: `
734
- <form
735
- scVestForm
736
- [suite]="suite"
737
- [formShape]="shape"
738
- (formValueChange)="formValue.set($event)"
739
- (ngSubmit)="onSubmit()"
740
- >
741
- <div sc-control-wrapper>
742
- <label>First Name</label>
743
- <input [ngModel]="formValue().firstName" name="firstName" />
744
- </div>
745
-
746
- <div sc-control-wrapper>
747
- <label>Last Name</label>
748
- <input [ngModel]="formValue().lastName" name="lastName" />
749
- </div>
750
-
751
- <div sc-control-wrapper>
752
- <label>Email</label>
753
- <input [ngModel]="formValue().email" name="email" type="email" />
754
- </div>
755
-
756
- <button type="submit">Submit</button>
757
- </form>
758
- `,
759
- })
760
- export class UserFormComponent {
761
- protected readonly formValue = signal<UserFormModel>({});
762
- protected readonly suite = userValidationSuite;
763
- protected readonly shape = userFormShape;
764
-
765
- protected onSubmit() {
766
- console.log('Form submitted:', this.formValue());
767
- }
768
- }
769
- ```
770
-
771
- ### Conditional Fields Example
772
-
773
- ```typescript
774
- @Component({
775
- template: `
776
- <form scVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
777
- <!-- Age field -->
778
- <div sc-control-wrapper>
779
- <label>Age</label>
780
- <input [ngModel]="formValue().age" name="age" type="number" />
781
- </div>
782
-
783
- <!-- Emergency contact - only required if under 18 -->
784
- @if (emergencyContactRequired()) {
785
- <div sc-control-wrapper>
786
- <label>Emergency Contact</label>
787
- <input
788
- [ngModel]="formValue().emergencyContact"
789
- name="emergencyContact"
790
- />
791
- </div>
792
- }
793
- </form>
794
- `,
795
- })
796
- export class ConditionalFormComponent {
797
- protected readonly formValue = signal<ConditionalFormModel>({});
798
-
799
- // Computed signal for conditional logic
800
- protected readonly emergencyContactRequired = computed(
801
- () => (this.formValue().age || 0) < 18
802
- );
803
- }
804
- ```
805
-
806
- ### Live Examples
807
-
808
- - **[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
809
- - **[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
810
-
811
- > **💡 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.
812
-
813
- ### Validations
814
-
815
- The absolute gem in ngx-vest-forms is the flexibility in validations without writing any boilerplate.
816
- The only dependency this lib has is [vest.js](https://vestjs.dev). An awesome lightweight validation framework.
817
- You can use it on the backend/frontend/Angular/react etc...
818
-
819
- We use vest because it introduces the concept of vest suites. These are suites that kind of look like unit-tests
820
- but that are highly flexible:
821
-
822
- - [x] Write validations on forms
823
- - [x] Write validations on form groups
824
- - [x] Write validations on form controls
825
- - [x] Composable/reuse-able different validation suites
826
- - [x] Write conditional validations
827
-
828
- ### Validation Performance with `only()`
829
-
830
- ngx-vest-forms automatically optimizes validation performance by running validations only for the field being interacted with. This is achieved through Vest's `only()` function:
831
-
832
- ```typescript
833
- import { enforce, only, staticSuite, test } from 'vest';
834
-
835
- export const myFormModelSuite = staticSuite(
836
- (model: MyFormModel, field?: string) => {
837
- if (field) {
838
- only(field); // Only validate the specific field during user interaction
839
- }
840
- // When field is undefined (e.g., on submit), all validations run
841
-
842
- test('firstName', 'First name is required', () => {
843
- enforce(model.firstName).isNotBlank();
844
- });
845
- test('lastName', 'Last name is required', () => {
846
- enforce(model.lastName).isNotBlank();
847
- });
848
- }
849
- );
850
- ```
851
-
852
- This pattern ensures:
853
-
854
- - ✅ During typing/blur: Only the current field validates (better performance)
855
- - ✅ On form submit: All fields validate (complete validation)
856
- - ✅ Untouched fields don't show errors prematurely (better UX)
857
-
858
- > [!IMPORTANT]
859
- > 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.
860
-
861
- ### Basic Validation Suite
862
-
863
- This is how you write a simple Vest suite:
864
-
865
- ```typescript
866
- import { enforce, only, staticSuite, test } from 'vest';
867
- import { MyFormModel } from '../models/my-form.model'
868
-
869
- export const myFormModelSuite = staticSuite(
870
- (model: MyformModel, field?: string) => {
871
- if (field) {
872
- // Needed to not run every validation every time
873
- only(field);
874
- }
875
- test('firstName', 'First name is required', () => {
876
- enforce(model.firstName).isNotBlank();
877
- });
878
- test('lastName', 'Last name is required', () => {
879
- enforce(model.lastName).isNotBlank();
880
- });
881
- }
882
- );
883
- };
884
- ```
885
-
886
- In the `test` function the first parameter is the field, the second is the validation error.
887
- The field is separated with the `.` syntax. So if we would have an `addresses` form group with an `billingAddress` form group inside
888
- and a form control `street` the field would be: `addresses.billingAddress.street`.
889
-
890
- This syntax should be self-explanatory and the entire enforcements guidelines can be found on [vest.js](https://vestjs.dev).
891
-
892
- Now let's connect this to our form. This is the biggest pain that ngx-vest-forms will fix for you: **Connecting Vest suites to Angular**
893
-
894
- ```typescript
895
- class MyComponent {
896
- protected readonly formValue = signal<MyFormModel>({});
897
- protected readonly suite = myFormModelSuite;
898
- }
899
- ```
900
-
901
- ```html
902
- <form
903
- scVestForm
904
- [formShape]="shape"
905
- [formValue]="formValue"
906
- [suite]="suite"
907
- (formValueChange)="formValue.set($event)"
908
- (ngSubmit)="onSubmit()"
909
- >
910
- ...
911
- </form>
912
- ```
913
-
914
- That's it. Validations are completely wired now. Because ngx-vest-forms will hook into the
915
- `[ngModel]` and `ngModelGroup` attributes, and create ngValidators automatically.
916
-
917
- It goes like this:
918
-
919
- - Control gets created, Angular recognizes the `ngModel` and `ngModelGroup` directives
920
- - These directives implement `AsyncValidator` and will connect to a vest suite
921
- - User types into control
922
- - The validate function gets called
923
- - Vest gets called for one field
924
- - Vest returns the errors
925
- - @simpilfied/forms puts those errors on the angular form control
926
-
927
- This means that `valid`, `invalid`, `errors`, `statusChanges` etc will keep on working
928
- just like it would with a regular angular form.
929
-
930
- #### Showing validation errors
931
-
932
- Now we want to show the validation errors in a consistent way.
933
- For that we have provided the `sc-control-wrapper` attribute component.
934
-
935
- You can use it on:
936
-
937
- - elements that hold `ngModelGroup`
938
- - elements that have an `ngModel` (or form control) inside of them.
939
-
940
- This will show errors automatically on:
941
-
942
- - form submit
943
- - blur
944
-
945
- **Note:** If those requirements don't fill your need, you can write a custom control-wrapper by copy-pasting the
946
- `control-wrapper` and adjusting the code.
947
-
948
- Let's update our form:
949
-
950
- ```html
951
-
952
- <div ngModelGroup="generalInfo" sc-control-wrapper>
953
- <div sc-control-wrapper>
954
- <label>First name</label
955
- <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
956
- </div>
957
-
958
- <div sc-control-wrapper>
959
- <label>Last name</label>
960
- <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
961
- </div>
962
- </div>
963
- ```
964
-
965
- This is the only thing we need to do to create a form that is completely wired with vest.
1021
+ ### Why These Utilities Are Needed
966
1022
 
967
- - [x] Automatic creation of form controls and form groups
968
- - [x] Automatic connection to vest suites
969
- - [x] Automatic typo validation
970
- - [x] Automatic adding of css error classes and showing validation messages
971
- - [x] On blur
972
- - [x] On submit
1023
+ The utilities are specifically needed when conditionally switching between **form inputs** and **non-form elements**:
973
1024
 
974
- ### Conditional validations
1025
+ **The Core Problem:**
975
1026
 
976
- Vest makes it extremely easy to create conditional validations.
977
- Assume we have a form model that has `age` and `emergencyContact`.
978
- The `emergencyContact` is required, but only when the person is not of legal age.
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)
979
1031
 
980
- We can use the `omitWhen` so that when the person is below 18, the assertion
981
- will not be done.
1032
+ **The Solution:**
982
1033
 
983
- ```typescript
984
- import { enforce, omitWhen, only, staticSuite, test } from 'vest';
1034
+ These utilities synchronize your component state with Angular's form state, ensuring consistency when form structure changes involve non-form content.
985
1035
 
986
- ...
987
- omitWhen((model.age || 0) >= 18, () => {
988
- test('emergencyContact', 'Emergency contact is required', () => {
989
- enforce(model.emergencyContact).isNotBlank();
990
- });
991
- });
992
- ```
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.
993
1037
 
994
- You can put those validations on every field that you want. On form group fields and on form control fields.
995
- Check this interesting example below:
1038
+ ### More Examples
996
1039
 
997
- - [x] Password is always required
998
- - [x] Confirm password is only required when there is a password
999
- - [x] The passwords should match, but only when they are both filled in
1040
+ #### Conditional Fields Example
1000
1041
 
1001
1042
  ```typescript
1002
- test('passwords.password', 'Password is not filled in', () => {
1003
- enforce(model.passwords?.password).isNotBlank();
1004
- });
1005
- omitWhen(!model.passwords?.password, () => {
1006
- test('passwords.confirmPassword', 'Confirm password is not filled in', () => {
1007
- enforce(model.passwords?.confirmPassword).isNotBlank();
1008
- });
1009
- });
1010
- omitWhen(
1011
- !model.passwords?.password || !model.passwords?.confirmPassword,
1012
- () => {
1013
- test('passwords', 'Passwords do not match', () => {
1014
- enforce(model.passwords?.confirmPassword).equals(
1015
- model.passwords?.password
1016
- );
1017
- });
1018
- }
1019
- );
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
+ }
1020
1076
  ```
1021
1077
 
1022
- Forget about manually adding, removing validators on reactive forms and not being able to
1023
- re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
1024
- **Oh, it's also pretty readable**
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.
1025
1084
 
1026
- ### Composable validations
1085
+ ### Composable Validations
1027
1086
 
1028
1087
  We can compose validations suites with sub suites. After all, we want to re-use certain pieces of our
1029
1088
  validation logic and we don't want one huge unreadable suite.
@@ -1079,7 +1138,7 @@ export const mySuite = staticSuite(
1079
1138
 
1080
1139
  We achieved decoupling, readability and reuse of our addressValidations.
1081
1140
 
1082
- #### A more complex example
1141
+ #### A More Complex Example
1083
1142
 
1084
1143
  Let's combine the conditional part with the reusable part.
1085
1144
  We have 2 addresses, but the shippingAddress is only required when the `shippingAddressIsDifferentFromBillingAddress`
@@ -1107,30 +1166,284 @@ omitWhen(!model.addresses?.shippingAddressDifferentFromBillingAddress, () => {
1107
1166
  });
1108
1167
  ```
1109
1168
 
1110
- ### Validation options
1169
+ ### Creating Custom Control Wrappers
1111
1170
 
1112
- The validation is triggered immediately when the input on the formModel changes.
1113
- In some cases you want to debounce the input (e.g. if you make an api call in the validation suite).
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.
1114
1172
 
1115
- You can configure additional `validationOptions` at various levels like `form`, `ngModelGroup` or `ngModel`.
1173
+ ##### Basic Custom Wrapper
1116
1174
 
1117
- ```html
1118
- <form scVestForm ... [validationOptions]="{ debounceTime: 0 }">
1119
- ...
1120
- <div sc-control-wrapper>
1121
- <label>UserId</label>
1122
- <input
1123
- type="text"
1124
- name="userId"
1125
- [ngModel]="formValue().userId"
1126
- [validationOptions]="{ debounceTime: 300 }"
1127
- />
1128
- </div>
1129
- ...
1130
- </form>
1175
+ ```typescript
1176
+ import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
1177
+ import { FormErrorDisplayDirective } from 'ngx-vest-forms';
1178
+
1179
+ @Component({
1180
+ selector: 'app-custom-control-wrapper',
1181
+ changeDetection: ChangeDetectionStrategy.OnPush,
1182
+ hostDirectives: [
1183
+ {
1184
+ directive: FormErrorDisplayDirective,
1185
+ inputs: ['errorDisplayMode'],
1186
+ },
1187
+ ],
1188
+ template: `
1189
+ <div class="field-wrapper">
1190
+ <ng-content />
1191
+
1192
+ @if (errorDisplay.shouldShowErrors()) {
1193
+ <div class="error-message">
1194
+ @for (error of errorDisplay.errors(); track error) {
1195
+ <span>{{ error.message || error }}</span>
1196
+ }
1197
+ </div>
1198
+ }
1199
+
1200
+ @if (errorDisplay.isPending()) {
1201
+ <div class="validating">Validating...</div>
1202
+ }
1203
+ </div>
1204
+ `,
1205
+ })
1206
+ export class CustomControlWrapperComponent {
1207
+ protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
1208
+ self: true,
1209
+ });
1210
+ }
1211
+ ```
1212
+
1213
+ ##### Advanced Custom Wrapper with Warnings
1214
+
1215
+ The `FormErrorDisplayDirective` also exposes warning messages from Vest.js:
1216
+
1217
+ ```typescript
1218
+ import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
1219
+ import { FormErrorDisplayDirective } from 'ngx-vest-forms';
1220
+
1221
+ @Component({
1222
+ selector: 'app-advanced-wrapper',
1223
+ changeDetection: ChangeDetectionStrategy.OnPush,
1224
+ hostDirectives: [FormErrorDisplayDirective],
1225
+ template: `
1226
+ <div class="form-field">
1227
+ <ng-content />
1228
+
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
+ }
1237
+
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
+ }
1246
+
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
+ ```
1263
+
1264
+ ##### Available Signals from FormErrorDisplayDirective
1265
+
1266
+ The directive exposes these computed signals for building custom UIs:
1267
+
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
+ ```
1287
+
1288
+ ##### Real-World Example: Material Design Style Wrapper
1289
+
1290
+ ```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
+ }
1345
+
1346
+ .mat-error {
1347
+ color: #f44336;
1348
+ font-size: 0.875rem;
1349
+ }
1350
+
1351
+ .mat-hint {
1352
+ color: rgba(0, 0, 0, 0.6);
1353
+ font-size: 0.875rem;
1354
+ }
1355
+
1356
+ .mat-warn {
1357
+ color: #ff9800;
1358
+ }
1359
+ `]
1360
+ })
1361
+ export class MatFieldWrapperComponent {
1362
+ protected readonly errorDisplay = inject(FormErrorDisplayDirective, {
1363
+ self: true,
1364
+ });
1365
+ }
1366
+ ```
1367
+
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
1395
+
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.
1399
+
1400
+ We can use the `omitWhen` so that when the person is below 18, the assertion
1401
+ will not be done.
1402
+
1403
+ ```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
1420
+
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
+ });
1429
+ });
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
+ );
1131
1440
  ```
1132
1441
 
1133
- ### Validations on the root form
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
1134
1447
 
1135
1448
  When we want to validate multiple fields that are depending on each other,
1136
1449
  it is a best practice to wrap them in a parent form group.
@@ -1378,6 +1691,7 @@ For that we have to use the `vestFormViewProviders` from `ngx-vest-forms`
1378
1691
 
1379
1692
  ```typescript
1380
1693
  ...
1694
+ import { input } from '@angular/core';
1381
1695
  import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
1382
1696
 
1383
1697
  @Component({
@@ -1385,10 +1699,47 @@ import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
1385
1699
  viewProviders: [vestFormsViewProviders]
1386
1700
  })
1387
1701
  export class AddressComponent {
1388
- @Input() address?: AddressModel;
1702
+ readonly address = input<AddressModel>();
1389
1703
  }
1390
1704
  ```
1391
1705
 
1706
+ ## Features
1707
+
1708
+ Now that you've seen how ngx-vest-forms works, here's a complete overview of its capabilities:
1709
+
1710
+ ### Core Features
1711
+
1712
+ - **Unidirectional Data Flow** - Predictable state management with Angular signals
1713
+ - **Type Safety** - Full TypeScript support with `DeepPartial<T>` and `DeepRequired<T>`
1714
+ - **Zero Boilerplate** - Automatic FormControl and FormGroup creation
1715
+ - **Shape Validation** - Runtime validation against your TypeScript models (dev mode)
1716
+
1717
+ ### Advanced Validation
1718
+
1719
+ - **Async Validations** - Built-in support with AbortController and pending state
1720
+ - **Conditional Logic** - Use `omitWhen()` for conditional validation rules
1721
+ - **Composable Suites** - Reusable validation functions across projects
1722
+ - **Custom Debouncing** - Configure validation timing per field or form
1723
+ - **Warnings Support** - Non-blocking feedback with Vest's `warn()` feature
1724
+ - **Performance Optimization** - Field-level validation with `only()` pattern
1725
+
1726
+ ### Dynamic Forms
1727
+
1728
+ - **Conditional Fields** - Show/hide fields based on form state
1729
+ - **Form Arrays** - Dynamic lists with add/remove functionality
1730
+ - **Reactive Disabling** - Disable fields based on computed signals
1731
+ - **State Management** - Preserve field state across conditional rendering
1732
+ - **Structure Change Detection** - Manual trigger for validation updates when form structure changes
1733
+
1734
+ ### Developer Experience
1735
+
1736
+ - **Runtime Shape Checking** - Catch typos in `name` attributes early
1737
+ - **Flexible Error Display** - Built-in `sc-control-wrapper` or create custom wrappers with `FormErrorDisplayDirective`
1738
+ - **Error Display Modes** - Control when errors show: on-blur, on-submit, or both
1739
+ - **Validation Config** - Declare field dependencies for complex scenarios
1740
+ - **Field State Utilities** - Helper functions for managing dynamic form state
1741
+ - **Modern Angular** - Built for Angular 18+ with standalone components and signals
1742
+
1392
1743
  ## Documentation
1393
1744
 
1394
1745
  ### Detailed Guides