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 +759 -408
- package/fesm2022/ngx-vest-forms.mjs +82 -86
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +2 -10
- package/package.json +1 -1
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) • [
|
|
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
|
-
##
|
|
144
|
+
## Complete Example
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Let's see a complete working form with validation to understand how everything fits together:
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
// 2. Create a shape for runtime validation (recommended)
|
|
161
|
+
const userFormShape: DeepRequired<UserFormModel> = {
|
|
162
|
+
firstName: '',
|
|
163
|
+
lastName: '',
|
|
164
|
+
email: '',
|
|
165
|
+
};
|
|
159
166
|
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
178
|
+
test('lastName', 'Last name is required', () => {
|
|
179
|
+
enforce(model.lastName).isNotBlank();
|
|
180
|
+
});
|
|
169
181
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
test('email', 'Valid email is required', () => {
|
|
183
|
+
enforce(model.email).isEmail();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
);
|
|
175
187
|
|
|
176
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1025
|
+
**The Core Problem:**
|
|
975
1026
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
981
|
-
will not be done.
|
|
1032
|
+
**The Solution:**
|
|
982
1033
|
|
|
983
|
-
|
|
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
|
-
|
|
995
|
-
Check this interesting example below:
|
|
1038
|
+
### More Examples
|
|
996
1039
|
|
|
997
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
**
|
|
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
|
|
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
|
|
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
|
-
###
|
|
1169
|
+
### Creating Custom Control Wrappers
|
|
1111
1170
|
|
|
1112
|
-
|
|
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
|
-
|
|
1173
|
+
##### Basic Custom Wrapper
|
|
1116
1174
|
|
|
1117
|
-
```
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|