ngx-vest-forms 1.3.0 → 1.4.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/fesm2022/ngx-vest-forms.mjs +584 -113
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +186 -29
- package/package.json +1 -1
|
@@ -1,9 +1,322 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import { FormGroup, FormArray, NG_ASYNC_VALIDATORS,
|
|
4
|
-
import { Subject, of, ReplaySubject, debounceTime, take, switchMap, Observable, takeUntil, filter, map, distinctUntilChanged, startWith, tap, finalize,
|
|
2
|
+
import { contentChild, inject, Injector, computed, signal, effect, afterEveryRender, Directive, InjectionToken, input, ChangeDetectionStrategy, Component, isDevMode, DestroyRef, Optional } from '@angular/core';
|
|
3
|
+
import { NgModel, NgModelGroup, NgForm, FormGroup, FormArray, NG_ASYNC_VALIDATORS, StatusChangeEvent, ValueChangeEvent, PristineChangeEvent, ControlContainer, FormsModule } from '@angular/forms';
|
|
4
|
+
import { Subject, of, ReplaySubject, debounceTime, take, switchMap, Observable, takeUntil, filter, map, distinctUntilChanged, startWith, tap, finalize, catchError, from } from 'rxjs';
|
|
5
5
|
import { outputFromObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
|
|
7
|
+
function getInitialFormControlState() {
|
|
8
|
+
return {
|
|
9
|
+
status: 'INVALID',
|
|
10
|
+
isValid: false,
|
|
11
|
+
isInvalid: true,
|
|
12
|
+
isPending: false,
|
|
13
|
+
isDisabled: false,
|
|
14
|
+
isTouched: false,
|
|
15
|
+
isDirty: false,
|
|
16
|
+
isPristine: true,
|
|
17
|
+
errors: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
class FormControlStateDirective {
|
|
21
|
+
#hostNgModel;
|
|
22
|
+
#hostNgModelGroup;
|
|
23
|
+
#injector;
|
|
24
|
+
/**
|
|
25
|
+
* Computed signal for the active control (NgModel or NgModelGroup)
|
|
26
|
+
*/
|
|
27
|
+
#activeControl;
|
|
28
|
+
/**
|
|
29
|
+
* Internal signal for robust touched/dirty tracking (syncs after every render)
|
|
30
|
+
*/
|
|
31
|
+
#interactionState;
|
|
32
|
+
/**
|
|
33
|
+
* Internal signal for control state (updated reactively)
|
|
34
|
+
*/
|
|
35
|
+
#controlStateSignal;
|
|
36
|
+
constructor() {
|
|
37
|
+
this.contentNgModel = contentChild(NgModel, ...(ngDevMode ? [{ debugName: "contentNgModel" }] : []));
|
|
38
|
+
this.contentNgModelGroup = contentChild(NgModelGroup, ...(ngDevMode ? [{ debugName: "contentNgModelGroup" }] : []));
|
|
39
|
+
this.#hostNgModel = inject(NgModel, {
|
|
40
|
+
self: true,
|
|
41
|
+
optional: true,
|
|
42
|
+
});
|
|
43
|
+
this.#hostNgModelGroup = inject(NgModelGroup, {
|
|
44
|
+
self: true,
|
|
45
|
+
optional: true,
|
|
46
|
+
});
|
|
47
|
+
this.#injector = inject(Injector);
|
|
48
|
+
/**
|
|
49
|
+
* Computed signal for the active control (NgModel or NgModelGroup)
|
|
50
|
+
*/
|
|
51
|
+
this.#activeControl = computed(() => {
|
|
52
|
+
return (this.#hostNgModel ||
|
|
53
|
+
this.#hostNgModelGroup ||
|
|
54
|
+
this.contentNgModel() ||
|
|
55
|
+
this.contentNgModelGroup() ||
|
|
56
|
+
null);
|
|
57
|
+
}, ...(ngDevMode ? [{ debugName: "#activeControl" }] : []));
|
|
58
|
+
/**
|
|
59
|
+
* Internal signal for robust touched/dirty tracking (syncs after every render)
|
|
60
|
+
*/
|
|
61
|
+
this.#interactionState = signal({
|
|
62
|
+
isTouched: false,
|
|
63
|
+
isDirty: false,
|
|
64
|
+
}, ...(ngDevMode ? [{ debugName: "#interactionState" }] : []));
|
|
65
|
+
/**
|
|
66
|
+
* Internal signal for control state (updated reactively)
|
|
67
|
+
*/
|
|
68
|
+
this.#controlStateSignal = signal(getInitialFormControlState(), ...(ngDevMode ? [{ debugName: "#controlStateSignal" }] : []));
|
|
69
|
+
/**
|
|
70
|
+
* Main control state computed signal (merges robust touched/dirty)
|
|
71
|
+
*/
|
|
72
|
+
this.controlState = computed(() => this.#controlStateSignal(), ...(ngDevMode ? [{ debugName: "controlState" }] : []));
|
|
73
|
+
/**
|
|
74
|
+
* Extracts error messages from Angular/Vest errors (recursively flattens)
|
|
75
|
+
*/
|
|
76
|
+
this.errorMessages = computed(() => {
|
|
77
|
+
const state = this.controlState();
|
|
78
|
+
if (!state?.errors)
|
|
79
|
+
return [];
|
|
80
|
+
// Vest errors are stored in the 'errors' property as an array
|
|
81
|
+
const vestErrors = state.errors['errors'];
|
|
82
|
+
if (Array.isArray(vestErrors)) {
|
|
83
|
+
return vestErrors;
|
|
84
|
+
}
|
|
85
|
+
// Fallback to flattened Angular error keys
|
|
86
|
+
return this.#flattenAngularErrors(state.errors);
|
|
87
|
+
}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : []));
|
|
88
|
+
/**
|
|
89
|
+
* ADVANCED: updateOn strategy (change/blur/submit) if available
|
|
90
|
+
*/
|
|
91
|
+
this.updateOn = computed(() => {
|
|
92
|
+
const ngModel = this.contentNgModel() || this.#hostNgModel;
|
|
93
|
+
// Angular's NgModel.options?.updateOn
|
|
94
|
+
return ngModel?.options?.updateOn ?? 'change';
|
|
95
|
+
}, ...(ngDevMode ? [{ debugName: "updateOn" }] : []));
|
|
96
|
+
/**
|
|
97
|
+
* ADVANCED: Composite/derived signals for advanced error display logic
|
|
98
|
+
*/
|
|
99
|
+
this.isValidTouched = computed(() => this.isValid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isValidTouched" }] : []));
|
|
100
|
+
this.isInvalidTouched = computed(() => this.isInvalid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isInvalidTouched" }] : []));
|
|
101
|
+
this.shouldShowErrors = computed(() => this.isInvalid() && this.isTouched() && !this.isPending(), ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : []));
|
|
102
|
+
/**
|
|
103
|
+
* Extracts warning messages from Vest validation results (robust)
|
|
104
|
+
*/
|
|
105
|
+
this.warningMessages = computed(() => {
|
|
106
|
+
const state = this.controlState();
|
|
107
|
+
if (!state?.errors)
|
|
108
|
+
return [];
|
|
109
|
+
const warnings = state.errors['warnings'];
|
|
110
|
+
if (Array.isArray(warnings)) {
|
|
111
|
+
return warnings;
|
|
112
|
+
}
|
|
113
|
+
// Optionally, flatten nested warnings if needed in future
|
|
114
|
+
return [];
|
|
115
|
+
}, ...(ngDevMode ? [{ debugName: "warningMessages" }] : []));
|
|
116
|
+
/**
|
|
117
|
+
* Whether async validation is in progress
|
|
118
|
+
*/
|
|
119
|
+
this.hasPendingValidation = computed(() => !!this.controlState().isPending, ...(ngDevMode ? [{ debugName: "hasPendingValidation" }] : []));
|
|
120
|
+
/**
|
|
121
|
+
* Convenience signals for common state checks
|
|
122
|
+
*/
|
|
123
|
+
this.isValid = computed(() => this.controlState().isValid || false, ...(ngDevMode ? [{ debugName: "isValid" }] : []));
|
|
124
|
+
this.isInvalid = computed(() => this.controlState().isInvalid || false, ...(ngDevMode ? [{ debugName: "isInvalid" }] : []));
|
|
125
|
+
this.isPending = computed(() => this.controlState().isPending || false, ...(ngDevMode ? [{ debugName: "isPending" }] : []));
|
|
126
|
+
this.isTouched = computed(() => this.controlState().isTouched || false, ...(ngDevMode ? [{ debugName: "isTouched" }] : []));
|
|
127
|
+
this.isDirty = computed(() => this.controlState().isDirty || false, ...(ngDevMode ? [{ debugName: "isDirty" }] : []));
|
|
128
|
+
this.isPristine = computed(() => this.controlState().isPristine || false, ...(ngDevMode ? [{ debugName: "isPristine" }] : []));
|
|
129
|
+
this.isDisabled = computed(() => this.controlState().isDisabled || false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
130
|
+
this.hasErrors = computed(() => this.errorMessages().length > 0, ...(ngDevMode ? [{ debugName: "hasErrors" }] : []));
|
|
131
|
+
// Update control state reactively
|
|
132
|
+
effect(() => {
|
|
133
|
+
const control = this.#activeControl();
|
|
134
|
+
const interaction = this.#interactionState();
|
|
135
|
+
if (!control) {
|
|
136
|
+
this.#controlStateSignal.set(getInitialFormControlState());
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Listen to control changes
|
|
140
|
+
const sub = control.control?.statusChanges?.subscribe(() => {
|
|
141
|
+
const { status, valid, invalid, pending, disabled, pristine, errors } = control;
|
|
142
|
+
this.#controlStateSignal.set({
|
|
143
|
+
status,
|
|
144
|
+
isValid: valid,
|
|
145
|
+
isInvalid: invalid,
|
|
146
|
+
isPending: pending,
|
|
147
|
+
isDisabled: disabled,
|
|
148
|
+
isTouched: interaction.isTouched,
|
|
149
|
+
isDirty: interaction.isDirty,
|
|
150
|
+
isPristine: pristine,
|
|
151
|
+
errors,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
// Initial update
|
|
155
|
+
const { status, valid, invalid, pending, disabled, pristine, errors } = control;
|
|
156
|
+
this.#controlStateSignal.set({
|
|
157
|
+
status,
|
|
158
|
+
isValid: valid,
|
|
159
|
+
isInvalid: invalid,
|
|
160
|
+
isPending: pending,
|
|
161
|
+
isDisabled: disabled,
|
|
162
|
+
isTouched: interaction.isTouched,
|
|
163
|
+
isDirty: interaction.isDirty,
|
|
164
|
+
isPristine: pristine,
|
|
165
|
+
errors,
|
|
166
|
+
});
|
|
167
|
+
return () => sub?.unsubscribe();
|
|
168
|
+
});
|
|
169
|
+
// Robustly sync touched/dirty after every render (Angular 18+ best practice)
|
|
170
|
+
afterEveryRender(() => {
|
|
171
|
+
const control = this.#activeControl();
|
|
172
|
+
if (control) {
|
|
173
|
+
const current = this.#interactionState();
|
|
174
|
+
const newTouched = control.touched ?? false;
|
|
175
|
+
const newDirty = control.dirty ?? false;
|
|
176
|
+
if (newTouched !== current.isTouched ||
|
|
177
|
+
newDirty !== current.isDirty) {
|
|
178
|
+
this.#interactionState.set({
|
|
179
|
+
isTouched: newTouched,
|
|
180
|
+
isDirty: newDirty,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, { injector: this.#injector });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Recursively flattens Angular error objects into an array of error keys.
|
|
188
|
+
*/
|
|
189
|
+
#flattenAngularErrors(errors) {
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const key of Object.keys(errors)) {
|
|
192
|
+
const value = errors[key];
|
|
193
|
+
if (typeof value === 'object' && value !== null) {
|
|
194
|
+
result.push(...this.#flattenAngularErrors(value));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
result.push(key);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormControlStateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
203
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "20.3.1", type: FormControlStateDirective, isStandalone: true, selector: "[formControlState]", queries: [{ propertyName: "contentNgModel", first: true, predicate: NgModel, descendants: true, isSignal: true }, { propertyName: "contentNgModelGroup", first: true, predicate: NgModelGroup, descendants: true, isSignal: true }], exportAs: ["formControlState"], ngImport: i0 }); }
|
|
204
|
+
}
|
|
205
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormControlStateDirective, decorators: [{
|
|
206
|
+
type: Directive,
|
|
207
|
+
args: [{
|
|
208
|
+
selector: '[formControlState]',
|
|
209
|
+
exportAs: 'formControlState',
|
|
210
|
+
}]
|
|
211
|
+
}], ctorParameters: () => [] });
|
|
212
|
+
|
|
213
|
+
const SC_ERROR_DISPLAY_MODE_TOKEN = new InjectionToken('SC_ERROR_DISPLAY_MODE_TOKEN', {
|
|
214
|
+
providedIn: 'root',
|
|
215
|
+
factory: () => 'on-blur-or-submit',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const SC_ERROR_DISPLAY_MODE_DEFAULT = 'on-blur-or-submit';
|
|
219
|
+
class FormErrorDisplayDirective {
|
|
220
|
+
#formControlState;
|
|
221
|
+
// Optionally inject NgForm for form submission tracking
|
|
222
|
+
#ngForm;
|
|
223
|
+
constructor() {
|
|
224
|
+
this.#formControlState = inject(FormControlStateDirective);
|
|
225
|
+
// Optionally inject NgForm for form submission tracking
|
|
226
|
+
this.#ngForm = inject(NgForm, { optional: true });
|
|
227
|
+
// Use DI token for global default, fallback to hardcoded default
|
|
228
|
+
this.errorDisplayMode = input(inject(SC_ERROR_DISPLAY_MODE_TOKEN, { optional: true }) ??
|
|
229
|
+
SC_ERROR_DISPLAY_MODE_DEFAULT, ...(ngDevMode ? [{ debugName: "errorDisplayMode" }] : []));
|
|
230
|
+
// Expose state signals from FormControlStateDirective
|
|
231
|
+
this.controlState = this.#formControlState.controlState;
|
|
232
|
+
this.errorMessages = this.#formControlState.errorMessages;
|
|
233
|
+
this.warningMessages = this.#formControlState.warningMessages;
|
|
234
|
+
this.hasPendingValidation = this.#formControlState.hasPendingValidation;
|
|
235
|
+
this.isTouched = this.#formControlState.isTouched;
|
|
236
|
+
this.isDirty = this.#formControlState.isDirty;
|
|
237
|
+
this.isValid = this.#formControlState.isValid;
|
|
238
|
+
this.isInvalid = this.#formControlState.isInvalid;
|
|
239
|
+
/**
|
|
240
|
+
* Expose updateOn and formSubmitted as public signals for advanced consumers.
|
|
241
|
+
* updateOn: The ngModelOptions.updateOn value for the control (change/blur/submit)
|
|
242
|
+
* formSubmitted: true after the form is submitted (if NgForm is present)
|
|
243
|
+
*/
|
|
244
|
+
this.updateOn = this.#formControlState.updateOn;
|
|
245
|
+
// formSubmitted signal is already public
|
|
246
|
+
// Signal for form submission state (true after submit)
|
|
247
|
+
this.formSubmitted = signal(false, ...(ngDevMode ? [{ debugName: "formSubmitted" }] : []));
|
|
248
|
+
/**
|
|
249
|
+
* Determines if errors should be shown based on the specified display mode
|
|
250
|
+
* and the control's state (touched/submitted).
|
|
251
|
+
*/
|
|
252
|
+
this.shouldShowErrors = computed(() => {
|
|
253
|
+
const mode = this.errorDisplayMode();
|
|
254
|
+
const isTouched = this.isTouched();
|
|
255
|
+
const hasErrors = this.errorMessages().length > 0;
|
|
256
|
+
const updateOn = this.updateOn();
|
|
257
|
+
const formSubmitted = this.formSubmitted();
|
|
258
|
+
// Always only show errors after submit if updateOn is 'submit'
|
|
259
|
+
if (updateOn === 'submit') {
|
|
260
|
+
return !!(formSubmitted && hasErrors);
|
|
261
|
+
}
|
|
262
|
+
// on-blur: show errors after blur (touch)
|
|
263
|
+
if (mode === 'on-blur') {
|
|
264
|
+
return !!(isTouched && hasErrors);
|
|
265
|
+
}
|
|
266
|
+
// on-submit: show errors after submit
|
|
267
|
+
if (mode === 'on-submit') {
|
|
268
|
+
return !!(formSubmitted && hasErrors);
|
|
269
|
+
}
|
|
270
|
+
// on-blur-or-submit: show errors after blur (touch) or submit
|
|
271
|
+
return !!((isTouched || formSubmitted) && hasErrors);
|
|
272
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : []));
|
|
273
|
+
/**
|
|
274
|
+
* Errors to display (filtered for pending state)
|
|
275
|
+
*/
|
|
276
|
+
this.errors = computed(() => {
|
|
277
|
+
if (this.hasPendingValidation())
|
|
278
|
+
return [];
|
|
279
|
+
return this.errorMessages();
|
|
280
|
+
}, ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
281
|
+
/**
|
|
282
|
+
* Warnings to display (filtered for pending state)
|
|
283
|
+
*/
|
|
284
|
+
this.warnings = computed(() => {
|
|
285
|
+
if (this.hasPendingValidation())
|
|
286
|
+
return [];
|
|
287
|
+
return this.warningMessages();
|
|
288
|
+
}, ...(ngDevMode ? [{ debugName: "warnings" }] : []));
|
|
289
|
+
/**
|
|
290
|
+
* Whether the control is currently being validated (pending)
|
|
291
|
+
*/
|
|
292
|
+
this.isPending = computed(() => this.hasPendingValidation(), ...(ngDevMode ? [{ debugName: "isPending" }] : []));
|
|
293
|
+
// Listen for form submit event if NgForm is present
|
|
294
|
+
if (this.#ngForm?.ngSubmit) {
|
|
295
|
+
this.#ngForm.ngSubmit.subscribe(() => {
|
|
296
|
+
this.formSubmitted.set(true);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// Warn about problematic combinations of updateOn and errorDisplayMode
|
|
300
|
+
effect(() => {
|
|
301
|
+
const mode = this.errorDisplayMode();
|
|
302
|
+
const updateOn = this.updateOn();
|
|
303
|
+
if (updateOn === 'submit' && mode === 'on-blur') {
|
|
304
|
+
console.warn('[ngx-vest-forms] Potential UX issue: errorDisplayMode is "on-blur" but updateOn is "submit". Errors will only show after form submission, not after blur.');
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormErrorDisplayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
309
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.1", type: FormErrorDisplayDirective, isStandalone: true, selector: "[formErrorDisplay]", inputs: { errorDisplayMode: { classPropertyName: "errorDisplayMode", publicName: "errorDisplayMode", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["formErrorDisplay"], hostDirectives: [{ directive: FormControlStateDirective }], ngImport: i0 }); }
|
|
310
|
+
}
|
|
311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormErrorDisplayDirective, decorators: [{
|
|
312
|
+
type: Directive,
|
|
313
|
+
args: [{
|
|
314
|
+
selector: '[formErrorDisplay]',
|
|
315
|
+
exportAs: 'formErrorDisplay',
|
|
316
|
+
hostDirectives: [FormControlStateDirective],
|
|
317
|
+
}]
|
|
318
|
+
}], ctorParameters: () => [] });
|
|
319
|
+
|
|
7
320
|
const ROOT_FORM = 'rootForm';
|
|
8
321
|
/**
|
|
9
322
|
* Debounce time in milliseconds for validation config dependency triggering.
|
|
@@ -258,7 +571,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
258
571
|
type: Directive,
|
|
259
572
|
args: [{
|
|
260
573
|
selector: 'form[validateRootForm][formValue][suite]',
|
|
261
|
-
standalone: true,
|
|
262
574
|
providers: [
|
|
263
575
|
{
|
|
264
576
|
provide: NG_ASYNC_VALIDATORS,
|
|
@@ -269,6 +581,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
269
581
|
}]
|
|
270
582
|
}] });
|
|
271
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Accessible ScControlWrapper
|
|
586
|
+
*
|
|
587
|
+
* Usage:
|
|
588
|
+
* - Wrap any form element with `sc-control-wrapper` or `[scControlWrapper]` (or `[sc-control-wrapper]` for legacy) that contains an `ngModel` or `ngModelGroup`.
|
|
589
|
+
* - Errors and warnings are shown when the control is invalid and touched, after form submit, or both, depending on the error display mode.
|
|
590
|
+
* - Pending state is shown with a spinner and aria-busy when async validation is running.
|
|
591
|
+
* - No manual error/warning/pending signal management is needed in your form components.
|
|
592
|
+
*
|
|
593
|
+
* Error & Warning Display Behavior:
|
|
594
|
+
* - The error display mode can be configured globally using the SC_ERROR_DISPLAY_MODE_TOKEN injection token (import from core), or per instance using the `errorDisplayMode` input on FormErrorDisplayDirective (which this component uses as a hostDirective).
|
|
595
|
+
* - Possible values: 'on-blur' | 'on-submit' | 'on-blur-or-submit' (default: 'on-blur-or-submit')
|
|
596
|
+
*
|
|
597
|
+
* Example (per instance):
|
|
598
|
+
* <div ngxControlWrapper>
|
|
599
|
+
* <label>
|
|
600
|
+
* <span>First name</span>
|
|
601
|
+
* <input type="text" name="firstName" [ngModel]="formValue().firstName" />
|
|
602
|
+
* </label>
|
|
603
|
+
* </div>
|
|
604
|
+
* /// To customize errorDisplayMode for this instance, use the errorDisplayMode input.
|
|
605
|
+
*
|
|
606
|
+
* Example (with warnings and pending):
|
|
607
|
+
* <sc-control-wrapper>
|
|
608
|
+
* <input name="username" ngModel />
|
|
609
|
+
* </sc-control-wrapper>
|
|
610
|
+
* /// If async validation is running, a spinner and 'Validating…' will be shown.
|
|
611
|
+
* /// If Vest warnings are present, they will be shown below errors.
|
|
612
|
+
*
|
|
613
|
+
* Example (global config):
|
|
614
|
+
* import { provide } from '@angular/core';
|
|
615
|
+
* import { SC_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
|
|
616
|
+
* @Component({
|
|
617
|
+
* providers: [
|
|
618
|
+
* provide(SC_ERROR_DISPLAY_MODE_TOKEN, { useValue: 'submit' })
|
|
619
|
+
* ]
|
|
620
|
+
* })
|
|
621
|
+
* export class MyComponent {}
|
|
622
|
+
*
|
|
623
|
+
* Best Practices:
|
|
624
|
+
* - Use for every input or group in your forms.
|
|
625
|
+
* - Do not manually display errors for individual fields; rely on this wrapper.
|
|
626
|
+
*/
|
|
627
|
+
class ControlWrapperComponent {
|
|
628
|
+
constructor() {
|
|
629
|
+
this.errorDisplay = inject(FormErrorDisplayDirective, {
|
|
630
|
+
self: true,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
634
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: ControlWrapperComponent, isStandalone: true, selector: "sc-control-wrapper, [scControlWrapper], [sc-control-wrapper]", host: { properties: { "class.sc-control-wrapper--invalid": "errorDisplay.shouldShowErrors()", "attr.aria-busy": "errorDisplay.isPending() ? 'true' : null" }, classAttribute: "sc-control-wrapper" }, hostDirectives: [{ directive: FormErrorDisplayDirective, inputs: ["errorDisplayMode", "errorDisplayMode"] }], ngImport: i0, template: "<div class=\"ngx-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content />\n </div>\n\n @if (errorDisplay.shouldShowErrors()) {\n <div class=\"text-sm text-red-600\" role=\"alert\" aria-live=\"polite\">\n <ul>\n @for (error of errorDisplay.errors(); track error) {\n <li>{{ error.message || error }}</li>\n }\n </ul>\n </div>\n }\n @if (errorDisplay.warnings().length > 0) {\n <div class=\"text-sm text-yellow-700\" role=\"status\" aria-live=\"polite\">\n <ul>\n @for (warn of errorDisplay.warnings(); track warn) {\n <li>{{ warn }}</li>\n }\n </ul>\n </div>\n }\n @if (errorDisplay.isPending()) {\n <div class=\"flex items-center gap-1 text-xs text-gray-500\" aria-busy=\"true\">\n <span\n class=\"inline-block h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent\"\n ></span>\n Validating\u2026\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
635
|
+
}
|
|
636
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
|
|
637
|
+
type: Component,
|
|
638
|
+
args: [{ selector: 'sc-control-wrapper, [scControlWrapper], [sc-control-wrapper]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
639
|
+
class: 'sc-control-wrapper',
|
|
640
|
+
'[class.sc-control-wrapper--invalid]': 'errorDisplay.shouldShowErrors()',
|
|
641
|
+
'[attr.aria-busy]': "errorDisplay.isPending() ? 'true' : null",
|
|
642
|
+
}, hostDirectives: [
|
|
643
|
+
{
|
|
644
|
+
directive: FormErrorDisplayDirective,
|
|
645
|
+
inputs: ['errorDisplayMode'],
|
|
646
|
+
},
|
|
647
|
+
], template: "<div class=\"ngx-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content />\n </div>\n\n @if (errorDisplay.shouldShowErrors()) {\n <div class=\"text-sm text-red-600\" role=\"alert\" aria-live=\"polite\">\n <ul>\n @for (error of errorDisplay.errors(); track error) {\n <li>{{ error.message || error }}</li>\n }\n </ul>\n </div>\n }\n @if (errorDisplay.warnings().length > 0) {\n <div class=\"text-sm text-yellow-700\" role=\"status\" aria-live=\"polite\">\n <ul>\n @for (warn of errorDisplay.warnings(); track warn) {\n <li>{{ warn }}</li>\n }\n </ul>\n </div>\n }\n @if (errorDisplay.isPending()) {\n <div class=\"flex items-center gap-1 text-xs text-gray-500\" aria-busy=\"true\">\n <span\n class=\"inline-block h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent\"\n ></span>\n Validating\u2026\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative}\n"] }]
|
|
648
|
+
}] });
|
|
649
|
+
|
|
272
650
|
/**
|
|
273
651
|
* Type guard to check if a value is a primitive type.
|
|
274
652
|
*
|
|
@@ -538,6 +916,16 @@ function validateFormValue(formValue, shape, path = '') {
|
|
|
538
916
|
}
|
|
539
917
|
|
|
540
918
|
class FormDirective {
|
|
919
|
+
/**
|
|
920
|
+
* Returns the current form state: validity and errors.
|
|
921
|
+
* Used by templates and tests as vestForm.formState().valid/errors
|
|
922
|
+
*/
|
|
923
|
+
formState() {
|
|
924
|
+
return {
|
|
925
|
+
valid: this.ngForm.form.valid,
|
|
926
|
+
errors: getAllFormErrors(this.ngForm.form),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
541
929
|
constructor() {
|
|
542
930
|
this.ngForm = inject(NgForm, { self: true, optional: false });
|
|
543
931
|
this.destroyRef = inject(DestroyRef);
|
|
@@ -613,17 +1001,21 @@ class FormDirective {
|
|
|
613
1001
|
/**
|
|
614
1002
|
* Used to debounce formValues to make sure vest isn't triggered all the time
|
|
615
1003
|
*/
|
|
1004
|
+
// Async validator cache for debouncing and single-flight per field
|
|
616
1005
|
this.formValueCache = {};
|
|
617
1006
|
this.validationInProgress = new Set();
|
|
618
1007
|
/**
|
|
619
1008
|
* Trigger shape validations if the form gets updated
|
|
620
1009
|
* This is how we can throw run-time errors
|
|
621
1010
|
*/
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1011
|
+
if (isDevMode()) {
|
|
1012
|
+
effect(() => {
|
|
1013
|
+
const v = this.formValue();
|
|
1014
|
+
if (v && this.formShape()) {
|
|
1015
|
+
validateShape(v, this.formShape());
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
627
1019
|
/**
|
|
628
1020
|
* Mark all the fields as touched when the form is submitted
|
|
629
1021
|
*/
|
|
@@ -749,35 +1141,109 @@ class FormDirective {
|
|
|
749
1141
|
* @param validationOptions
|
|
750
1142
|
* @returns an asynchronous validator function
|
|
751
1143
|
*/
|
|
1144
|
+
/**
|
|
1145
|
+
* Improved async validator: composable, debounced, robust error handling.
|
|
1146
|
+
* Compatible with v2 pattern, non-breaking.
|
|
1147
|
+
*/
|
|
752
1148
|
createAsyncValidator(field, validationOptions) {
|
|
753
|
-
if (!this.suite()) {
|
|
754
|
-
return () => of(null);
|
|
755
|
-
}
|
|
756
1149
|
return (value) => {
|
|
757
|
-
|
|
1150
|
+
const suite = this.suite();
|
|
1151
|
+
const formValue = this.formValue();
|
|
1152
|
+
if (!suite || !formValue) {
|
|
758
1153
|
return of(null);
|
|
759
1154
|
}
|
|
760
|
-
const
|
|
761
|
-
set(
|
|
1155
|
+
const candidate = cloneDeep(formValue);
|
|
1156
|
+
set(candidate, field, value);
|
|
762
1157
|
if (!this.formValueCache[field]) {
|
|
763
|
-
this.formValueCache[field] =
|
|
764
|
-
sub$$: new ReplaySubject(1), // Keep track of the last model
|
|
765
|
-
};
|
|
766
|
-
this.formValueCache[field].debounced = this.formValueCache[field].sub$$.pipe(debounceTime(validationOptions.debounceTime));
|
|
1158
|
+
this.formValueCache[field] = new ReplaySubject(1);
|
|
767
1159
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1160
|
+
const subject = this.formValueCache[field];
|
|
1161
|
+
subject.next(candidate);
|
|
1162
|
+
const debounceMs = validationOptions?.debounceTime ?? 0;
|
|
1163
|
+
return subject.pipe(debounceTime(debounceMs), take(1), switchMap((model) => {
|
|
1164
|
+
if (isDevMode()) {
|
|
1165
|
+
console.debug('[ngx-vest-forms] Running suite for field', field, 'with model', model);
|
|
1166
|
+
}
|
|
773
1167
|
return new Observable((observer) => {
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
1168
|
+
const emitResult = (vestResult) => {
|
|
1169
|
+
const pushResult = () => {
|
|
1170
|
+
try {
|
|
1171
|
+
let errors;
|
|
1172
|
+
if (typeof vestResult?.getErrors === 'function') {
|
|
1173
|
+
const direct = vestResult.getErrors(field);
|
|
1174
|
+
if (Array.isArray(direct)) {
|
|
1175
|
+
errors = direct;
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
const bag = vestResult.getErrors();
|
|
1179
|
+
errors = bag?.[field];
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (isDevMode()) {
|
|
1183
|
+
console.debug('[ngx-vest-forms] Final result errors for field', field, errors);
|
|
1184
|
+
}
|
|
1185
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
1186
|
+
observer.next({ error: errors[0], errors });
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
observer.next(null);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
observer.next({
|
|
1194
|
+
vestInternalError: error instanceof Error
|
|
1195
|
+
? error.message
|
|
1196
|
+
: 'Unknown validation error',
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
finally {
|
|
1200
|
+
observer.complete();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
if (typeof queueMicrotask === 'function') {
|
|
1204
|
+
queueMicrotask(pushResult);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
setTimeout(pushResult, 0);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
let suiteResult;
|
|
1211
|
+
try {
|
|
1212
|
+
suiteResult = suite(model, field);
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
observer.next({
|
|
1216
|
+
vestInternalError: error instanceof Error
|
|
1217
|
+
? error.message
|
|
1218
|
+
: 'Unknown validation error',
|
|
1219
|
+
});
|
|
777
1220
|
observer.complete();
|
|
778
|
-
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (isDevMode()) {
|
|
1224
|
+
console.debug('[ngx-vest-forms] Suite returned', suiteResult);
|
|
1225
|
+
}
|
|
1226
|
+
const doneFn = suiteResult?.done;
|
|
1227
|
+
if (typeof doneFn === 'function') {
|
|
1228
|
+
try {
|
|
1229
|
+
doneFn.call(suiteResult, emitResult);
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
observer.next({
|
|
1233
|
+
vestInternalError: error instanceof Error
|
|
1234
|
+
? error.message
|
|
1235
|
+
: 'Unknown validation error',
|
|
1236
|
+
});
|
|
1237
|
+
observer.complete();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
emitResult(suiteResult);
|
|
1242
|
+
}
|
|
779
1243
|
});
|
|
780
|
-
}),
|
|
1244
|
+
}), catchError((err) => of({
|
|
1245
|
+
vestInternalError: err?.message || 'Unknown validation error',
|
|
1246
|
+
})), takeUntilDestroyed(this.destroyRef));
|
|
781
1247
|
};
|
|
782
1248
|
}
|
|
783
1249
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -788,80 +1254,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
788
1254
|
args: [{
|
|
789
1255
|
selector: 'form[scVestForm]',
|
|
790
1256
|
exportAs: 'scVestForm',
|
|
791
|
-
standalone: true,
|
|
792
1257
|
}]
|
|
793
1258
|
}], ctorParameters: () => [] });
|
|
794
1259
|
|
|
795
|
-
class ControlWrapperComponent {
|
|
796
|
-
constructor() {
|
|
797
|
-
this.ngModelGroup = inject(NgModelGroup, {
|
|
798
|
-
optional: true,
|
|
799
|
-
self: true,
|
|
800
|
-
});
|
|
801
|
-
this.destroy$$ = new Subject();
|
|
802
|
-
this.cdRef = inject(ChangeDetectorRef);
|
|
803
|
-
this.formDirective = inject(FormDirective);
|
|
804
|
-
// Signal-based state management
|
|
805
|
-
this.controlState = signal({ touched: false, pending: false, errors: undefined }, ...(ngDevMode ? [{ debugName: "controlState" }] : []));
|
|
806
|
-
// Computed signal for invalid state
|
|
807
|
-
this.invalid = computed(() => {
|
|
808
|
-
const state = this.controlState();
|
|
809
|
-
return state.touched && state.errors && state.errors.length > 0;
|
|
810
|
-
}, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
811
|
-
// Computed signal for errors with pending state handling
|
|
812
|
-
this.errors = computed(() => {
|
|
813
|
-
const state = this.controlState();
|
|
814
|
-
if (state.pending) {
|
|
815
|
-
return this.previousError;
|
|
816
|
-
}
|
|
817
|
-
else {
|
|
818
|
-
this.previousError = state.errors;
|
|
819
|
-
}
|
|
820
|
-
return state.errors;
|
|
821
|
-
}, ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
822
|
-
}
|
|
823
|
-
get control() {
|
|
824
|
-
return this.ngModelGroup
|
|
825
|
-
? this.ngModelGroup.control
|
|
826
|
-
: this.ngModel?.control;
|
|
827
|
-
}
|
|
828
|
-
ngOnDestroy() {
|
|
829
|
-
this.destroy$$.next();
|
|
830
|
-
}
|
|
831
|
-
ngAfterViewInit() {
|
|
832
|
-
// Wait until the form is idle
|
|
833
|
-
// Then, listen to all events of the ngModelGroup or ngModel
|
|
834
|
-
// and update our signal-based state
|
|
835
|
-
this.formDirective.idle$
|
|
836
|
-
.pipe(switchMap(() => this.ngModelGroup?.control?.events || of(null)), mergeWith(this.control?.events || of(null)), takeUntil(this.destroy$$))
|
|
837
|
-
.subscribe(() => {
|
|
838
|
-
this.updateControlState();
|
|
839
|
-
this.cdRef.markForCheck();
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
updateControlState() {
|
|
843
|
-
const control = this.control;
|
|
844
|
-
if (control) {
|
|
845
|
-
this.controlState.set({
|
|
846
|
-
touched: control.touched,
|
|
847
|
-
pending: control.pending,
|
|
848
|
-
errors: control.errors?.['errors'],
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
853
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: ControlWrapperComponent, isStandalone: true, selector: "[sc-control-wrapper]", host: { properties: { "class.sc-control-wrapper--invalid": "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: [":host{display:block;position:relative}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
854
|
-
}
|
|
855
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
|
|
856
|
-
type: Component,
|
|
857
|
-
args: [{ selector: '[sc-control-wrapper]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
858
|
-
'[class.sc-control-wrapper--invalid]': 'invalid()',
|
|
859
|
-
}, 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: [":host{display:block;position:relative}\n"] }]
|
|
860
|
-
}], propDecorators: { ngModel: [{
|
|
861
|
-
type: ContentChild,
|
|
862
|
-
args: [NgModel]
|
|
863
|
-
}] } });
|
|
864
|
-
|
|
865
1260
|
/**
|
|
866
1261
|
* Hooks into the ngModel selector and triggers an asynchronous validation for a form model
|
|
867
1262
|
* It will use a vest suite behind the scenes
|
|
@@ -869,12 +1264,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
869
1264
|
class FormModelDirective {
|
|
870
1265
|
constructor() {
|
|
871
1266
|
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : []));
|
|
872
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* Reference to the form that needs to be validated
|
|
1269
|
+
* Injected optionally so that using ngModel outside of an ngxVestForm
|
|
1270
|
+
* does not crash the application. In that case, validation becomes a no-op.
|
|
1271
|
+
*/
|
|
1272
|
+
this.formDirective = inject(FormDirective, {
|
|
1273
|
+
optional: true,
|
|
1274
|
+
});
|
|
873
1275
|
}
|
|
874
1276
|
validate(control) {
|
|
875
|
-
|
|
1277
|
+
// Null check for control
|
|
1278
|
+
if (!control) {
|
|
1279
|
+
if (isDevMode()) {
|
|
1280
|
+
console.debug('[ngx-vest-forms] Validate called with null control in FormModelDirective.');
|
|
1281
|
+
}
|
|
1282
|
+
return of(null);
|
|
1283
|
+
}
|
|
1284
|
+
// Null check for form context
|
|
1285
|
+
const context = this.formDirective;
|
|
1286
|
+
if (!context) {
|
|
1287
|
+
if (isDevMode()) {
|
|
1288
|
+
console.debug('[ngx-vest-forms] ngModel used outside of ngxVestForm; skipping validation.');
|
|
1289
|
+
}
|
|
1290
|
+
return of(null);
|
|
1291
|
+
}
|
|
1292
|
+
const { ngForm } = context;
|
|
876
1293
|
const field = getFormControlField(ngForm.control, control);
|
|
877
|
-
|
|
1294
|
+
if (!field) {
|
|
1295
|
+
if (isDevMode()) {
|
|
1296
|
+
console.debug('[ngx-vest-forms] Could not determine field name for validation in FormModelDirective (skipping).');
|
|
1297
|
+
}
|
|
1298
|
+
return of(null);
|
|
1299
|
+
}
|
|
1300
|
+
const asyncValidator = context.createAsyncValidator(field, this.validationOptions());
|
|
1301
|
+
// Pass the control value to the validator
|
|
1302
|
+
const validationResult = asyncValidator(control.value);
|
|
1303
|
+
if (validationResult instanceof Observable) {
|
|
1304
|
+
return validationResult;
|
|
1305
|
+
}
|
|
1306
|
+
else if (validationResult instanceof Promise) {
|
|
1307
|
+
if (isDevMode()) {
|
|
1308
|
+
console.debug('[ngx-vest-forms] Async validator returned a Promise. Converting to Observable.');
|
|
1309
|
+
}
|
|
1310
|
+
return from(validationResult);
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
if (isDevMode()) {
|
|
1314
|
+
console.error('[ngx-vest-forms] Async validator returned an unexpected type:', validationResult);
|
|
1315
|
+
}
|
|
1316
|
+
return of(null);
|
|
1317
|
+
}
|
|
878
1318
|
}
|
|
879
1319
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
880
1320
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.1", type: FormModelDirective, isStandalone: true, selector: "[ngModel]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
@@ -889,7 +1329,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
889
1329
|
type: Directive,
|
|
890
1330
|
args: [{
|
|
891
1331
|
selector: '[ngModel]',
|
|
892
|
-
standalone: true,
|
|
893
1332
|
providers: [
|
|
894
1333
|
{
|
|
895
1334
|
provide: NG_ASYNC_VALIDATORS,
|
|
@@ -907,12 +1346,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
907
1346
|
class FormModelGroupDirective {
|
|
908
1347
|
constructor() {
|
|
909
1348
|
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : []));
|
|
910
|
-
this.formDirective = inject(FormDirective);
|
|
1349
|
+
this.formDirective = inject(FormDirective, { optional: true });
|
|
911
1350
|
}
|
|
912
1351
|
validate(control) {
|
|
913
|
-
|
|
1352
|
+
// Null check for control
|
|
1353
|
+
if (!control) {
|
|
1354
|
+
if (isDevMode()) {
|
|
1355
|
+
console.debug('[ngx-vest-forms] Validate called with null control in FormModelGroupDirective.');
|
|
1356
|
+
}
|
|
1357
|
+
return of(null);
|
|
1358
|
+
}
|
|
1359
|
+
// Null check for form context
|
|
1360
|
+
const context = this.formDirective;
|
|
1361
|
+
if (!context) {
|
|
1362
|
+
if (isDevMode()) {
|
|
1363
|
+
console.debug('[ngx-vest-forms] ngModelGroup used outside of ngxVestForm; skipping validation.');
|
|
1364
|
+
}
|
|
1365
|
+
return of(null);
|
|
1366
|
+
}
|
|
1367
|
+
const { ngForm } = context;
|
|
914
1368
|
const field = getFormGroupField(ngForm.control, control);
|
|
915
|
-
|
|
1369
|
+
if (!field) {
|
|
1370
|
+
if (isDevMode()) {
|
|
1371
|
+
console.debug('[ngx-vest-forms] Could not determine field name for validation in FormModelGroupDirective (skipping).');
|
|
1372
|
+
}
|
|
1373
|
+
return of(null);
|
|
1374
|
+
}
|
|
1375
|
+
const asyncValidator = context.createAsyncValidator(field, this.validationOptions());
|
|
1376
|
+
const validationResult = asyncValidator(control.value);
|
|
1377
|
+
if (validationResult instanceof Observable) {
|
|
1378
|
+
return validationResult;
|
|
1379
|
+
}
|
|
1380
|
+
else if (validationResult instanceof Promise) {
|
|
1381
|
+
if (isDevMode()) {
|
|
1382
|
+
console.debug('[ngx-vest-forms] Async validator returned a Promise. Converting to Observable.');
|
|
1383
|
+
}
|
|
1384
|
+
return from(validationResult);
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
if (isDevMode()) {
|
|
1388
|
+
console.error('[ngx-vest-forms] Async validator returned an unexpected type:', validationResult);
|
|
1389
|
+
}
|
|
1390
|
+
return of(null);
|
|
1391
|
+
}
|
|
916
1392
|
}
|
|
917
1393
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: FormModelGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
918
1394
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.1", type: FormModelGroupDirective, isStandalone: true, selector: "[ngModelGroup]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
@@ -927,7 +1403,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
927
1403
|
type: Directive,
|
|
928
1404
|
args: [{
|
|
929
1405
|
selector: '[ngModelGroup]',
|
|
930
|
-
standalone: true,
|
|
931
1406
|
providers: [
|
|
932
1407
|
{
|
|
933
1408
|
provide: NG_ASYNC_VALIDATORS,
|
|
@@ -1207,13 +1682,9 @@ function keepFieldsWhen(currentState, conditions) {
|
|
|
1207
1682
|
return result;
|
|
1208
1683
|
}
|
|
1209
1684
|
|
|
1210
|
-
/*
|
|
1211
|
-
* Public API Surface of ngx-vest-forms
|
|
1212
|
-
*/
|
|
1213
|
-
|
|
1214
1685
|
/**
|
|
1215
1686
|
* Generated bundle index. Do not edit.
|
|
1216
1687
|
*/
|
|
1217
1688
|
|
|
1218
|
-
export { ControlWrapperComponent, FormDirective, FormModelDirective, FormModelGroupDirective, ROOT_FORM, ShapeMismatchError, VALIDATION_CONFIG_DEBOUNCE_TIME, ValidateRootFormDirective, arrayToObject, clearFields, clearFieldsWhen, cloneDeep, fastDeepEqual, getAllFormErrors, getFormControlField, getFormGroupField, keepFieldsWhen, mergeValuesAndRawValues, set, shallowEqual, validateShape, vestForms, vestFormsViewProviders };
|
|
1689
|
+
export { ControlWrapperComponent, FormControlStateDirective, FormDirective, FormErrorDisplayDirective, FormModelDirective, FormModelGroupDirective, ROOT_FORM, ShapeMismatchError, VALIDATION_CONFIG_DEBOUNCE_TIME, ValidateRootFormDirective, arrayToObject, clearFields, clearFieldsWhen, cloneDeep, fastDeepEqual, getAllFormErrors, getFormControlField, getFormGroupField, keepFieldsWhen, mergeValuesAndRawValues, set, shallowEqual, validateShape, vestForms, vestFormsViewProviders };
|
|
1219
1690
|
//# sourceMappingURL=ngx-vest-forms.mjs.map
|