ngx-vest-forms 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +586 -0
  2. package/esm2022/lib/components/control-wrapper/control-wrapper.component.mjs +56 -0
  3. package/esm2022/lib/constants.mjs +2 -0
  4. package/esm2022/lib/directives/form-model-group.directive.mjs +34 -0
  5. package/esm2022/lib/directives/form-model.directive.mjs +34 -0
  6. package/esm2022/lib/directives/form.directive.mjs +167 -0
  7. package/esm2022/lib/directives/validate-root-form.directive.mjs +70 -0
  8. package/esm2022/lib/exports.mjs +63 -0
  9. package/esm2022/lib/utils/array-to-object.mjs +4 -0
  10. package/esm2022/lib/utils/deep-partial.mjs +2 -0
  11. package/esm2022/lib/utils/deep-required.mjs +2 -0
  12. package/esm2022/lib/utils/form-utils.mjs +163 -0
  13. package/esm2022/lib/utils/shape-validation.mjs +59 -0
  14. package/esm2022/ngx-vest-forms.mjs +5 -0
  15. package/esm2022/public-api.mjs +14 -0
  16. package/fesm2022/ngx-vest-forms.mjs +629 -0
  17. package/fesm2022/ngx-vest-forms.mjs.map +1 -0
  18. package/index.d.ts +5 -0
  19. package/lib/components/control-wrapper/control-wrapper.component.d.ts +17 -0
  20. package/lib/constants.d.ts +1 -0
  21. package/lib/directives/form-model-group.directive.d.ts +13 -0
  22. package/lib/directives/form-model.directive.d.ts +13 -0
  23. package/lib/directives/form.directive.d.ts +81 -0
  24. package/lib/directives/validate-root-form.directive.d.ts +22 -0
  25. package/lib/exports.d.ts +17 -0
  26. package/lib/utils/array-to-object.d.ts +3 -0
  27. package/lib/utils/deep-partial.d.ts +8 -0
  28. package/lib/utils/deep-required.d.ts +7 -0
  29. package/lib/utils/form-utils.d.ts +32 -0
  30. package/lib/utils/shape-validation.d.ts +15 -0
  31. package/package.json +36 -0
  32. package/public-api.d.ts +12 -0
