ngx-vest-forms 1.0.3 → 1.2.0
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 +747 -155
- package/fesm2022/ngx-vest-forms.mjs +149 -84
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +261 -3
- package/package.json +8 -7
- package/esm2022/lib/components/control-wrapper/control-wrapper.component.mjs +0 -60
- package/esm2022/lib/constants.mjs +0 -2
- package/esm2022/lib/directives/form-model-group.directive.mjs +0 -42
- package/esm2022/lib/directives/form-model.directive.mjs +0 -42
- package/esm2022/lib/directives/form.directive.mjs +0 -176
- package/esm2022/lib/directives/validate-root-form.directive.mjs +0 -84
- package/esm2022/lib/exports.mjs +0 -70
- package/esm2022/lib/utils/array-to-object.mjs +0 -4
- package/esm2022/lib/utils/deep-partial.mjs +0 -2
- package/esm2022/lib/utils/deep-required.mjs +0 -2
- package/esm2022/lib/utils/form-utils.mjs +0 -180
- package/esm2022/lib/utils/shape-validation.mjs +0 -61
- package/esm2022/ngx-vest-forms.mjs +0 -5
- package/esm2022/public-api.mjs +0 -14
- package/lib/components/control-wrapper/control-wrapper.component.d.ts +0 -18
- package/lib/constants.d.ts +0 -1
- package/lib/directives/form-model-group.directive.d.ts +0 -13
- package/lib/directives/form-model.directive.d.ts +0 -13
- package/lib/directives/form.directive.d.ts +0 -88
- package/lib/directives/validate-root-form.directive.d.ts +0 -24
- package/lib/exports.d.ts +0 -17
- package/lib/utils/array-to-object.d.ts +0 -3
- package/lib/utils/deep-partial.d.ts +0 -8
- package/lib/utils/deep-required.d.ts +0 -7
- package/lib/utils/form-utils.d.ts +0 -36
- package/lib/utils/shape-validation.d.ts +0 -15
- package/public-api.d.ts +0 -12
package/README.md
CHANGED
|
@@ -1,34 +1,95 @@
|
|
|
1
|
+
<!-- prettier-ignore -->
|
|
2
|
+
<div align="center">
|
|
3
|
+
|
|
4
|
+
<img src="./course.jpeg" alt="ngx-vest-forms" align="center" height="96" />
|
|
5
|
+
|
|
1
6
|
# ngx-vest-forms
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
[](https://www.npmjs.com/package/ngx-vest-forms)
|
|
9
|
+
[](https://github.com/ngx-vest-forms/ngx-vest-forms/actions/workflows/cd.yml)
|
|
10
|
+
[](https://angular.dev)
|
|
11
|
+
[](https://www.typescriptlang.org)
|
|
12
|
+
[](LICENSE)
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
This package gives us the ability to create unidirectional forms without any boilerplate.
|
|
7
|
-
It is meant for complex forms with a high focus on complex validations and conditionals.
|
|
14
|
+
⭐ If you like this project, star it on GitHub — it helps a lot!
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
across different frameworks and technologies.
|
|
16
|
+
[Overview](#overview) • [Getting Started](#getting-started) • [Features](#features) • [Basic Usage](#basic-usage) • [Examples](#examples) • [Resources](#resources) • [Developer Resources](#developer-resources) • [Acknowledgments](#acknowledgments)
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> **New Maintainer**: I'm [the-ult](https://bsky.app/profile/the-ult.bsky.social), now maintaining this project as Brecht Billiet has moved on to other priorities. Huge thanks to Brecht for creating this amazing library and his foundational work on Angular forms!
|
|
22
|
+
|
|
23
|
+
A lightweight, type-safe adapter between Angular template-driven forms and [Vest.js](https://vestjs.dev) validation. Build complex forms with unidirectional data flow, sophisticated async validations, and zero boilerplate.
|
|
24
|
+
|
|
25
|
+
> [!TIP]
|
|
26
|
+
> **For Developers**: This project includes comprehensive instruction files for GitHub Copilot and detailed development guides. See [Developer Resources](#developer-resources) to copy these files to your workspace for enhanced development experience.
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
**ngx-vest-forms** transforms Angular template-driven forms into a powerful, type-safe solution for complex form scenarios. By combining Angular's simplicity with Vest.js's validation power, you get:
|
|
31
|
+
|
|
32
|
+
- **Unidirectional Data Flow** - Predictable state management with Angular signals
|
|
33
|
+
- **Type Safety** - Full TypeScript support with runtime shape validation
|
|
34
|
+
- **Async Validations** - Built-in support for complex, conditional validations
|
|
35
|
+
- **Zero Boilerplate** - Automatic form control creation and validation wiring
|
|
36
|
+
- **Conditional Logic** - Show/hide fields and validation rules dynamically
|
|
37
|
+
- **Reusable Validations** - Share validation suites across frameworks
|
|
13
38
|
|
|
14
|
-
|
|
39
|
+
### Why Choose ngx-vest-forms?
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
Traditional Angular reactive forms require extensive boilerplate for complex scenarios. Template-driven forms are simpler but lack type safety and advanced validation features. **ngx-vest-forms bridges this gap**, giving you the best of both worlds.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Before: Complex reactive form setup
|
|
45
|
+
const form = this.fb.group({
|
|
46
|
+
generalInfo: this.fb.group({
|
|
47
|
+
firstName: ['', [Validators.required]],
|
|
48
|
+
lastName: ['', [Validators.required]]
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// After: Simple, type-safe template-driven approach
|
|
53
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
54
|
+
protected readonly suite = myValidationSuite;
|
|
18
55
|
```
|
|
19
56
|
|
|
20
|
-
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
### Prerequisites
|
|
21
60
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
61
|
+
- **Angular**: >=18.0.0 (Signals support required)
|
|
62
|
+
- **Vest.js**: >=5.4.6 (Validation engine)
|
|
63
|
+
- **TypeScript**: >=5.8.0 (Modern Angular features)
|
|
64
|
+
- **Node.js**: >=22.0.0 (Required for Angular 18+)
|
|
65
|
+
|
|
66
|
+
### Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install ngx-vest-forms
|
|
70
|
+
```
|
|
26
71
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
### Quick Start
|
|
73
|
+
|
|
74
|
+
Create your first ngx-vest-forms component in 3 simple steps:
|
|
75
|
+
|
|
76
|
+
#### Step 1: Define your form model
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { signal } from '@angular/core';
|
|
80
|
+
import { vestForms, DeepPartial } from 'ngx-vest-forms';
|
|
81
|
+
|
|
82
|
+
type MyFormModel = DeepPartial<{
|
|
83
|
+
generalInfo: {
|
|
84
|
+
firstName: string;
|
|
85
|
+
lastName: string;
|
|
86
|
+
};
|
|
87
|
+
}>;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Step 2: Set up your component
|
|
91
|
+
|
|
92
|
+
Use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow:
|
|
32
93
|
|
|
33
94
|
```typescript
|
|
34
95
|
import { vestForms, DeepPartial } from 'ngx-vest-forms';
|
|
@@ -38,24 +99,34 @@ type MyFormModel = DeepPartial<{
|
|
|
38
99
|
generalInfo: {
|
|
39
100
|
firstName: string;
|
|
40
101
|
lastName: string;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
102
|
+
};
|
|
103
|
+
}>;
|
|
43
104
|
|
|
44
105
|
@Component({
|
|
45
106
|
imports: [vestForms],
|
|
46
107
|
template: `
|
|
47
|
-
<form
|
|
108
|
+
<form
|
|
109
|
+
scVestForm
|
|
48
110
|
(formValueChange)="formValue.set($event)"
|
|
49
|
-
(ngSubmit)="onSubmit()"
|
|
111
|
+
(ngSubmit)="onSubmit()"
|
|
112
|
+
>
|
|
50
113
|
<div ngModelGroup="generalInfo">
|
|
51
114
|
<label>First name</label>
|
|
52
|
-
<input
|
|
53
|
-
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
name="firstName"
|
|
118
|
+
[ngModel]="formValue().generalInfo?.firstName"
|
|
119
|
+
/>
|
|
120
|
+
|
|
54
121
|
<label>Last name</label>
|
|
55
|
-
<input
|
|
122
|
+
<input
|
|
123
|
+
type="text"
|
|
124
|
+
name="lastName"
|
|
125
|
+
[ngModel]="formValue().generalInfo?.lastName"
|
|
126
|
+
/>
|
|
56
127
|
</div>
|
|
57
|
-
</form>
|
|
58
|
-
|
|
128
|
+
</form>
|
|
129
|
+
`,
|
|
59
130
|
})
|
|
60
131
|
export class MyComponent {
|
|
61
132
|
// This signal will hold the state of our form
|
|
@@ -63,22 +134,59 @@ export class MyComponent {
|
|
|
63
134
|
}
|
|
64
135
|
```
|
|
65
136
|
|
|
66
|
-
|
|
137
|
+
#### Step 3: That's it! 🎉
|
|
138
|
+
|
|
139
|
+
Your form automatically creates FormGroups and FormControls with type-safe, unidirectional data flow.
|
|
140
|
+
|
|
141
|
+
> [!IMPORTANT]
|
|
142
|
+
> Notice we use `[ngModel]` (not `[(ngModel)]`) for unidirectional data flow, and the `?` operator since template-driven forms are `DeepPartial`.
|
|
67
143
|
|
|
68
|
-
|
|
69
|
-
|
|
144
|
+
## Features
|
|
145
|
+
|
|
146
|
+
### Core Features
|
|
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)
|
|
152
|
+
|
|
153
|
+
### Advanced Validation
|
|
154
|
+
|
|
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
|
|
159
|
+
|
|
160
|
+
### Dynamic Forms
|
|
161
|
+
|
|
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
|
+
|
|
167
|
+
### Developer Experience
|
|
168
|
+
|
|
169
|
+
- **Runtime Shape Checking** - Catch typos in `name` attributes early
|
|
170
|
+
- **Built-in Error Display** - `sc-control-wrapper` component for consistent UX
|
|
171
|
+
- **Validation Config** - Declare field dependencies for complex scenarios
|
|
172
|
+
- **Modern Angular** - Built for Angular 18+ with standalone components
|
|
173
|
+
|
|
174
|
+
## Basic Usage
|
|
175
|
+
|
|
176
|
+
The form value will be automatically populated like this:
|
|
70
177
|
|
|
71
178
|
```typescript
|
|
72
179
|
formValue = {
|
|
73
180
|
generalInfo: {
|
|
74
181
|
firstName: '',
|
|
75
|
-
lastName: ''
|
|
76
|
-
}
|
|
77
|
-
}
|
|
182
|
+
lastName: '',
|
|
183
|
+
},
|
|
184
|
+
};
|
|
78
185
|
```
|
|
79
186
|
|
|
80
187
|
The ngForm will contain automatically created FormGroups and FormControls.
|
|
81
188
|
This does not have anything to do with this package. It's just Angular:
|
|
189
|
+
|
|
82
190
|
```typescript
|
|
83
191
|
form = {
|
|
84
192
|
controls: {
|
|
@@ -92,14 +200,14 @@ form = {
|
|
|
92
200
|
}
|
|
93
201
|
```
|
|
94
202
|
|
|
95
|
-
The `scVestForm` directive offers
|
|
203
|
+
The `scVestForm` directive offers these outputs:
|
|
96
204
|
|
|
97
|
-
| Output
|
|
98
|
-
|
|
99
|
-
| formValueChange
|
|
100
|
-
| dirtyChange
|
|
101
|
-
| validChange
|
|
102
|
-
| errorsChange
|
|
205
|
+
| Output | Description |
|
|
206
|
+
| ----------------- | ----------------------------------------------------------------------------------------------- |
|
|
207
|
+
| `formValueChange` | Emits when the form value changes (debounced since template-driven forms are created over time) |
|
|
208
|
+
| `dirtyChange` | Emits when the dirty state of the form changes |
|
|
209
|
+
| `validChange` | Emits when the form becomes valid or invalid |
|
|
210
|
+
| `errorsChange` | Emits the complete list of errors for the form and all its controls |
|
|
103
211
|
|
|
104
212
|
### Avoiding typo's
|
|
105
213
|
|
|
@@ -114,33 +222,42 @@ type MyFormModel = DeepPartial<{
|
|
|
114
222
|
generalInfo: {
|
|
115
223
|
firstName: string;
|
|
116
224
|
lastName: string;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
225
|
+
};
|
|
226
|
+
}>;
|
|
119
227
|
|
|
120
228
|
export const myFormModelShape: DeepRequired<MyFormModel> = {
|
|
121
229
|
generalInfo: {
|
|
122
230
|
firstName: '',
|
|
123
|
-
lastName: ''
|
|
124
|
-
}
|
|
231
|
+
lastName: '',
|
|
232
|
+
},
|
|
125
233
|
};
|
|
126
234
|
|
|
127
235
|
@Component({
|
|
128
236
|
imports: [vestForms],
|
|
129
237
|
template: `
|
|
130
|
-
<form
|
|
238
|
+
<form
|
|
239
|
+
scVestForm
|
|
131
240
|
[formShape]="shape"
|
|
132
241
|
(formValueChange)="formValue.set($event)"
|
|
133
|
-
(ngSubmit)="onSubmit()"
|
|
134
|
-
|
|
242
|
+
(ngSubmit)="onSubmit()"
|
|
243
|
+
>
|
|
135
244
|
<div ngModelGroup="generalInfo">
|
|
136
245
|
<label>First name</label>
|
|
137
|
-
<input
|
|
138
|
-
|
|
246
|
+
<input
|
|
247
|
+
type="text"
|
|
248
|
+
name="firstName"
|
|
249
|
+
[ngModel]="formValue().generalInformation?.firstName"
|
|
250
|
+
/>
|
|
251
|
+
|
|
139
252
|
<label>Last name</label>
|
|
140
|
-
<input
|
|
253
|
+
<input
|
|
254
|
+
type="text"
|
|
255
|
+
name="lastName"
|
|
256
|
+
[ngModel]="formValue().generalInformation?.lastName"
|
|
257
|
+
/>
|
|
141
258
|
</div>
|
|
142
|
-
</form>
|
|
143
|
-
|
|
259
|
+
</form>
|
|
260
|
+
`,
|
|
144
261
|
})
|
|
145
262
|
export class MyComponent {
|
|
146
263
|
protected readonly formValue = signal<MyFormModel>({});
|
|
@@ -179,7 +296,7 @@ Error: Shape mismatch:
|
|
|
179
296
|
|
|
180
297
|
### Conditional fields
|
|
181
298
|
|
|
182
|
-
What if we want to remove a form control or form group? With reactive forms that would require a lot of work
|
|
299
|
+
What if we want to remove a form control or form group? With reactive forms that would require a lot of work
|
|
183
300
|
but since Template driven forms do all the hard work for us, we can simply create a computed signal for that and
|
|
184
301
|
bind that in the template. Having logic in the template is considered a bad practice, so we can do all
|
|
185
302
|
the calculations in our class.
|
|
@@ -189,11 +306,19 @@ Let's hide `lastName` if `firstName` is not filled in:
|
|
|
189
306
|
```html
|
|
190
307
|
<div ngModelGroup="generalInfo">
|
|
191
308
|
<label>First name</label>
|
|
192
|
-
<input
|
|
193
|
-
|
|
309
|
+
<input
|
|
310
|
+
type="text"
|
|
311
|
+
name="firstName"
|
|
312
|
+
[ngModel]="formValue().generalInformation?.firstName"
|
|
313
|
+
/>
|
|
314
|
+
|
|
194
315
|
@if(lastNameAvailable()){
|
|
195
|
-
|
|
196
|
-
|
|
316
|
+
<label>Last name</label>
|
|
317
|
+
<input
|
|
318
|
+
type="text"
|
|
319
|
+
name="lastName"
|
|
320
|
+
[ngModel]="formValue().generalInformation?.lastName"
|
|
321
|
+
/>
|
|
197
322
|
}
|
|
198
323
|
</div>
|
|
199
324
|
```
|
|
@@ -201,8 +326,8 @@ Let's hide `lastName` if `firstName` is not filled in:
|
|
|
201
326
|
```typescript
|
|
202
327
|
class MyComponent {
|
|
203
328
|
...
|
|
204
|
-
protected readonly lastNameAvailable =
|
|
205
|
-
computed(() => !!this.formValue().
|
|
329
|
+
protected readonly lastNameAvailable =
|
|
330
|
+
computed(() => !!this.formValue().generalInfo?.firstName);
|
|
206
331
|
}
|
|
207
332
|
```
|
|
208
333
|
|
|
@@ -211,13 +336,21 @@ This also works for a form group:
|
|
|
211
336
|
|
|
212
337
|
```html
|
|
213
338
|
@if(showGeneralInfo()){
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
339
|
+
<div ngModelGroup="generalInfo">
|
|
340
|
+
<label>First name</label>
|
|
341
|
+
<input
|
|
342
|
+
type="text"
|
|
343
|
+
name="firstName"
|
|
344
|
+
[ngModel]="formValue().generalInformation?.firstName"
|
|
345
|
+
/>
|
|
346
|
+
|
|
347
|
+
<label>Last name</label>
|
|
348
|
+
<input
|
|
349
|
+
type="text"
|
|
350
|
+
name="lastName"
|
|
351
|
+
[ngModel]="formValue().generalInformation?.lastName"
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
221
354
|
}
|
|
222
355
|
```
|
|
223
356
|
|
|
@@ -227,18 +360,150 @@ To achieve reactive disabling, we just have to take advantage of computed signal
|
|
|
227
360
|
|
|
228
361
|
```typescript
|
|
229
362
|
class MyComponent {
|
|
230
|
-
protected readonly lastNameDisabled =
|
|
231
|
-
|
|
363
|
+
protected readonly lastNameDisabled = computed(
|
|
364
|
+
() => !this.formValue().generalInfo?.firstName
|
|
365
|
+
);
|
|
232
366
|
}
|
|
233
367
|
```
|
|
234
368
|
|
|
235
369
|
We can bind the computed signal to the `disabled` directive of Angular.
|
|
370
|
+
|
|
236
371
|
```html
|
|
237
|
-
<input
|
|
238
|
-
|
|
239
|
-
|
|
372
|
+
<input
|
|
373
|
+
type="text"
|
|
374
|
+
name="lastName"
|
|
375
|
+
[disabled]="lastNameDisabled()"
|
|
376
|
+
[ngModel]="formValue().generalInformation?.lastName"
|
|
377
|
+
/>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Examples
|
|
381
|
+
|
|
382
|
+
### Simple Form with Validation
|
|
383
|
+
|
|
384
|
+
Here's a complete example showing form setup, validation, and error display:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { Component, signal } from '@angular/core';
|
|
388
|
+
import { staticSuite, test, enforce } from 'vest';
|
|
389
|
+
import { vestForms, DeepPartial, DeepRequired } from 'ngx-vest-forms';
|
|
390
|
+
|
|
391
|
+
// 1. Define your form model
|
|
392
|
+
type UserFormModel = DeepPartial<{
|
|
393
|
+
firstName: string;
|
|
394
|
+
lastName: string;
|
|
395
|
+
email: string;
|
|
396
|
+
}>;
|
|
397
|
+
|
|
398
|
+
// 2. Create a shape for runtime validation (recommended)
|
|
399
|
+
const userFormShape: DeepRequired<UserFormModel> = {
|
|
400
|
+
firstName: '',
|
|
401
|
+
lastName: '',
|
|
402
|
+
email: '',
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// 3. Create a Vest validation suite
|
|
406
|
+
const userValidationSuite = staticSuite(
|
|
407
|
+
(model: UserFormModel, field?: string) => {
|
|
408
|
+
if (field) {
|
|
409
|
+
only(field); // Critical for performance - only validate the active field
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
test('firstName', 'First name is required', () => {
|
|
413
|
+
enforce(model.firstName).isNotBlank();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('lastName', 'Last name is required', () => {
|
|
417
|
+
enforce(model.lastName).isNotBlank();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('email', 'Valid email is required', () => {
|
|
421
|
+
enforce(model.email).isEmail();
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
@Component({
|
|
427
|
+
selector: 'app-user-form',
|
|
428
|
+
imports: [vestForms],
|
|
429
|
+
template: `
|
|
430
|
+
<form
|
|
431
|
+
scVestForm
|
|
432
|
+
[suite]="suite"
|
|
433
|
+
[formShape]="shape"
|
|
434
|
+
(formValueChange)="formValue.set($event)"
|
|
435
|
+
(ngSubmit)="onSubmit()"
|
|
436
|
+
>
|
|
437
|
+
<div sc-control-wrapper>
|
|
438
|
+
<label>First Name</label>
|
|
439
|
+
<input [ngModel]="formValue().firstName" name="firstName" />
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div sc-control-wrapper>
|
|
443
|
+
<label>Last Name</label>
|
|
444
|
+
<input [ngModel]="formValue().lastName" name="lastName" />
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div sc-control-wrapper>
|
|
448
|
+
<label>Email</label>
|
|
449
|
+
<input [ngModel]="formValue().email" name="email" type="email" />
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<button type="submit">Submit</button>
|
|
453
|
+
</form>
|
|
454
|
+
`,
|
|
455
|
+
})
|
|
456
|
+
export class UserFormComponent {
|
|
457
|
+
protected readonly formValue = signal<UserFormModel>({});
|
|
458
|
+
protected readonly suite = userValidationSuite;
|
|
459
|
+
protected readonly shape = userFormShape;
|
|
460
|
+
|
|
461
|
+
protected onSubmit() {
|
|
462
|
+
console.log('Form submitted:', this.formValue());
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Conditional Fields Example
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
@Component({
|
|
471
|
+
template: `
|
|
472
|
+
<form scVestForm [suite]="suite" (formValueChange)="formValue.set($event)">
|
|
473
|
+
<!-- Age field -->
|
|
474
|
+
<div sc-control-wrapper>
|
|
475
|
+
<label>Age</label>
|
|
476
|
+
<input [ngModel]="formValue().age" name="age" type="number" />
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<!-- Emergency contact - only required if under 18 -->
|
|
480
|
+
@if (emergencyContactRequired()) {
|
|
481
|
+
<div sc-control-wrapper>
|
|
482
|
+
<label>Emergency Contact</label>
|
|
483
|
+
<input
|
|
484
|
+
[ngModel]="formValue().emergencyContact"
|
|
485
|
+
name="emergencyContact"
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
}
|
|
489
|
+
</form>
|
|
490
|
+
`,
|
|
491
|
+
})
|
|
492
|
+
export class ConditionalFormComponent {
|
|
493
|
+
protected readonly formValue = signal<ConditionalFormModel>({});
|
|
494
|
+
|
|
495
|
+
// Computed signal for conditional logic
|
|
496
|
+
protected readonly emergencyContactRequired = computed(
|
|
497
|
+
() => (this.formValue().age || 0) < 18
|
|
498
|
+
);
|
|
499
|
+
}
|
|
240
500
|
```
|
|
241
501
|
|
|
502
|
+
### Live Examples
|
|
503
|
+
|
|
504
|
+
- **[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
|
|
505
|
+
- **[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
|
|
506
|
+
|
|
242
507
|
### Validations
|
|
243
508
|
|
|
244
509
|
The absolute gem in ngx-vest-forms is the flexibility in validations without writing any boilerplate.
|
|
@@ -247,13 +512,50 @@ You can use it on the backend/frontend/Angular/react etc...
|
|
|
247
512
|
|
|
248
513
|
We use vest because it introduces the concept of vest suites. These are suites that kind of look like unit-tests
|
|
249
514
|
but that are highly flexible:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
515
|
+
|
|
516
|
+
- [x] Write validations on forms
|
|
517
|
+
- [x] Write validations on form groups
|
|
518
|
+
- [x] Write validations on form controls
|
|
519
|
+
- [x] Composable/reuse-able different validation suites
|
|
520
|
+
- [x] Write conditional validations
|
|
521
|
+
|
|
522
|
+
### Validation Performance with `only()`
|
|
523
|
+
|
|
524
|
+
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:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { enforce, only, staticSuite, test } from 'vest';
|
|
528
|
+
|
|
529
|
+
export const myFormModelSuite = staticSuite(
|
|
530
|
+
(model: MyFormModel, field?: string) => {
|
|
531
|
+
if (field) {
|
|
532
|
+
only(field); // Only validate the specific field during user interaction
|
|
533
|
+
}
|
|
534
|
+
// When field is undefined (e.g., on submit), all validations run
|
|
535
|
+
|
|
536
|
+
test('firstName', 'First name is required', () => {
|
|
537
|
+
enforce(model.firstName).isNotBlank();
|
|
538
|
+
});
|
|
539
|
+
test('lastName', 'Last name is required', () => {
|
|
540
|
+
enforce(model.lastName).isNotBlank();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
This pattern ensures:
|
|
547
|
+
|
|
548
|
+
- ✅ During typing/blur: Only the current field validates (better performance)
|
|
549
|
+
- ✅ On form submit: All fields validate (complete validation)
|
|
550
|
+
- ✅ Untouched fields don't show errors prematurely (better UX)
|
|
551
|
+
|
|
552
|
+
> [!IMPORTANT]
|
|
553
|
+
> 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.
|
|
554
|
+
|
|
555
|
+
### Basic Validation Suite
|
|
255
556
|
|
|
256
557
|
This is how you write a simple Vest suite:
|
|
558
|
+
|
|
257
559
|
```typescript
|
|
258
560
|
import { enforce, only, staticSuite, test } from 'vest';
|
|
259
561
|
import { MyFormModel } from '../models/my-form.model'
|
|
@@ -291,21 +593,23 @@ class MyComponent {
|
|
|
291
593
|
```
|
|
292
594
|
|
|
293
595
|
```html
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
596
|
+
<form
|
|
597
|
+
scVestForm
|
|
598
|
+
[formShape]="shape"
|
|
599
|
+
[formValue]="formValue"
|
|
600
|
+
[suite]="suite"
|
|
601
|
+
(formValueChange)="formValue.set($event)"
|
|
602
|
+
(ngSubmit)="onSubmit()"
|
|
603
|
+
>
|
|
301
604
|
...
|
|
302
605
|
</form>
|
|
303
606
|
```
|
|
304
607
|
|
|
305
|
-
That's it. Validations are completely wired now. Because ngx-vest-forms will hook into the
|
|
608
|
+
That's it. Validations are completely wired now. Because ngx-vest-forms will hook into the
|
|
306
609
|
`[ngModel]` and `ngModelGroup` attributes, and create ngValidators automatically.
|
|
307
610
|
|
|
308
|
-
It goes like this:
|
|
611
|
+
It goes like this:
|
|
612
|
+
|
|
309
613
|
- Control gets created, Angular recognizes the `ngModel` and `ngModelGroup` directives
|
|
310
614
|
- These directives implement `AsyncValidator` and will connect to a vest suite
|
|
311
615
|
- User types into control
|
|
@@ -314,7 +618,7 @@ It goes like this:
|
|
|
314
618
|
- Vest returns the errors
|
|
315
619
|
- @simpilfied/forms puts those errors on the angular form control
|
|
316
620
|
|
|
317
|
-
This means that `valid`, `invalid`, `errors`,
|
|
621
|
+
This means that `valid`, `invalid`, `errors`, `statusChanges` etc will keep on working
|
|
318
622
|
just like it would with a regular angular form.
|
|
319
623
|
|
|
320
624
|
#### Showing validation errors
|
|
@@ -323,10 +627,12 @@ Now we want to show the validation errors in a consistent way.
|
|
|
323
627
|
For that we have provided the `sc-control-wrapper` attribute component.
|
|
324
628
|
|
|
325
629
|
You can use it on:
|
|
630
|
+
|
|
326
631
|
- elements that hold `ngModelGroup`
|
|
327
632
|
- elements that have an `ngModel` (or form control) inside of them.
|
|
328
633
|
|
|
329
634
|
This will show errors automatically on:
|
|
635
|
+
|
|
330
636
|
- form submit
|
|
331
637
|
- blur
|
|
332
638
|
|
|
@@ -351,12 +657,13 @@ Let's update our form:
|
|
|
351
657
|
```
|
|
352
658
|
|
|
353
659
|
This is the only thing we need to do to create a form that is completely wired with vest.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
660
|
+
|
|
661
|
+
- [x] Automatic creation of form controls and form groups
|
|
662
|
+
- [x] Automatic connection to vest suites
|
|
663
|
+
- [x] Automatic typo validation
|
|
664
|
+
- [x] Automatic adding of css error classes and showing validation messages
|
|
665
|
+
- [x] On blur
|
|
666
|
+
- [x] On submit
|
|
360
667
|
|
|
361
668
|
### Conditional validations
|
|
362
669
|
|
|
@@ -381,9 +688,9 @@ omitWhen((model.age || 0) >= 18, () => {
|
|
|
381
688
|
You can put those validations on every field that you want. On form group fields and on form control fields.
|
|
382
689
|
Check this interesting example below:
|
|
383
690
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
691
|
+
- [x] Password is always required
|
|
692
|
+
- [x] Confirm password is only required when there is a password
|
|
693
|
+
- [x] The passwords should match, but only when they are both filled in
|
|
387
694
|
|
|
388
695
|
```typescript
|
|
389
696
|
test('passwords.password', 'Password is not filled in', () => {
|
|
@@ -394,11 +701,16 @@ omitWhen(!model.passwords?.password, () => {
|
|
|
394
701
|
enforce(model.passwords?.confirmPassword).isNotBlank();
|
|
395
702
|
});
|
|
396
703
|
});
|
|
397
|
-
omitWhen(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
704
|
+
omitWhen(
|
|
705
|
+
!model.passwords?.password || !model.passwords?.confirmPassword,
|
|
706
|
+
() => {
|
|
707
|
+
test('passwords', 'Passwords do not match', () => {
|
|
708
|
+
enforce(model.passwords?.confirmPassword).equals(
|
|
709
|
+
model.passwords?.password
|
|
710
|
+
);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
);
|
|
402
714
|
```
|
|
403
715
|
|
|
404
716
|
Forget about manually adding, removing validators on reactive forms and not being able to
|
|
@@ -414,7 +726,10 @@ This is quite straightforward with Vest.
|
|
|
414
726
|
Let's take this simple function that validates an address:
|
|
415
727
|
|
|
416
728
|
```typescript
|
|
417
|
-
export function addressValidations(
|
|
729
|
+
export function addressValidations(
|
|
730
|
+
model: AddressModel | undefined,
|
|
731
|
+
field: string
|
|
732
|
+
): void {
|
|
418
733
|
test(`${field}.street`, 'Street is required', () => {
|
|
419
734
|
enforce(model?.street).isNotBlank();
|
|
420
735
|
});
|
|
@@ -444,8 +759,14 @@ export const mySuite = staticSuite(
|
|
|
444
759
|
if (field) {
|
|
445
760
|
only(field);
|
|
446
761
|
}
|
|
447
|
-
addressValidations(
|
|
448
|
-
|
|
762
|
+
addressValidations(
|
|
763
|
+
model.addresses?.billingAddress,
|
|
764
|
+
'addresses.billingAddress'
|
|
765
|
+
);
|
|
766
|
+
addressValidations(
|
|
767
|
+
model.addresses?.shippingAddress,
|
|
768
|
+
'addresses.shippingAddress'
|
|
769
|
+
);
|
|
449
770
|
}
|
|
450
771
|
);
|
|
451
772
|
```
|
|
@@ -460,36 +781,54 @@ Checkbox is checked. But if it is checked, all fields are required.
|
|
|
460
781
|
And if both addresses are filled in, they should be different.
|
|
461
782
|
|
|
462
783
|
This gives us validation on:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
784
|
+
|
|
785
|
+
- [x] The addresses form field (they can't be equal)
|
|
786
|
+
- [x] The shipping Address field (only required when checkbox is checked)
|
|
787
|
+
- [x] validation on all the address fields (street, number, etc) on both addresses
|
|
466
788
|
|
|
467
789
|
```typescript
|
|
468
|
-
addressValidations(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
() => {
|
|
475
|
-
|
|
476
|
-
model.addresses?.shippingAddress
|
|
477
|
-
'addresses.shippingAddress'
|
|
790
|
+
addressValidations(model.addresses?.billingAddress, 'addresses.billingAddress');
|
|
791
|
+
omitWhen(!model.addresses?.shippingAddressDifferentFromBillingAddress, () => {
|
|
792
|
+
addressValidations(
|
|
793
|
+
model.addresses?.shippingAddress,
|
|
794
|
+
'addresses.shippingAddress'
|
|
795
|
+
);
|
|
796
|
+
test('addresses', 'The addresses appear to be the same', () => {
|
|
797
|
+
enforce(JSON.stringify(model.addresses?.billingAddress)).notEquals(
|
|
798
|
+
JSON.stringify(model.addresses?.shippingAddress)
|
|
478
799
|
);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Validation options
|
|
805
|
+
|
|
806
|
+
The validation is triggered immediately when the input on the formModel changes.
|
|
807
|
+
In some cases you want to debounce the input (e.g. if you make an api call in the validation suite).
|
|
808
|
+
|
|
809
|
+
You can configure additional `validationOptions` at various levels like `form`, `ngModelGroup` or `ngModel`.
|
|
810
|
+
|
|
811
|
+
```html
|
|
812
|
+
<form scVestForm ... [validationOptions]="{ debounceTime: 0 }">
|
|
813
|
+
...
|
|
814
|
+
<div sc-control-wrapper>
|
|
815
|
+
<label>UserId</label>
|
|
816
|
+
<input
|
|
817
|
+
type="text"
|
|
818
|
+
name="userId"
|
|
819
|
+
[ngModel]="formValue().userId"
|
|
820
|
+
[validationOptions]="{ debounceTime: 300 }"
|
|
821
|
+
/>
|
|
822
|
+
</div>
|
|
823
|
+
...
|
|
824
|
+
</form>
|
|
486
825
|
```
|
|
487
826
|
|
|
488
827
|
### Validations on the root form
|
|
489
828
|
|
|
490
829
|
When we want to validate multiple fields that are depending on each other,
|
|
491
830
|
it is a best practice to wrap them in a parent form group.
|
|
492
|
-
If `password`
|
|
831
|
+
If `password` and `confirmPassword` have to be equal the validation should not happen on
|
|
493
832
|
`password` nor on `confirmPassword`, it should happen on `passwords`:
|
|
494
833
|
|
|
495
834
|
```typescript
|
|
@@ -497,8 +836,8 @@ const form = {
|
|
|
497
836
|
// validation happens here
|
|
498
837
|
passwords: {
|
|
499
838
|
password: '',
|
|
500
|
-
confirmPassword: ''
|
|
501
|
-
}
|
|
839
|
+
confirmPassword: '',
|
|
840
|
+
},
|
|
502
841
|
};
|
|
503
842
|
```
|
|
504
843
|
|
|
@@ -508,16 +847,19 @@ Use the `errorsChange` output to keep the errors as state in a signal that we ca
|
|
|
508
847
|
wherever we want.
|
|
509
848
|
|
|
510
849
|
```html
|
|
511
|
-
{{ errors()?.['rootForm'] }}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
850
|
+
{{ errors()?.['rootForm'] }}
|
|
851
|
+
<!-- render the errors on the rootForm -->
|
|
852
|
+
{{ errors() }}
|
|
853
|
+
<!-- render all the errors -->
|
|
854
|
+
<form
|
|
855
|
+
scVestForm
|
|
856
|
+
[formValue]="formValue()"
|
|
857
|
+
[validateRootForm]="true"
|
|
858
|
+
[formShape]="shape"
|
|
859
|
+
[suite]="suite"
|
|
860
|
+
(errorsChange)="errors.set($event)"
|
|
861
|
+
...
|
|
862
|
+
></form>
|
|
521
863
|
```
|
|
522
864
|
|
|
523
865
|
```typescript
|
|
@@ -525,7 +867,7 @@ export class MyformComponent {
|
|
|
525
867
|
protected readonly formValue = signal<MyFormModel>({});
|
|
526
868
|
protected readonly suite = myFormModelSuite;
|
|
527
869
|
// Keep the errors in state
|
|
528
|
-
protected readonly errors = signal<Record<string, string>>({
|
|
870
|
+
protected readonly errors = signal<Record<string, string>>({});
|
|
529
871
|
}
|
|
530
872
|
```
|
|
531
873
|
|
|
@@ -539,29 +881,187 @@ import { ROOT_FORM } from 'ngx-vest-forms';
|
|
|
539
881
|
|
|
540
882
|
test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
|
|
541
883
|
enforce(
|
|
542
|
-
model.firstName === 'Brecht' &&
|
|
543
|
-
|
|
544
|
-
|
|
884
|
+
model.firstName === 'Brecht' &&
|
|
885
|
+
model.lastName === 'Billiet' &&
|
|
886
|
+
model.age === 30
|
|
887
|
+
).isFalsy();
|
|
545
888
|
});
|
|
546
889
|
```
|
|
547
890
|
|
|
548
|
-
### Validation
|
|
891
|
+
### Validation of dependant controls and or groups
|
|
549
892
|
|
|
550
|
-
Sometimes
|
|
551
|
-
|
|
552
|
-
|
|
893
|
+
Sometimes, form validations are dependent on the values of other form controls or groups.
|
|
894
|
+
This scenario is common when a field's validity relies on the input of another field.
|
|
895
|
+
A typical example is the `confirmPassword` field, which should only be validated if the `password` field is filled in.
|
|
896
|
+
When the `password` field value changes, it necessitates re-validating the `confirmPassword` field to ensure
|
|
897
|
+
consistency.
|
|
553
898
|
|
|
899
|
+
#### Understanding the Architecture: Why `validationConfig` Is Needed
|
|
554
900
|
|
|
555
|
-
|
|
901
|
+
Before diving into the implementation, it's important to understand the architectural boundaries between Vest.js and Angular:
|
|
556
902
|
|
|
557
|
-
|
|
903
|
+
**What Vest.js Handles:**
|
|
558
904
|
|
|
559
|
-
|
|
905
|
+
- ✅ Validation logic and rules
|
|
906
|
+
- ✅ Conditional validation with `omitWhen()`, `skipWhen()`
|
|
907
|
+
- ✅ Field-level optimization with `only()`
|
|
908
|
+
- ✅ Async validations with AbortController
|
|
909
|
+
- ✅ Cross-field validation logic (e.g., "passwords must match")
|
|
560
910
|
|
|
561
|
-
|
|
911
|
+
**What Vest.js Cannot Do:**
|
|
912
|
+
|
|
913
|
+
- ❌ Trigger Angular to revalidate a different form control
|
|
914
|
+
- ❌ Control Angular's form control lifecycle
|
|
915
|
+
- ❌ Tell Angular "when field X changes, also validate field Y"
|
|
916
|
+
|
|
917
|
+
**Angular's Limitation:**
|
|
918
|
+
Angular template-driven forms do not natively know about cross-field dependencies. When a field changes, only its own validators run automatically.
|
|
919
|
+
|
|
920
|
+
**How `validationConfig` Bridges This Gap:**
|
|
921
|
+
|
|
922
|
+
The `validationConfig` tells Angular's form system: "when field X changes, also call `updateValueAndValidity()` on field Y". This ensures that:
|
|
923
|
+
|
|
924
|
+
- Cross-field validations run at the right time
|
|
925
|
+
- UI error states update correctly
|
|
926
|
+
- Form validation state remains consistent
|
|
927
|
+
|
|
928
|
+
**Example of the Problem:**
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
// In your Vest suite
|
|
932
|
+
test('confirmPassword', 'Passwords must match', () => {
|
|
933
|
+
enforce(model.confirmPassword).equals(model.password);
|
|
934
|
+
});
|
|
935
|
+
```
|
|
562
936
|
|
|
563
|
-
|
|
937
|
+
Without `validationConfig`: If user changes `password`, the `confirmPassword` field won't be revalidated automatically, even though its validity depends on the password value.
|
|
564
938
|
|
|
939
|
+
With `validationConfig`: Angular knows to revalidate `confirmPassword` whenever `password` changes.
|
|
940
|
+
|
|
941
|
+
**Architectural Benefits of This Separation:**
|
|
942
|
+
|
|
943
|
+
This separation of concerns provides several advantages:
|
|
944
|
+
|
|
945
|
+
- **Clarity**: Vest.js focuses on validation logic, `validationConfig` handles Angular orchestration
|
|
946
|
+
- **Reusability**: Vest suites work across frameworks, while `validationConfig` is Angular-specific
|
|
947
|
+
- **Maintainability**: Changes to validation logic don't affect dependency management
|
|
948
|
+
- **Performance**: Only necessary validations run, only necessary controls revalidate
|
|
949
|
+
- **Testability**: Validation logic can be tested independently from Angular form behavior
|
|
950
|
+
|
|
951
|
+
Here's how you can handle validation dependencies with ngx-vest-forms and vest.js:
|
|
952
|
+
|
|
953
|
+
Use Vest to create a suite where you define the conditional validations.
|
|
954
|
+
For example, the `confirmPassword` field should only be validated when the `password` field is not empty.
|
|
955
|
+
Additionally, you need to ensure that both fields match.
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
import { enforce, omitWhen, staticSuite, test } from 'vest';
|
|
959
|
+
import { MyFormModel } from '../models/my-form.model';
|
|
960
|
+
|
|
961
|
+
import { enforce, omitWhen, only, staticSuite, test } from 'vest';
|
|
962
|
+
|
|
963
|
+
test('password', 'Password is required', () => {
|
|
964
|
+
enforce(model.password).isNotBlank();
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
omitWhen(!model.password, () => {
|
|
968
|
+
test('confirmPassword', 'Confirm password is required', () => {
|
|
969
|
+
enforce(model.confirmPassword).isNotBlank();
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
omitWhen(!model.password || !model.confirmPassword, () => {
|
|
974
|
+
test('passwords', 'Passwords do not match', () => {
|
|
975
|
+
enforce(model.confirmPassword).equals(model.password);
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
Creating a validation config.
|
|
983
|
+
The `scVestForm` has an input called `validationConfig`, that we can use to let the system know when to retrigger validations.
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
protected validationConfig = {
|
|
987
|
+
password: ['passwords.confirmPassword']
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
Here we see that when password changes, it needs to update the field `passwords.confirmPassword`.
|
|
992
|
+
This validationConfig is completely dynamic, and can also be used for form arrays.
|
|
993
|
+
|
|
994
|
+
```html
|
|
995
|
+
<form scVestForm ... [validationConfig]="validationConfig">
|
|
996
|
+
<div ngModelGroup="passwords">
|
|
997
|
+
<label>Password</label>
|
|
998
|
+
<input
|
|
999
|
+
type="password"
|
|
1000
|
+
name="password"
|
|
1001
|
+
[ngModel]="formValue().passwords?.password"
|
|
1002
|
+
/>
|
|
1003
|
+
|
|
1004
|
+
<label>Confirm Password</label>
|
|
1005
|
+
<input
|
|
1006
|
+
type="password"
|
|
1007
|
+
name="confirmPassword"
|
|
1008
|
+
[ngModel]="formValue().passwords?.confirmPassword"
|
|
1009
|
+
/>
|
|
1010
|
+
</div>
|
|
1011
|
+
</form>
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
#### Advanced State Management Patterns
|
|
1015
|
+
|
|
1016
|
+
The `validationConfig` works seamlessly with different state management approaches. By default, most examples show using a single signal for both input and output:
|
|
1017
|
+
|
|
1018
|
+
```typescript
|
|
1019
|
+
// Standard pattern: single signal for both input and output
|
|
1020
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
1021
|
+
|
|
1022
|
+
// Template
|
|
1023
|
+
<form scVestForm
|
|
1024
|
+
[formValue]="formValue()"
|
|
1025
|
+
(formValueChange)="formValue.set($event)"
|
|
1026
|
+
[validationConfig]="validationConfig">
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
However, you can also use **separate signals** for input and output if your application architecture requires it:
|
|
1030
|
+
|
|
1031
|
+
```typescript
|
|
1032
|
+
// Advanced pattern: separate input and output signals
|
|
1033
|
+
protected readonly inputFormValue = signal<MyFormModel>({});
|
|
1034
|
+
protected readonly outputFormValue = signal<MyFormModel>({});
|
|
1035
|
+
|
|
1036
|
+
// Template
|
|
1037
|
+
<form scVestForm
|
|
1038
|
+
[formValue]="inputFormValue()"
|
|
1039
|
+
(formValueChange)="handleFormChange($event)"
|
|
1040
|
+
[validationConfig]="validationConfig">
|
|
1041
|
+
<input name="password" [ngModel]="outputFormValue().password" />
|
|
1042
|
+
<input name="confirmPassword" [ngModel]="outputFormValue().confirmPassword" />
|
|
1043
|
+
</form>
|
|
1044
|
+
|
|
1045
|
+
// Component
|
|
1046
|
+
handleFormChange(value: MyFormModel) {
|
|
1047
|
+
// Update output signal independently
|
|
1048
|
+
this.outputFormValue.set(value);
|
|
1049
|
+
// Optionally sync input signal or perform other logic
|
|
1050
|
+
}
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
**Why this works**: The validation configuration operates at the **form control level**, listening directly to Angular's form control changes rather than component signals. This makes it independent of your chosen state management pattern.
|
|
1054
|
+
|
|
1055
|
+
This pattern is useful when:
|
|
1056
|
+
|
|
1057
|
+
- You need different processing logic for form inputs vs outputs
|
|
1058
|
+
- You're integrating with state management libraries
|
|
1059
|
+
- You want to maintain separate concerns between form display and form handling
|
|
1060
|
+
|
|
1061
|
+
#### Form array validations
|
|
1062
|
+
|
|
1063
|
+
An example can be found [in this simplified courses article](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)
|
|
1064
|
+
There is also a complex example of form arrays with complex validations in the examples.
|
|
565
1065
|
|
|
566
1066
|
### Child form components
|
|
567
1067
|
|
|
@@ -583,9 +1083,101 @@ export class AddressComponent {
|
|
|
583
1083
|
}
|
|
584
1084
|
```
|
|
585
1085
|
|
|
586
|
-
|
|
1086
|
+
## Resources
|
|
1087
|
+
|
|
1088
|
+
### Documentation & Tutorials
|
|
1089
|
+
|
|
1090
|
+
- **[Angular Official Documentation](https://angular.dev/guide/forms)** - Template-driven forms guide
|
|
1091
|
+
- **[Vest.js Documentation](https://vestjs.dev)** - Validation framework used by ngx-vest-forms
|
|
1092
|
+
- **[Live Examples Repository](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples)** - Complex form examples and patterns
|
|
1093
|
+
- **[Interactive Stackblitz Demo](https://stackblitz.com/~/github.com/simplifiedcourses/ngx-vest-forms-stackblitz)** - Try it in your browser
|
|
1094
|
+
|
|
1095
|
+
### Running Examples Locally
|
|
1096
|
+
|
|
1097
|
+
Clone this repo and run the examples:
|
|
1098
|
+
|
|
1099
|
+
```bash
|
|
1100
|
+
npm install
|
|
1101
|
+
npm start
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Learning Resources
|
|
1105
|
+
|
|
1106
|
+
[](https://www.simplified.courses/complex-angular-template-driven-forms)
|
|
1107
|
+
|
|
1108
|
+
**[Complex Angular Template-Driven Forms Course](https://www.simplified.courses/complex-angular-template-driven-forms)** - Master advanced form patterns and become a form expert.
|
|
1109
|
+
|
|
1110
|
+
### Founding Articles by Brecht Billiet
|
|
1111
|
+
|
|
1112
|
+
This library was originally created by [Brecht Billiet](https://twitter.com/brechtbilliet). Here are his foundational blog posts that inspired and guided the development:
|
|
1113
|
+
|
|
1114
|
+
- **[Introducing ngx-vest-forms](https://blog.simplified.courses/introducing-ngx-vest-forms/)** - The original introduction and motivation
|
|
1115
|
+
- **[Making Angular Template-Driven Forms Type-Safe](https://blog.simplified.courses/making-angular-template-driven-forms-typesafe/)** - Deep dive into type safety
|
|
1116
|
+
- **[Asynchronous Form Validators in Angular with Vest](https://blog.simplified.courses/asynchronous-form-validators-in-angular-with-vest/)** - Advanced async validation patterns
|
|
1117
|
+
- **[Template-Driven Forms with Form Arrays](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)** - Dynamic form arrays implementation
|
|
1118
|
+
|
|
1119
|
+
### Community & Support
|
|
1120
|
+
|
|
1121
|
+
- **[GitHub Issues](https://github.com/ngx-vest-forms/ngx-vest-forms/issues)** - Report bugs or request features
|
|
1122
|
+
- **[GitHub Discussions](https://github.com/ngx-vest-forms/ngx-vest-forms/discussions)** - Ask questions and share ideas
|
|
1123
|
+
- **[npm Package](https://www.npmjs.com/package/ngx-vest-forms)** - Official package page
|
|
1124
|
+
|
|
1125
|
+
## Developer Resources
|
|
1126
|
+
|
|
1127
|
+
### Comprehensive Instruction Files
|
|
1128
|
+
|
|
1129
|
+
This project includes detailed instruction files designed to help developers master ngx-vest-forms and Vest.js patterns:
|
|
1130
|
+
|
|
1131
|
+
- **[`.github/instructions/ngx-vest-forms.instructions.md`](.github/instructions/ngx-vest-forms.instructions.md)** - Complete guide for using ngx-vest-forms library
|
|
1132
|
+
- **[`.github/instructions/vest.instructions.md`](.github/instructions/vest.instructions.md)** - Comprehensive Vest.js validation patterns and best practices
|
|
1133
|
+
- **[`.github/copilot-instructions.md`](.github/copilot-instructions.md)** - Main GitHub Copilot instructions for this workspace
|
|
1134
|
+
|
|
1135
|
+
### Using Instruction Files in Your Workspace
|
|
1136
|
+
|
|
1137
|
+
For the best development experience with ngx-vest-forms, **copy these instruction files to your own project's `.github/` directory**:
|
|
1138
|
+
|
|
1139
|
+
```bash
|
|
1140
|
+
# Create the directories in your project
|
|
1141
|
+
mkdir -p .github/instructions
|
|
1142
|
+
|
|
1143
|
+
# Copy the instruction files
|
|
1144
|
+
curl -o .github/instructions/ngx-vest-forms.instructions.md \
|
|
1145
|
+
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/instructions/ngx-vest-forms.instructions.md
|
|
1146
|
+
|
|
1147
|
+
curl -o .github/instructions/vest.instructions.md \
|
|
1148
|
+
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/instructions/vest.instructions.md
|
|
1149
|
+
|
|
1150
|
+
# Optionally, adapt the main copilot instructions for your project
|
|
1151
|
+
curl -o .github/copilot-instructions.md \
|
|
1152
|
+
https://raw.githubusercontent.com/ngx-vest-forms/ngx-vest-forms/main/.github/copilot-instructions.md
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
**Benefits of copying instruction files:**
|
|
1156
|
+
|
|
1157
|
+
- **GitHub Copilot Integration** - Enhanced code generation aligned with best practices
|
|
1158
|
+
- **Comprehensive Documentation** - Complete patterns and examples at your fingertips
|
|
1159
|
+
- **Consistent Code Quality** - Maintain validation patterns and architectural standards
|
|
1160
|
+
- **Faster Development** - Quick reference for complex scenarios and optimizations
|
|
1161
|
+
|
|
1162
|
+
## Acknowledgments
|
|
1163
|
+
|
|
1164
|
+
🙏 **Special thanks to [Brecht Billiet](https://twitter.com/brechtbilliet)** for creating the original version of this library and his pioneering work on Angular forms. His vision and expertise laid the foundation for what ngx-vest-forms has become today.
|
|
1165
|
+
|
|
1166
|
+
### Core Contributors & Inspirations
|
|
1167
|
+
|
|
1168
|
+
**[Evyatar Alush](https://twitter.com/evyataral)** - Creator of [Vest.js](https://vestjs.dev/)
|
|
1169
|
+
|
|
1170
|
+
- 🎯 **The validation engine** that powers ngx-vest-forms
|
|
1171
|
+
- 🎙️ **Featured on PodRocket**: [Vest with Evyatar Alush](https://dev.to/podrocket/vest-with-evyatar-alush) - Deep dive into the philosophy and architecture of Vest.js
|
|
1172
|
+
|
|
1173
|
+
**[Ward Bell](https://twitter.com/wardbell)** - Template-Driven Forms Advocate
|
|
1174
|
+
|
|
1175
|
+
- 📢 **Evangelized Template-Driven Forms**: [Prefer Template-Driven Forms](https://devconf.net/talk/prefer-template-driven-forms-ward-bell-ng-conf-2021) (ng-conf 2021)
|
|
1176
|
+
- 🎥 **Original Vest.js + Angular Integration**: [Form validation done right](https://www.youtube.com/watch?v=EMUAtQlh9Ko) - The foundational talk that inspired this approach
|
|
1177
|
+
- 💻 **Early Implementation**: [ngc-validate](https://github.com/wardbell/ngc-validate) - The initial version of template-driven forms with Vest.js
|
|
1178
|
+
|
|
1179
|
+
These pioneers laid the groundwork that made ngx-vest-forms possible, combining the power of declarative validation with the elegance of Angular's template-driven approach.
|
|
587
1180
|
|
|
588
|
-
##
|
|
589
|
-
[](https://www.simplified.courses/complex-angular-template-driven-forms)
|
|
1181
|
+
## License
|
|
590
1182
|
|
|
591
|
-
|
|
1183
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|