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
@@ -0,0 +1,629 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, DestroyRef, Directive, isDevMode, Output, ChangeDetectorRef, Component, ChangeDetectionStrategy, ContentChild, HostBinding, Optional } from '@angular/core';
3
+ import { FormGroup, FormArray, NG_ASYNC_VALIDATORS, NgForm, StatusChangeEvent, ValueChangeEvent, PristineChangeEvent, NgModelGroup, NgModel, ControlContainer, FormsModule } from '@angular/forms';
4
+ import { of, ReplaySubject, debounceTime, take, switchMap, Observable, filter, map, distinctUntilChanged, tap, zip, mergeWith } from 'rxjs';
5
+ import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
6
+
7
+ const ROOT_FORM = 'rootForm';
8
+
9
+ /**
10
+ * Recursively calculates the path of a form control
11
+ * @param formGroup
12
+ * @param control
13
+ */
14
+ function getControlPath(formGroup, control) {
15
+ for (const key in formGroup.controls) {
16
+ if (formGroup.controls.hasOwnProperty(key)) {
17
+ const ctrl = formGroup.get(key);
18
+ if (ctrl instanceof FormGroup) {
19
+ const path = getControlPath(ctrl, control);
20
+ if (path) {
21
+ return key + '.' + path;
22
+ }
23
+ }
24
+ else if (ctrl === control) {
25
+ return key;
26
+ }
27
+ }
28
+ }
29
+ return '';
30
+ }
31
+ /**
32
+ * Recursively calculates the path of a form group
33
+ * @param formGroup
34
+ * @param control
35
+ */
36
+ function getGroupPath(formGroup, control) {
37
+ for (const key in formGroup.controls) {
38
+ if (formGroup.controls.hasOwnProperty(key)) {
39
+ const ctrl = formGroup.get(key);
40
+ if (ctrl === control) {
41
+ return key;
42
+ }
43
+ if (ctrl instanceof FormGroup) {
44
+ const path = getGroupPath(ctrl, control);
45
+ if (path) {
46
+ return key + '.' + path;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return '';
52
+ }
53
+ /**
54
+ * Calculates the field name of a form control: Eg: addresses.shippingAddress.street
55
+ * @param rootForm
56
+ * @param control
57
+ */
58
+ function getFormControlField(rootForm, control) {
59
+ return getControlPath(rootForm, control);
60
+ }
61
+ /**
62
+ * Calcuates the field name of a form group Eg: addresses.shippingAddress
63
+ * @param rootForm
64
+ * @param control
65
+ */
66
+ function getFormGroupField(rootForm, control) {
67
+ return getGroupPath(rootForm, control);
68
+ }
69
+ /**
70
+ * This RxJS operator merges the value of the form with the raw value.
71
+ * By doing this we can assure that we don't lose values of disabled form fields
72
+ * @param form
73
+ */
74
+ function mergeValuesAndRawValues(form) {
75
+ // Retrieve the standard values (respecting references)
76
+ const value = { ...form.value };
77
+ // Retrieve the raw values (including disabled values)
78
+ const rawValue = form.getRawValue();
79
+ // Recursive function to merge rawValue into value
80
+ function mergeRecursive(target, source) {
81
+ Object.keys(source).forEach(key => {
82
+ if (target[key] === undefined) {
83
+ // If the key is not in the target, add it directly (for disabled fields)
84
+ target[key] = source[key];
85
+ }
86
+ else if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
87
+ // If the value is an object, merge it recursively
88
+ mergeRecursive(target[key], source[key]);
89
+ }
90
+ // If the target already has the key with a primitive value, it's left as is to maintain references
91
+ });
92
+ }
93
+ mergeRecursive(value, rawValue);
94
+ return value;
95
+ }
96
+ function isPrimitive(value) {
97
+ return value === null || (typeof value !== "object" && typeof value !== "function");
98
+ }
99
+ /**
100
+ * Performs a deep-clone of an object
101
+ * @param obj
102
+ */
103
+ function cloneDeep(obj) {
104
+ // Handle primitives (null, undefined, boolean, string, number, function)
105
+ if (isPrimitive(obj)) {
106
+ return obj;
107
+ }
108
+ // Handle Date
109
+ if (obj instanceof Date) {
110
+ return new Date(obj.getTime());
111
+ }
112
+ // Handle Array
113
+ if (Array.isArray(obj)) {
114
+ return obj.map(item => cloneDeep(item));
115
+ }
116
+ // Handle Object
117
+ if (obj instanceof Object) {
118
+ const clonedObj = {};
119
+ for (const key in obj) {
120
+ if (obj.hasOwnProperty(key)) {
121
+ clonedObj[key] = cloneDeep(obj[key]);
122
+ }
123
+ }
124
+ return clonedObj;
125
+ }
126
+ throw new Error("Unable to copy object! Its type isn't supported.");
127
+ }
128
+ /**
129
+ * Sets a value in an object in the correct path
130
+ * @param obj
131
+ * @param path
132
+ * @param value
133
+ */
134
+ function set(obj, path, value) {
135
+ const keys = path.split('.');
136
+ let current = obj;
137
+ for (let i = 0; i < keys.length - 1; i++) {
138
+ const key = keys[i];
139
+ if (!current[key]) {
140
+ current[key] = {};
141
+ }
142
+ current = current[key];
143
+ }
144
+ current[keys[keys.length - 1]] = value;
145
+ }
146
+ function getAllFormErrors(form) {
147
+ const errors = {};
148
+ if (!form) {
149
+ return errors;
150
+ }
151
+ function collect(control, path) {
152
+ if (control instanceof FormGroup || control instanceof FormArray) {
153
+ Object.keys(control.controls).forEach((key) => {
154
+ const childControl = control.get(key);
155
+ const controlPath = path ? `${path}.${key}` : key;
156
+ if (childControl) {
157
+ collect(childControl, controlPath);
158
+ }
159
+ });
160
+ }
161
+ if (control.errors && control.enabled) {
162
+ errors[path] = control.errors['errors'];
163
+ }
164
+ }
165
+ collect(form, '');
166
+ errors[ROOT_FORM] = form.errors && form.errors['errors'];
167
+ return errors;
168
+ }
169
+
170
+ class ValidateRootFormDirective {
171
+ constructor() {
172
+ this.formValue = input(null);
173
+ this.suite = input(null);
174
+ this.destroyRef = inject(DestroyRef);
175
+ /**
176
+ * Whether the root form should be validated or not
177
+ * This will use the field rootForm
178
+ */
179
+ this.validateRootForm = input(false);
180
+ /**
181
+ * Used to debounce formValues to make sure vest isn't triggered all the time
182
+ */
183
+ this.formValueCache = {};
184
+ }
185
+ validate(control) {
186
+ if (!this.suite() || !this.formValue()) {
187
+ return of(null);
188
+ }
189
+ return this.createAsyncValidator('rootForm')(control.getRawValue());
190
+ }
191
+ createAsyncValidator(field) {
192
+ if (!this.suite()) {
193
+ return () => of(null);
194
+ }
195
+ return (value) => {
196
+ if (!this.formValue()) {
197
+ return of(null);
198
+ }
199
+ const mod = cloneDeep(value);
200
+ set(mod, field, value); // Update the property with path
201
+ if (!this.formValueCache[field]) {
202
+ this.formValueCache[field] = {
203
+ sub$$: new ReplaySubject(1), // Keep track of the last model
204
+ };
205
+ this.formValueCache[field].debounced = this.formValueCache[field].sub$$.pipe(debounceTime(0));
206
+ }
207
+ // Next the latest model in the cache for a certain field
208
+ this.formValueCache[field].sub$$.next(mod);
209
+ return this.formValueCache[field].debounced.pipe(
210
+ // When debounced, take the latest value and perform the asynchronous vest validation
211
+ take(1), switchMap(() => {
212
+ return new Observable((observer) => {
213
+ this.suite()(mod, field).done((result) => {
214
+ const errors = result.getErrors()[field];
215
+ observer.next((errors ? { error: errors[0], errors } : null));
216
+ observer.complete();
217
+ });
218
+ });
219
+ }), takeUntilDestroyed(this.destroyRef));
220
+ };
221
+ }
222
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ValidateRootFormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
223
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: ValidateRootFormDirective, isStandalone: true, selector: "form[validateRootForm][formValue][suite]", inputs: { formValue: { classPropertyName: "formValue", publicName: "formValue", isSignal: true, isRequired: false, transformFunction: null }, suite: { classPropertyName: "suite", publicName: "suite", isSignal: true, isRequired: false, transformFunction: null }, validateRootForm: { classPropertyName: "validateRootForm", publicName: "validateRootForm", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ValidateRootFormDirective, multi: true }], ngImport: i0 }); }
224
+ }
225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ValidateRootFormDirective, decorators: [{
226
+ type: Directive,
227
+ args: [{
228
+ selector: 'form[validateRootForm][formValue][suite]',
229
+ standalone: true,
230
+ providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ValidateRootFormDirective, multi: true }]
231
+ }]
232
+ }] });
233
+
234
+ /**
235
+ * Clean error that improves the DX when making typo's in the `name` or `ngModelGroup` attributes
236
+ */
237
+ class ShapeMismatchError extends Error {
238
+ constructor(errorList) {
239
+ super(`Shape mismatch:\n\n${errorList.join('\n')}\n\n`);
240
+ }
241
+ }
242
+ /**
243
+ * Validates a form value against a shape
244
+ * When there is something in the form value that is not in the shape, throw an error
245
+ * This is how we throw runtime errors in develop when the developer has made a typo in the `name` or `ngModelGroup`
246
+ * attributes.
247
+ * @param formVal
248
+ * @param shape
249
+ */
250
+ function validateShape(formVal, shape) {
251
+ // Only execute in dev mode
252
+ if (isDevMode()) {
253
+ const errors = validateFormValue(formVal, shape);
254
+ if (errors.length) {
255
+ throw new ShapeMismatchError(errors);
256
+ }
257
+ }
258
+ }
259
+ /**
260
+ * Validates a form value against a shape value to see if it matches
261
+ * Returns clean errors that have a good DX
262
+ * @param formValue
263
+ * @param shape
264
+ * @param path
265
+ */
266
+ function validateFormValue(formValue, shape, path = '') {
267
+ const errors = [];
268
+ for (const key in formValue) {
269
+ if (Object.keys(formValue).includes(key)) {
270
+ // In form arrays we don't know how many items there are
271
+ // This means that we always need to provide one record in the shape of our form array
272
+ // so every time reset the key to '0' when the key is a number and is bigger than 0
273
+ let keyToCompareWith = key;
274
+ if (parseFloat(key) > 0) {
275
+ keyToCompareWith = '0';
276
+ }
277
+ const newPath = path ? `${path}.${key}` : key;
278
+ if (typeof formValue[key] === 'object' && formValue[key] !== null) {
279
+ if ((typeof shape[keyToCompareWith] !== 'object' || shape[keyToCompareWith] === null) && isNaN(parseFloat(key))) {
280
+ errors.push(`[ngModelGroup] Mismatch: '${newPath}'`);
281
+ }
282
+ errors.push(...validateFormValue(formValue[key], shape[keyToCompareWith], newPath));
283
+ }
284
+ else if ((shape ? !(key in shape) : true) && isNaN(parseFloat(key))) {
285
+ errors.push(`[ngModel] Mismatch '${newPath}'`);
286
+ }
287
+ }
288
+ }
289
+ return errors;
290
+ }
291
+
292
+ class FormDirective {
293
+ constructor() {
294
+ this.ngForm = inject(NgForm, { self: true, optional: false });
295
+ /**
296
+ * The value of the form, this is needed for the validation part
297
+ */
298
+ this.formValue = input(null);
299
+ /**
300
+ * Static vest suite that will be used to feed our angular validators
301
+ */
302
+ this.suite = input(null);
303
+ /**
304
+ * The shape of our form model. This is a deep required version of the form model
305
+ * The goal is to add default values to the shape so when the template-driven form
306
+ * contains values that shouldn't be there (typo's) that the developer gets run-time
307
+ * errors in dev mode
308
+ */
309
+ this.formShape = input(null);
310
+ /**
311
+ * Updates the validation config which is a dynamic object that will be used to
312
+ * trigger validations on the dependant fields
313
+ * Eg: ```typescript
314
+ * validationConfig = {
315
+ * 'passwords.password': ['passwords.confirmPassword']
316
+ * }
317
+ * ```
318
+ *
319
+ * This will trigger the updateValueAndValidity on passwords.confirmPassword every time the passwords.password gets a new value
320
+ *
321
+ * @param v
322
+ */
323
+ this.validationConfig = input(null);
324
+ this.destroyRef = inject(DestroyRef);
325
+ this.statusChanges$ = this.ngForm.form.events.pipe(filter(v => v instanceof StatusChangeEvent), map(v => v.status), distinctUntilChanged());
326
+ /**
327
+ * Fired when the form is idle. This fixes the PENDING state issues
328
+ * that are introduced due to the nature of asynchronous validations
329
+ */
330
+ this.idle$ = this.statusChanges$.pipe(filter(v => v !== 'PENDING'), distinctUntilChanged());
331
+ this.valueChanges$ = this.ngForm.form.events.pipe(filter(v => v instanceof ValueChangeEvent), map(v => v.value), map(() => mergeValuesAndRawValues(this.ngForm.form)));
332
+ /**
333
+ * Triggered as soon as the form value changes
334
+ */
335
+ this.formValueChange = this.valueChanges$.pipe(debounceTime(0) // wait until all form elements are rendered
336
+ );
337
+ this.dirtyChanges$ = this.ngForm.form.events.pipe(filter(v => v instanceof PristineChangeEvent), map(v => !v.pristine), distinctUntilChanged());
338
+ /**
339
+ * Triggered as soon as the form becomes dirty
340
+ */
341
+ this.dirtyChange = this.dirtyChanges$;
342
+ this.validChanges$ = this.statusChanges$.pipe(filter(e => e === 'VALID' || e === 'INVALID'), map(v => v === 'VALID'), switchMap((v) => this.idle$), map(() => this.ngForm?.form.valid), distinctUntilChanged());
343
+ /**
344
+ * Triggered When the form becomes valid but waits until the form is idle
345
+ */
346
+ this.validChange = this.validChanges$;
347
+ /**
348
+ * Emits an object with all the errors of the form
349
+ */
350
+ this.errorsChange = this.valueChanges$.pipe(switchMap(() => this.idle$), map(() => getAllFormErrors(this.ngForm.form)));
351
+ /**
352
+ * Used to debounce formValues to make sure vest isn't triggered all the time
353
+ */
354
+ this.formValueCache = {};
355
+ // When the validation config changes
356
+ // Listen to changes of the left-side of the config and trigger the updateValueAndValidity
357
+ // function on the dependant controls or groups at the right-side of the config
358
+ toObservable(this.validationConfig)
359
+ .pipe(filter((conf) => !!conf), switchMap((conf) => {
360
+ if (!conf) {
361
+ return of(null);
362
+ }
363
+ const streams = Object.keys(conf).map((key) => {
364
+ return this.ngForm?.form.get(key)?.valueChanges.pipe(
365
+ // wait until the form is idle
366
+ switchMap(() => this.idle$), map(() => this.ngForm?.form.get(key)?.value), takeUntilDestroyed(this.destroyRef), tap((v) => {
367
+ conf[key]?.forEach((path) => {
368
+ this.ngForm?.form.get(path)?.updateValueAndValidity({
369
+ onlySelf: true,
370
+ emitEvent: true
371
+ });
372
+ });
373
+ }));
374
+ });
375
+ return zip(streams);
376
+ }))
377
+ .subscribe();
378
+ /**
379
+ * Trigger shape validations if the form gets updated
380
+ * This is how we can throw run-time errors
381
+ */
382
+ this.valueChanges$.pipe(switchMap((v) => this.idle$.pipe(map(() => v))), takeUntilDestroyed(this.destroyRef)).subscribe(v => {
383
+ if (this.formShape()) {
384
+ validateShape(v, this.formShape());
385
+ }
386
+ });
387
+ /**
388
+ * Mark all the fields as touched when the form is submitted
389
+ */
390
+ this.ngForm.ngSubmit.subscribe(() => {
391
+ this.ngForm.form.markAllAsTouched();
392
+ });
393
+ }
394
+ /**
395
+ * This will feed the formValueCache, debounce it till the next tick
396
+ * and create an asynchronous validator that runs a vest suite
397
+ * @param field
398
+ * @param model
399
+ * @param suite
400
+ * @returns an asynchronous validator function
401
+ */
402
+ createAsyncValidator(field) {
403
+ if (!this.suite()) {
404
+ return () => of(null);
405
+ }
406
+ return (value) => {
407
+ if (!this.formValue()) {
408
+ return of(null);
409
+ }
410
+ const mod = cloneDeep(this.formValue());
411
+ set(mod, field, value); // Update the property with path
412
+ if (!this.formValueCache[field]) {
413
+ this.formValueCache[field] = {
414
+ sub$$: new ReplaySubject(1), // Keep track of the last model
415
+ };
416
+ this.formValueCache[field].debounced = this.formValueCache[field].sub$$.pipe(debounceTime(0));
417
+ }
418
+ // Next the latest model in the cache for a certain field
419
+ this.formValueCache[field].sub$$.next(mod);
420
+ return this.formValueCache[field].debounced.pipe(
421
+ // When debounced, take the latest value and perform the asynchronous vest validation
422
+ take(1), switchMap(() => {
423
+ return new Observable((observer) => {
424
+ this.suite()(mod, field).done((result) => {
425
+ const errors = result.getErrors()[field];
426
+ observer.next((errors ? { error: errors[0], errors } : null));
427
+ observer.complete();
428
+ });
429
+ });
430
+ }), takeUntilDestroyed(this.destroyRef));
431
+ };
432
+ }
433
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
434
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: FormDirective, isStandalone: true, selector: "form[scVestForm]", inputs: { formValue: { classPropertyName: "formValue", publicName: "formValue", isSignal: true, isRequired: false, transformFunction: null }, suite: { classPropertyName: "suite", publicName: "suite", isSignal: true, isRequired: false, transformFunction: null }, formShape: { classPropertyName: "formShape", publicName: "formShape", isSignal: true, isRequired: false, transformFunction: null }, validationConfig: { classPropertyName: "validationConfig", publicName: "validationConfig", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formValueChange: "formValueChange", dirtyChange: "dirtyChange", validChange: "validChange", errorsChange: "errorsChange" }, ngImport: i0 }); }
435
+ }
436
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormDirective, decorators: [{
437
+ type: Directive,
438
+ args: [{
439
+ selector: 'form[scVestForm]',
440
+ standalone: true,
441
+ }]
442
+ }], ctorParameters: () => [], propDecorators: { formValueChange: [{
443
+ type: Output
444
+ }], dirtyChange: [{
445
+ type: Output
446
+ }], validChange: [{
447
+ type: Output
448
+ }], errorsChange: [{
449
+ type: Output
450
+ }] } });
451
+
452
+ class ControlWrapperComponent {
453
+ constructor() {
454
+ this.cdRef = inject(ChangeDetectorRef);
455
+ this.formDirective = inject(FormDirective);
456
+ this.destroyRef = inject(DestroyRef);
457
+ this.ngModelGroup = inject(NgModelGroup, {
458
+ optional: true,
459
+ self: true,
460
+ });
461
+ }
462
+ get control() {
463
+ return this.ngModelGroup ? this.ngModelGroup.control : this.ngModel?.control;
464
+ }
465
+ get invalid() {
466
+ return this.control?.touched && this.errors;
467
+ }
468
+ get errors() {
469
+ if (this.control?.pending) {
470
+ return this.previousError;
471
+ }
472
+ else {
473
+ this.previousError = this.control?.errors?.['errors'];
474
+ }
475
+ return this.control?.errors?.['errors'];
476
+ }
477
+ ngAfterViewInit() {
478
+ // Wait until the form is idle
479
+ // Then, listen to all events of the ngModelGroup or ngModel
480
+ // and mark the component and its ancestors as dirty
481
+ // This allows us to use the OnPush ChangeDetection Strategy
482
+ this.formDirective.idle$
483
+ .pipe(switchMap(() => this.ngModelGroup?.control?.events || of(null)), mergeWith(this.control?.events || of(null)), takeUntilDestroyed(this.destroyRef))
484
+ .subscribe(() => {
485
+ this.cdRef.markForCheck();
486
+ });
487
+ }
488
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
489
+ 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 }); }
490
+ }
491
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
492
+ type: Component,
493
+ 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" }]
494
+ }], propDecorators: { ngModel: [{
495
+ type: ContentChild,
496
+ args: [NgModel]
497
+ }], invalid: [{
498
+ type: HostBinding,
499
+ args: ['class.sc-control-wrapper--invalid']
500
+ }] } });
501
+
502
+ /**
503
+ * Hooks into the ngModel selector and triggers an asynchronous validation for a form model
504
+ * It will use a vest suite behind the scenes
505
+ */
506
+ class FormModelDirective {
507
+ constructor() {
508
+ this.formDirective = inject(FormDirective);
509
+ }
510
+ validate(control) {
511
+ const { ngForm, suite, formValue } = this.formDirective;
512
+ const field = getFormControlField(ngForm.control, control);
513
+ return this.formDirective.createAsyncValidator(field)(control.getRawValue());
514
+ }
515
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
516
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.1", type: FormModelDirective, isStandalone: true, selector: "[ngModel]", providers: [
517
+ { provide: NG_ASYNC_VALIDATORS, useExisting: FormModelDirective, multi: true },
518
+ ], ngImport: i0 }); }
519
+ }
520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelDirective, decorators: [{
521
+ type: Directive,
522
+ args: [{
523
+ selector: '[ngModel]',
524
+ standalone: true,
525
+ providers: [
526
+ { provide: NG_ASYNC_VALIDATORS, useExisting: FormModelDirective, multi: true },
527
+ ],
528
+ }]
529
+ }] });
530
+
531
+ /**
532
+ * Hooks into the ngModelGroup selector and triggers an asynchronous validation for a form group
533
+ * It will use a vest suite behind the scenes
534
+ */
535
+ class FormModelGroupDirective {
536
+ constructor() {
537
+ this.formDirective = inject(FormDirective);
538
+ }
539
+ validate(control) {
540
+ const { ngForm } = this.formDirective;
541
+ const field = getFormGroupField(ngForm.control, control);
542
+ return this.formDirective.createAsyncValidator(field)(control.value);
543
+ }
544
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
545
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.1", type: FormModelGroupDirective, isStandalone: true, selector: "[ngModelGroup]", providers: [
546
+ { provide: NG_ASYNC_VALIDATORS, useExisting: FormModelGroupDirective, multi: true },
547
+ ], ngImport: i0 }); }
548
+ }
549
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelGroupDirective, decorators: [{
550
+ type: Directive,
551
+ args: [{
552
+ selector: '[ngModelGroup]',
553
+ standalone: true,
554
+ providers: [
555
+ { provide: NG_ASYNC_VALIDATORS, useExisting: FormModelGroupDirective, multi: true },
556
+ ],
557
+ }]
558
+ }] });
559
+
560
+ /**
561
+ * This is borrowed from [https://github.com/wardbell/ngc-validate/blob/main/src/app/core/form-container-view-provider.ts](https://github.com/wardbell/ngc-validate/blob/main/src/app/core/form-container-view-provider.ts)
562
+ * Thank you so much Ward Bell for your effort!:
563
+ *
564
+ * Provide a ControlContainer to a form component from the
565
+ * nearest parent NgModelGroup (preferred) or NgForm.
566
+ *
567
+ * Required for Reactive Forms as well (unless you write CVA)
568
+ *
569
+ * @example
570
+ * ```
571
+ * @Component({
572
+ * ...
573
+ * viewProviders[ formViewProvider ]
574
+ * })
575
+ * ```
576
+ * @see Kara's AngularConnect 2017 talk: https://youtu.be/CD_t3m2WMM8?t=1826
577
+ *
578
+ * Without this provider
579
+ * - Controls are not registered with parent NgForm or NgModelGroup
580
+ * - Form-level flags say "untouched" and "valid"
581
+ * - No form-level validation roll-up
582
+ * - Controls still validate, update model, and update their statuses
583
+ * - If within NgForm, no compiler error because ControlContainer is optional for ngModel
584
+ *
585
+ * Note: if the SubForm Component that uses this Provider
586
+ * is not within a Form or NgModelGroup, the provider returns `null`
587
+ * resulting in an error, something like
588
+ * ```
589
+ * preview-fef3604083950c709c52b.js:1 ERROR Error:
590
+ * ngModelGroup cannot be used with a parent formGroup directive.
591
+ *```
592
+ */
593
+ const formViewProvider = {
594
+ provide: ControlContainer,
595
+ useFactory: _formViewProviderFactory,
596
+ deps: [
597
+ [new Optional(), NgForm],
598
+ [new Optional(), NgModelGroup]
599
+ ]
600
+ };
601
+ function _formViewProviderFactory(ngForm, ngModelGroup) {
602
+ return ngModelGroup || ngForm || null;
603
+ }
604
+ /**
605
+ * The providers we need in every child component that holds an ngModelGroup
606
+ */
607
+ const vestFormsViewProviders = [
608
+ { provide: ControlContainer, useExisting: NgForm },
609
+ formViewProvider // very important if we want nested components with ngModelGroup
610
+ ];
611
+ /**
612
+ * Exports all the stuff we need to use the template driven forms
613
+ */
614
+ const vestForms = [ValidateRootFormDirective, ControlWrapperComponent, FormDirective, FormsModule, FormModelDirective, FormModelGroupDirective];
615
+
616
+ function arrayToObject(arr) {
617
+ return arr.reduce((acc, value, index) => ({ ...acc, [index]: value }), {});
618
+ }
619
+
620
+ /*
621
+ * Public API Surface of ngx-vest-forms
622
+ */
623
+
624
+ /**
625
+ * Generated bundle index. Do not edit.
626
+ */
627
+
628
+ export { ControlWrapperComponent, FormDirective, FormModelDirective, FormModelGroupDirective, ROOT_FORM, ShapeMismatchError, ValidateRootFormDirective, arrayToObject, cloneDeep, getAllFormErrors, getFormControlField, getFormGroupField, mergeValuesAndRawValues, set, validateShape, vestForms, vestFormsViewProviders };
629
+ //# sourceMappingURL=ngx-vest-forms.mjs.map