package/README.md ADDED
@@ -0,0 +1,586 @@
1
+ # ngx-vest-forms
2
+
3
+ ### Introduction
4
+
5
+ This is a very lightweight adapter for Angular template-driven forms and [vestjs](https://vestjs.dev).
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.
8
+
9
+ All the validations are asynchronous and use [vestjs](https://vestjs.dev) suites that can be re-used
10
+ across different frameworks and technologies.
11
+
12
+ ### Installation
13
+
14
+ You can install the package by running:
15
+
16
+ ```shell
17
+ npm i ngx-vest-forms
18
+ ```
19
+
20
+ ### Creating a simple form
21
+
22
+ Let's start by explaining how to create a simple form.
23
+ I want a form with a form group called `general` info that has 2 properties:
24
+ - `firstName`
25
+ - `lastName`
26
+
27
+ We need to import the `vestForms` const in the imports section of the `@Component` decorator.
28
+ Now we can apply the `scVestForm` directive to the `form` tag and listen to the `formValueChange` output to feed our signal.
29
+ In the form we create a form group for `generalInfo` with the `ngModelGroup` directive.
30
+ And we crate 2 inputs with the `name` attribute and the `[ngModel]` input.
31
+ **Do note that we are not using the banana in the box syntax but only tha square brackets, resulting in a unidirectional dataflow**
32
+
33
+ ```typescript
34
+ import { vestForms, DeepPartial } from 'ngx-vest-forms';
35
+
36
+ // A form model is always deep partial because angular will create it over time organically
37
+ type MyFormModel = DeepPartial<{
38
+ generalInfo: {
39
+ firstName: string;
40
+ lastName: string;
41
+ }
42
+ }>
43
+
44
+ @Component({
45
+ imports: [vestForms],
46
+ template: `
47
+ <form scVestForm
48
+ (formValueChange)="formValue.set($event)"
49
+ (ngSubmit)="onSubmit()">
50
+ <div ngModelGroup="generalInfo">
51
+ <label>First name</label>
52
+ <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
53
+
54
+ <label>Last name</label>
55
+ <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
56
+ </div>
57
+ </form>
58
+ `
59
+ })
60
+ export class MyComponent {
61
+ // This signal will hold the state of our form
62
+ protected readonly formValue = signal<MyFormModel>({});
63
+ }
64
+ ```
65
+
66
+ **Note: Template-driven forms are deep partial, so always use the `?` operator in your templates.**
67
+
68
+ That's it! This will feed the `formValue` signal and angular will create a form group and 2 form controls for us automatically.
69
+ The object that will be fed in the `formValue` signal will look like this:
70
+
71
+ ```typescript
72
+ formValue = {
73
+ generalInfo: {
74
+ firstName: '',
75
+ lastName: ''
76
+ }
77
+ }
78
+ ```
79
+
80
+ The ngForm will contain automatically created FormGroups and FormControls.
81
+ This does not have anything to do with this package. It's just Angular:
82
+ ```typescript
83
+ form = {
84
+ controls: {
85
+ generalInformation: { // FormGroup
86
+ controls: {
87
+ firstName: {...}, // FormControl
88
+ lastName: {...} //FormControl
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ The `scVestForm` directive offers some basic outputs for us though:
96
+
97
+ | Output | Description |
98
+ |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
99
+ | formValueChange<T> | Emits when the form value changes. But debounces<br/> the events since template-driven forms are created by the<br/>framework over time |
100
+ | dirtyChange<boolean> | Emits when the dirty state of the form changes |
101
+ | validChange<boolean> | Emits when the form becomes dirty or pristine |
102
+ | errorsChange | Emits an entire list of the form and all its form groups and controls |
103
+
104
+ ### Avoiding typo's
105
+
106
+ Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
107
+ Making a typo in those can result in a time-consuming endeavor. For this we have introduced shapes.
108
+ A shape is an object where the `scVestForm` can validate to. It is a deep required of the form model:
109
+
110
+ ```typescript
111
+ import { DeepPartial, DeepRequired, vestForms } from 'ngx-vest-forms';
112
+
113
+ type MyFormModel = DeepPartial<{
114
+ generalInfo: {
115
+ firstName: string;
116
+ lastName: string;
117
+ }
118
+ }>
119
+
120
+ export const myFormModelShape: DeepRequired<MyFormModel> = {
121
+ generalInfo: {
122
+ firstName: '',
123
+ lastName: ''
124
+ }
125
+ };
126
+
127
+ @Component({
128
+ imports: [vestForms],
129
+ template: `
130
+ <form scVestForm
131
+ [formShape]="shape"
132
+ (formValueChange)="formValue.set($event)"
133
+ (ngSubmit)="onSubmit()">
134
+
135
+ <div ngModelGroup="generalInfo">
136
+ <label>First name</label>
137
+ <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
138
+
139
+ <label>Last name</label>
140
+ <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
141
+ </div>
142
+ </form>
143
+ `
144
+ })
145
+ export class MyComponent {
146
+ protected readonly formValue = signal<MyFormModel>({});
147
+ protected readonly shape = myFormModelShape;
148
+ }
149
+ ```
150
+
151
+ By passing the shape to the `formShape` input the `scVestForm` will validate the actual form value
152
+ against the form shape every time the form changes, but only when Angular is in devMode.
153
+
154
+ Making a typo in the name attribute or an ngModelGroup attribute would result in runtime errors.
155
+ The console would look like this:
156
+
157
+ ```chatinput
158
+ Error: Shape mismatch:
159
+
160
+ [ngModel] Mismatch 'firstame'
161
+ [ngModelGroup] Mismatch: 'addresses.billingddress'
162
+ [ngModel] Mismatch 'addresses.billingddress.steet'
163
+ [ngModel] Mismatch 'addresses.billingddress.number'
164
+ [ngModel] Mismatch 'addresses.billingddress.city'
165
+ [ngModel] Mismatch 'addresses.billingddress.zipcode'
166
+ [ngModel] Mismatch 'addresses.billingddress.country'
167
+
168
+
169
+ at validateShape (shape-validation.ts:28:19)
170
+ at Object.next (form.directive.ts:178:17)
171
+ at ConsumerObserver.next (Subscriber.js:91:33)
172
+ at SafeSubscriber._next (Subscriber.js:60:26)
173
+ at SafeSubscriber.next (Subscriber.js:31:18)
174
+ at subscribe.innerSubscriber (switchMap.js:14:144)
175
+ at OperatorSubscriber._next (OperatorSubscriber.js:13:21)
176
+ at OperatorSubscriber.next (Subscriber.js:31:18)
177
+ at map.js:7:24
178
+ ```
179
+
180
+ ### Conditional fields
181
+
182
+ What if we want to remove a form control or form group? With reactive forms that would require a lot of work
183
+ but since Template driven forms do all the hard work for us, we can simply create a computed signal for that and
184
+ bind that in the template. Having logic in the template is considered a bad practice, so we can do all
185
+ the calculations in our class.
186
+
187
+ Let's hide `lastName` if `firstName` is not filled in:
188
+
189
+ ```html
190
+ <div ngModelGroup="generalInfo">
191
+ <label>First name</label>
192
+ <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
193
+
194
+ @if(lastNameAvailable()){
195
+ <label>Last name</label>
196
+ <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
197
+ }
198
+ </div>
199
+ ```
200
+
201
+ ```typescript
202
+ class MyComponent {
203
+ ...
204
+ protected readonly lastNameAvailable =
205
+ computed(() => !!this.formValue().generalInformation?.firstName);
206
+ }
207
+ ```
208
+
209
+ This will automatically add and remove the form control from our form model.
210
+ This also works for a form group:
211
+
212
+ ```html
213
+ @if(showGeneralInfo()){
214
+ <div ngModelGroup="generalInfo">
215
+ <label>First name</label>
216
+ <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
217
+
218
+ <label>Last name</label>
219
+ <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
220
+ </div>
221
+ }
222
+ ```
223
+
224
+ ### Reactive disabling
225
+
226
+ To achieve reactive disabling, we just have to take advantage of computed signals as well:
227
+
228
+ ```typescript
229
+ class MyComponent {
230
+ protected readonly lastNameDisabled =
231
+ computed(() => !this.formValue().generalInformation?.firstName);
232
+ }
233
+ ```
234
+
235
+ We can bind the computed signal to the `disabled` directive of Angular.
236
+ ```html
237
+ <input type="text" name="lastName"
238
+ [disabled]="lastNameDisabled()"
239
+ [ngModel]="formValue().generalInformation?.lastName"/>
240
+ ```
241
+
242
+ ### Validations
243
+
244
+ The absolute gem in ngx-vest-forms is the flexibility in validations without writing any boilerplate.
245
+ The only dependency this lib has is [vest.js](https://vestjs.dev). An awesome lightweight validation framework.
246
+ You can use it on the backend/frontend/Angular/react etc...
247
+
248
+ We use vest because it introduces the concept of vest suites. These are suites that kind of look like unit-tests
249
+ but that are highly flexible:
250
+ * [X] Write validations on forms
251
+ * [X] Write validations on form groups
252
+ * [X] Write validations on form controls
253
+ * [X] Composable/reuse-able different validation suites
254
+ * [X] Write conditional validations
255
+
256
+ This is how you write a simple Vest suite:
257
+ ```typescript
258
+ import { enforce, only, staticSuite, test } from 'vest';
259
+ import { MyFormModel } from '../models/my-form.model'
260
+
261
+ export const myFormModelSuite = staticSuite(
262
+ (model: MyformModel, field?: string) => {
263
+ if (field) {
264
+ // Needed to not run every validation every time
265
+ only(field);
266
+ }
267
+ test('firstName', 'First name is required', () => {
268
+ enforce(model.firstName).isNotBlank();
269
+ });
270
+ test('lastName', 'Last name is required', () => {
271
+ enforce(model.lastName).isNotBlank();
272
+ });
273
+ }
274
+ );
275
+ };
276
+ ```
277
+
278
+ In the `test` function the first parameter is the field, the second is the validation error.
279
+ The field is separated with the `.` syntax. So if we would have an `addresses` form group with an `billingAddress` form group inside
280
+ and a form control `street` the field would be: `addresses.billingAddress.street`.
281
+
282
+ This syntax should be self-explanatory and the entire enforcements guidelines can be found on [vest.js](https://vestjs.dev).
283
+
284
+ 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**
285
+
286
+ ```typescript
287
+ class MyComponent {
288
+ protected readonly formValue = signal<MyFormModel>({});
289
+ protected readonly suite = myFormModelSuite;
290
+ }
291
+ ```
292
+
293
+ ```html
294
+
295
+ <form scVestForm
296
+ [formShape]="shape"
297
+ [formValue]="formValue"
298
+ [suite]="suite"
299
+ (formValueChange)="formValue.set($event)"
300
+ (ngSubmit)="onSubmit()">
301
+ ...
302
+ </form>
303
+ ```
304
+
305
+ That's it. Validations are completely wired now. Because ngx-vest-forms will hook into the
306
+ `[ngModel]` and `ngModelGroup` attributes, and create ngValidators automatically.
307
+
308
+ It goes like this:
309
+ - Control gets created, Angular recognizes the `ngModel` and `ngModelGroup` directives
310
+ - These directives implement `AsyncValidator` and will connect to a vest suite
311
+ - User types into control
312
+ - The validate function gets called
313
+ - Vest gets called for one field
314
+ - Vest returns the errors
315
+ - @simpilfied/forms puts those errors on the angular form control
316
+
317
+ This means that `valid`, `invalid`, `errors`, `statusChanges` etc will keep on working
318
+ just like it would with a regular angular form.
319
+
320
+ #### Showing validation errors
321
+
322
+ Now we want to show the validation errors in a consistent way.
323
+ For that we have provided the `sc-control-wrapper` attribute component.
324
+
325
+ You can use it on:
326
+ - elements that hold `ngModelGroup`
327
+ - elements that have an `ngModel` (or form control) inside of them.
328
+
329
+ This will show errors automatically on:
330
+ - form submit
331
+ - blur
332
+
333
+ **Note:** If those requirements don't fill your need, you can write a custom control-wrapper by copy-pasting the
334
+ `control-wrapper` and adjusting the code.
335
+
336
+ Let's update our form:
337
+
338
+ ```html
339
+
340
+ <div ngModelGroup="generalInfo" sc-control-wrapper>
341
+ <div sc-control-wrapper>
342
+ <label>First name</label
343
+ <input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
344
+ </div>
345
+
346
+ <div sc-control-wrapper>
347
+ <label>Last name</label>
348
+ <input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
349
+ </div>
350
+ </div>
351
+ ```
352
+
353
+ This is the only thing we need to do to create a form that is completely wired with vest.
354
+ * [x] Automatic creation of form controls and form groups
355
+ * [x] Automatic connection to vest suites
356
+ * [x] Automatic typo validation
357
+ * [x] Automatic adding of css error classes and showing validation messages
358
+ * [x] On blur
359
+ * [x] On submit
360
+
361
+ ### Conditional validations
362
+
363
+ Vest makes it extremely easy to create conditional validations.
364
+ Assume we have a form model that has `age` and `emergencyContact`.
365
+ The `emergencyContact` is required, but only when the person is not of legal age.
366
+
367
+ We can use the `omitWhen` so that when the person is below 18, the assertion
368
+ will not be done.
369
+
370
+ ```typescript
371
+ import { enforce, omitWhen, only, staticSuite, test } from 'vest';
372
+
373
+ ...
374
+ omitWhen((model.age || 0) >= 18, () => {
375
+ test('emergencyContact', 'Emergency contact is required', () => {
376
+ enforce(model.emergencyContact).isNotBlank();
377
+ });
378
+ });
379
+ ```
380
+
381
+ You can put those validations on every field that you want. On form group fields and on form control fields.
382
+ Check this interesting example below:
383
+
384
+ * [x] Password is always required
385
+ * [x] Confirm password is only required when there is a password
386
+ * [x] The passwords should match, but only when they are both filled in
387
+
388
+ ```typescript
389
+ test('passwords.password', 'Password is not filled in', () => {
390
+ enforce(model.passwords?.password).isNotBlank();
391
+ });
392
+ omitWhen(!model.passwords?.password, () => {
393
+ test('passwords.confirmPassword', 'Confirm password is not filled in', () => {
394
+ enforce(model.passwords?.confirmPassword).isNotBlank();
395
+ });
396
+ });
397
+ omitWhen(!model.passwords?.password || !model.passwords?.confirmPassword, () => {
398
+ test('passwords', 'Passwords do not match', () => {
399
+ enforce(model.passwords?.confirmPassword).equals(model.passwords?.password);
400
+ });
401
+ });
402
+ ```
403
+
404
+ Forget about manually adding, removing validators on reactive forms and not being able to
405
+ re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
406
+ **Oh, it's also pretty readable**
407
+
408
+ ### Composable validations
409
+
410
+ We can compose validations suites with sub suites. After all, we want to re-use certain pieces of our
411
+ validation logic and we don't want one huge unreadable suite.
412
+ This is quite straightforward with Vest.
413
+
414
+ Let's take this simple function that validates an address:
415
+
416
+ ```typescript
417
+ export function addressValidations(model: AddressModel | undefined, field: string): void {
418
+ test(`${field}.street`, 'Street is required', () => {
419
+ enforce(model?.street).isNotBlank();
420
+ });
421
+ test(`${field}.city`, 'City is required', () => {
422
+ enforce(model?.city).isNotBlank();
423
+ });
424
+ test(`${field}.zipcode`, 'Zipcode is required', () => {
425
+ enforce(model?.zipcode).isNotBlank();
426
+ });
427
+ test(`${field}.number`, 'Number is required', () => {
428
+ enforce(model?.number).isNotBlank();
429
+ });
430
+ test(`${field}.country`, 'Country is required', () => {
431
+ enforce(model?.country).isNotBlank();
432
+ });
433
+ }
434
+ ```
435
+
436
+ Our suite would consume it like this:
437
+
438
+ ```typescript
439
+ import { enforce, omitWhen, only, staticSuite, test } from 'vest';
440
+ import { PurchaseFormModel } from '../models/purchaseFormModel';
441
+
442
+ export const mySuite = staticSuite(
443
+ (model: PurchaseFormModel, field?: string) => {
444
+ if (field) {
445
+ only(field);
446
+ }
447
+ addressValidations(model.addresses?.billingAddress, 'addresses.billingAddress');
448
+ addressValidations(model.addresses?.shippingAddress, 'addresses.shippingAddress');
449
+ }
450
+ );
451
+ ```
452
+
453
+ We achieved decoupling, readability and reuse of our addressValidations.
454
+
455
+ #### A more complex example
456
+
457
+ Let's combine the conditional part with the reusable part.
458
+ We have 2 addresses, but the shippingAddress is only required when the `shippingAddressIsDifferentFromBillingAddress`
459
+ Checkbox is checked. But if it is checked, all fields are required.
460
+ And if both addresses are filled in, they should be different.
461
+
462
+ This gives us validation on:
463
+ * [x] The addresses form field (they can't be equal)
464
+ * [x] The shipping Address field (only required when checkbox is checked)
465
+ * [x] validation on all the address fields (street, number, etc) on both addresses
466
+
467
+ ```typescript
468
+ addressValidations(
469
+ model.addresses?.billingAddress,
470
+ 'addresses.billingAddress'
471
+ );
472
+ omitWhen(
473
+ !model.addresses?.shippingAddressDifferentFromBillingAddress,
474
+ () => {
475
+ addressValidations(
476
+ model.addresses?.shippingAddress,
477
+ 'addresses.shippingAddress'
478
+ );
479
+ test('addresses', 'The addresses appear to be the same', () => {
480
+ enforce(JSON.stringify(model.addresses?.billingAddress)).notEquals(
481
+ JSON.stringify(model.addresses?.shippingAddress)
482
+ );
483
+ });
484
+ }
485
+ );
486
+ ```
487
+
488
+ ### Validations on the root form
489
+
490
+ When we want to validate multiple fields that are depending on each other,
491
+ it is a best practice to wrap them in a parent form group.
492
+ If `password` and `confirmPassword` have to be equal the validation should not happen on
493
+ `password` nor on `confirmPassword`, it should happen on `passwords`:
494
+
495
+ ```typescript
496
+ const form = {
497
+ // validation happens here
498
+ passwords: {
499
+ password: '',
500
+ confirmPassword: ''
501
+ }
502
+ };
503
+ ```
504
+
505
+ Sometimes we don't have the ability to create a form group for 2 depending fields, or sometimes we just
506
+ want to create validation rules on portions of the form. For that we can use `validateRootForm`.
507
+ Use the `errorsChange` output to keep the errors as state in a signal that we can use in the template
508
+ wherever we want.
509
+
510
+ ```html
511
+ {{ errors()?.['rootForm'] }} <!-- render the errors on the rootForm -->
512
+ {{ errors() }} <!-- render all the errors -->
513
+ <form scVestForm
514
+ [formValue]="formValue()"
515
+ [validateRootForm]="true"
516
+ [formShape]="shape"
517
+ [suite]="suite"
518
+ (errorsChange)="errors.set($event)"
519
+ ...>
520
+ </form>
521
+ ```
522
+
523
+ ```typescript
524
+ export class MyformComponent {
525
+ protected readonly formValue = signal<MyFormModel>({});
526
+ protected readonly suite = myFormModelSuite;
527
+ // Keep the errors in state
528
+ protected readonly errors = signal<Record<string, string>>({ });
529
+ }
530
+ ```
531
+
532
+ When setting the `[validateRootForm]` directive to true, the form will
533
+ also create an ngValidator on root level, that listens to the ROOT_FORM field.
534
+
535
+ To make this work we need to use the field in the vest suite like this:
536
+
537
+ ```typescript
538
+ import { ROOT_FORM } from 'ngx-vest-forms';
539
+
540
+ test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
541
+ enforce(
542
+ model.firstName === 'Brecht' &&
543
+ model.lastName === 'Billiet' &&
544
+ model.age === 30).isFalsy();
545
+ });
546
+ ```
547
+
548
+ ### Validation dependencies
549
+
550
+ Sometimes we need to re-trigger validations of form controls or form groups because they are dependant on other form controls or form groups.
551
+ For instance: A `confirmPassword` field is not required unless the `password` is filled in. Which means that when the `password` field gets a new value,
552
+ we need to run the validations on `confirmPassword`.
553
+
554
+
555
+ ### Form arrays
556
+
557
+ Todo
558
+
559
+ #### Form array validations
560
+
561
+ An example can be found [in this simplified courses article](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)
562
+
563
+ We can look in `projects/examples/src/app/validations/phonenumber.validations.ts` to see an example on the validations part.
564
+
565
+
566
+ ### Child form components
567
+
568
+ Big forms result in big files. It makes sense to split them up.
569
+ For instance an address form can be reused, so we want to create a child component for that.
570
+ We have to make sure that this child component can access the ngForm.
571
+ For that we have to use the `vestFormViewProviders` from `ngx-vest-forms`
572
+
573
+ ```typescript
574
+ ...
575
+ import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
576
+
577
+ @Component({
578
+ ...
579
+ viewProviders: [vestFormsViewProviders]
580
+ })
581
+ export class AddressComponent {
582
+ @Input() address?: AddressModel;
583
+ }
584
+ ```
585
+
586
+ You can check the examples in the github repo [here](https://github.com/simplifiedcourses/ngx-vest-forms/blob/master/projects/examples).
@@ -0,0 +1,56 @@
1
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DestroyRef, HostBinding, inject } from '@angular/core';
2
+ import { NgModel, NgModelGroup } from '@angular/forms';
3
+ import { mergeWith, of, switchMap } from 'rxjs';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { FormDirective } from '../../directives/form.directive';
6
+ import * as i0 from "@angular/core";
7
+ export class ControlWrapperComponent {
8
+ constructor() {
9
+ this.cdRef = inject(ChangeDetectorRef);
10
+ this.formDirective = inject(FormDirective);
11
+ this.destroyRef = inject(DestroyRef);
12
+ this.ngModelGroup = inject(NgModelGroup, {
13
+ optional: true,
14
+ self: true,
15
+ });
16
+ }
17
+ get control() {
18
+ return this.ngModelGroup ? this.ngModelGroup.control : this.ngModel?.control;
19
+ }
20
+ get invalid() {
21
+ return this.control?.touched && this.errors;
22
+ }
23
+ get errors() {
24
+ if (this.control?.pending) {
25
+ return this.previousError;
26
+ }
27
+ else {
28
+ this.previousError = this.control?.errors?.['errors'];
29
+ }
30
+ return this.control?.errors?.['errors'];
31
+ }
32
+ ngAfterViewInit() {
33
+ // Wait until the form is idle
34
+ // Then, listen to all events of the ngModelGroup or ngModel
35
+ // and mark the component and its ancestors as dirty
36
+ // This allows us to use the OnPush ChangeDetection Strategy
37
+ this.formDirective.idle$
38
+ .pipe(switchMap(() => this.ngModelGroup?.control?.events || of(null)), mergeWith(this.control?.events || of(null)), takeUntilDestroyed(this.destroyRef))
39
+ .subscribe(() => {
40
+ this.cdRef.markForCheck();
41
+ });
42
+ }
43
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
44
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.1", type: ControlWrapperComponent, isStandalone: true, selector: "[sc-control-wrapper]", host: { properties: { "class.sc-control-wrapper--invalid": "this.invalid" } }, queries: [{ propertyName: "ngModel", first: true, predicate: NgModel, descendants: true }], ngImport: i0, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
45
+ }
46
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
47
+ type: Component,
48
+ args: [{ selector: '[sc-control-wrapper]', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n" }]
49
+ }], propDecorators: { ngModel: [{
50
+ type: ContentChild,
51
+ args: [NgModel]
52
+ }], invalid: [{
53
+ type: HostBinding,
54
+ args: ['class.sc-control-wrapper--invalid']
55
+ }] } });
56
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC12ZXN0LWZvcm1zL3NyYy9saWIvY29tcG9uZW50cy9jb250cm9sLXdyYXBwZXIvY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC12ZXN0LWZvcm1zL3NyYy9saWIvY29tcG9uZW50cy9jb250cm9sLXdyYXBwZXIvY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFFSCx1QkFBdUIsRUFDdkIsaUJBQWlCLEVBQ2pCLFNBQVMsRUFDVCxZQUFZLEVBQ1osVUFBVSxFQUNWLFdBQVcsRUFDWCxNQUFNLEVBQ1QsTUFBTSxlQUFlLENBQUM7QUFFdkIsT0FBTyxFQUFtQixPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEUsT0FBTyxFQUFFLFNBQVMsRUFBRSxFQUFFLEVBQUUsU0FBUyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQ2hELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQzs7QUFTaEUsTUFBTSxPQUFPLHVCQUF1QjtJQVBwQztRQVFxQixVQUFLLEdBQUcsTUFBTSxDQUFDLGlCQUFpQixDQUFDLENBQUM7UUFDbEMsa0JBQWEsR0FBRyxNQUFNLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDdEMsZUFBVSxHQUFHLE1BQU0sQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUdqQyxpQkFBWSxHQUF3QixNQUFNLENBQUMsWUFBWSxFQUFFO1lBQ3JFLFFBQVEsRUFBRSxJQUFJO1lBQ2QsSUFBSSxFQUFFLElBQUk7U0FDYixDQUFDLENBQUM7S0F1Q047SUFqQ0csSUFBWSxPQUFPO1FBQ2YsT0FBTyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUM7SUFDakYsQ0FBQztJQUVELElBQ1csT0FBTztRQUNkLE9BQU8sSUFBSSxDQUFDLE9BQU8sRUFBRSxPQUFPLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQztJQUNoRCxDQUFDO0lBRUQsSUFBVyxNQUFNO1FBQ2IsSUFBSSxJQUFJLENBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDO1lBQ3hCLE9BQU8sSUFBSSxDQUFDLGFBQWEsQ0FBQztRQUM5QixDQUFDO2FBQU0sQ0FBQztZQUNKLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUMxRCxDQUFDO1FBQ0QsT0FBTyxJQUFJLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzVDLENBQUM7SUFFTSxlQUFlO1FBQ2xCLDhCQUE4QjtRQUM5Qiw0REFBNEQ7UUFDNUQsb0RBQW9EO1FBQ3BELDREQUE0RDtRQUM1RCxJQUFJLENBQUMsYUFBYSxDQUFDLEtBQUs7YUFDbkIsSUFBSSxDQUNELFNBQVMsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxFQUFFLE9BQU8sRUFBRSxNQUFNLElBQUksRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQy9ELFNBQVMsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLE1BQU0sSUFBSSxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsRUFDM0Msa0JBQWtCLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUN0QzthQUNBLFNBQVMsQ0FBQyxHQUFHLEVBQUU7WUFDWixJQUFJLENBQUMsS0FBSyxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzlCLENBQUMsQ0FBQyxDQUFDO0lBQ1gsQ0FBQzs4R0EvQ1EsdUJBQXVCO2tHQUF2Qix1QkFBdUIsb01BS2xCLE9BQU8sZ0RDNUJ6Qiw2VEFZQTs7MkZEV2EsdUJBQXVCO2tCQVBuQyxTQUFTOytCQUNJLHNCQUFzQixjQUNwQixJQUFJLG1CQUdDLHVCQUF1QixDQUFDLE1BQU07OEJBT2pCLE9BQU87c0JBQXBDLFlBQVk7dUJBQUMsT0FBTztnQkFlVixPQUFPO3NCQURqQixXQUFXO3VCQUFDLG1DQUFtQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gICAgQWZ0ZXJWaWV3SW5pdCxcbiAgICBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSxcbiAgICBDaGFuZ2VEZXRlY3RvclJlZixcbiAgICBDb21wb25lbnQsXG4gICAgQ29udGVudENoaWxkLFxuICAgIERlc3Ryb3lSZWYsXG4gICAgSG9zdEJpbmRpbmcsXG4gICAgaW5qZWN0XG59IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuXG5pbXBvcnQgeyBBYnN0cmFjdENvbnRyb2wsIE5nTW9kZWwsIE5nTW9kZWxHcm91cCB9IGZyb20gJ0Bhbmd1bGFyL2Zvcm1zJztcbmltcG9ydCB7IG1lcmdlV2l0aCwgb2YsIHN3aXRjaE1hcCB9IGZyb20gJ3J4anMnO1xuaW1wb3J0IHsgdGFrZVVudGlsRGVzdHJveWVkIH0gZnJvbSAnQGFuZ3VsYXIvY29yZS9yeGpzLWludGVyb3AnO1xuaW1wb3J0IHsgRm9ybURpcmVjdGl2ZSB9IGZyb20gJy4uLy4uL2RpcmVjdGl2ZXMvZm9ybS5kaXJlY3RpdmUnO1xuXG5AQ29tcG9uZW50KHtcbiAgICBzZWxlY3RvcjogJ1tzYy1jb250cm9sLXdyYXBwZXJdJyxcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxuICAgIHRlbXBsYXRlVXJsOiAnLi9jb250cm9sLXdyYXBwZXIuY29tcG9uZW50Lmh0bWwnLFxuICAgIHN0eWxlVXJsczogWycuL2NvbnRyb2wtd3JhcHBlci5jb21wb25lbnQuc2NzcyddLFxuICAgIGNoYW5nZURldGVjdGlvbjogQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3kuT25QdXNoXG59KVxuZXhwb3J0IGNsYXNzIENvbnRyb2xXcmFwcGVyQ29tcG9uZW50IGltcGxlbWVudHMgQWZ0ZXJWaWV3SW5pdCB7XG4gICAgcHJpdmF0ZSByZWFkb25seSBjZFJlZiA9IGluamVjdChDaGFuZ2VEZXRlY3RvclJlZik7XG4gICAgcHJpdmF0ZSByZWFkb25seSBmb3JtRGlyZWN0aXZlID0gaW5qZWN0KEZvcm1EaXJlY3RpdmUpO1xuICAgIHByaXZhdGUgcmVhZG9ubHkgZGVzdHJveVJlZiA9IGluamVjdChEZXN0cm95UmVmKTtcblxuICAgIEBDb250ZW50Q2hpbGQoTmdNb2RlbCkgcHVibGljIG5nTW9kZWw/OiBOZ01vZGVsOyAvLyBPcHRpb25hbCBuZ01vZGVsXG4gICAgcHVibGljIHJlYWRvbmx5IG5nTW9kZWxHcm91cDogTmdNb2RlbEdyb3VwIHwgbnVsbCA9IGluamVjdChOZ01vZGVsR3JvdXAsIHtcbiAgICAgICAgb3B0aW9uYWw6IHRydWUsXG4gICAgICAgIHNlbGY6IHRydWUsXG4gICAgfSk7XG5cblxuICAgIC8vIENhY2hlIHRoZSBwcmV2aW91cyBlcnJvciB0byBhdm9pZCAnZmxpY2tlcmluZydcbiAgICBwcml2YXRlIHByZXZpb3VzRXJyb3I/OiBzdHJpbmdbXTtcblxuICAgIHByaXZhdGUgZ2V0IGNvbnRyb2woKTogQWJzdHJhY3RDb250cm9sIHwgdW5kZWZpbmVkIHtcbiAgICAgICAgcmV0dXJuIHRoaXMubmdNb2RlbEdyb3VwID8gdGhpcy5uZ01vZGVsR3JvdXAuY29udHJvbCA6IHRoaXMubmdNb2RlbD8uY29udHJvbDtcbiAgICB9XG5cbiAgICBASG9zdEJpbmRpbmcoJ2NsYXNzLnNjLWNvbnRyb2wtd3JhcHBlci0taW52YWxpZCcpXG4gICAgcHVibGljIGdldCBpbnZhbGlkKCkge1xuICAgICAgICByZXR1cm4gdGhpcy5jb250cm9sPy50b3VjaGVkICYmIHRoaXMuZXJyb3JzO1xuICAgIH1cblxuICAgIHB1YmxpYyBnZXQgZXJyb3JzKCk6IHN0cmluZ1tdIHwgdW5kZWZpbmVkIHtcbiAgICAgICAgaWYgKHRoaXMuY29udHJvbD8ucGVuZGluZykge1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMucHJldmlvdXNFcnJvcjtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHRoaXMucHJldmlvdXNFcnJvciA9IHRoaXMuY29udHJvbD8uZXJyb3JzPy5bJ2Vycm9ycyddO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLmNvbnRyb2w/LmVycm9ycz8uWydlcnJvcnMnXTtcbiAgICB9XG5cbiAgICBwdWJsaWMgbmdBZnRlclZpZXdJbml0KCk6IHZvaWQge1xuICAgICAgICAvLyBXYWl0IHVudGlsIHRoZSBmb3JtIGlzIGlkbGVcbiAgICAgICAgLy8gVGhlbiwgbGlzdGVuIHRvIGFsbCBldmVudHMgb2YgdGhlIG5nTW9kZWxHcm91cCBvciBuZ01vZGVsXG4gICAgICAgIC8vIGFuZCBtYXJrIHRoZSBjb21wb25lbnQgYW5kIGl0cyBhbmNlc3RvcnMgYXMgZGlydHlcbiAgICAgICAgLy8gVGhpcyBhbGxvd3MgdXMgdG8gdXNlIHRoZSBPblB1c2ggQ2hhbmdlRGV0ZWN0aW9uIFN0cmF0ZWd5XG4gICAgICAgIHRoaXMuZm9ybURpcmVjdGl2ZS5pZGxlJFxuICAgICAgICAgICAgLnBpcGUoXG4gICAgICAgICAgICAgICAgc3dpdGNoTWFwKCgpID0+IHRoaXMubmdNb2RlbEdyb3VwPy5jb250cm9sPy5ldmVudHMgfHwgb2YobnVsbCkpLFxuICAgICAgICAgICAgICAgIG1lcmdlV2l0aCh0aGlzLmNvbnRyb2w/LmV2ZW50cyB8fCBvZihudWxsKSksXG4gICAgICAgICAgICAgICAgdGFrZVVudGlsRGVzdHJveWVkKHRoaXMuZGVzdHJveVJlZilcbiAgICAgICAgICAgIClcbiAgICAgICAgICAgIC5zdWJzY3JpYmUoKCkgPT4ge1xuICAgICAgICAgICAgICAgIHRoaXMuY2RSZWYubWFya0ZvckNoZWNrKCk7XG4gICAgICAgICAgICB9KTtcbiAgICB9XG59XG4iLCI8ZGl2IGNsYXNzPVwic2MtY29udHJvbC13cmFwcGVyXCI+XG4gIDxkaXYgY2xhc3M9XCJzYy1jb250cm9sLXdyYXBwZXJfX2NvbnRlbnRcIj5cbiAgICA8bmctY29udGVudD48L25nLWNvbnRlbnQ+XG4gIDwvZGl2PlxuICA8ZGl2IGNsYXNzPVwic2MtY29udHJvbC13cmFwcGVyX19lcnJvcnNcIj5cbiAgICA8dWwgW2hpZGRlbl09XCIhaW52YWxpZFwiPlxuICAgICAgQGZvciAoZXJyb3Igb2YgZXJyb3JzOyB0cmFjayBlcnJvcikge1xuICAgICAgICA8bGk+e3sgZXJyb3IgfX08L2xpPlxuICAgICAgfVxuICAgIDwvdWw+XG4gIDwvZGl2PlxuPC9kaXY+XG4iXX0=
@@ -0,0 +1,2 @@
1
+ export const ROOT_FORM = 'rootForm';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LXZlc3QtZm9ybXMvc3JjL2xpYi9jb25zdGFudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLE1BQU0sU0FBUyxHQUFHLFVBQVUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBST09UX0ZPUk0gPSAncm9vdEZvcm0nOyJdfQ==