ngx-vest-forms 2.5.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -1
- package/fesm2022/ngx-vest-forms.mjs +904 -245
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/package.json +3 -2
- package/types/ngx-vest-forms.d.ts +162 -30
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, isDevMode, inject, ElementRef, DestroyRef, ChangeDetectorRef,
|
|
3
|
-
import { isFormArray, isFormGroup, NgForm,
|
|
2
|
+
import { InjectionToken, isDevMode, signal, inject, ElementRef, DestroyRef, ChangeDetectorRef, linkedSignal, computed, input, output, effect, untracked, Directive, contentChild, Injector, afterNextRender, afterEveryRender, isSignal, ChangeDetectionStrategy, Component, booleanAttribute, Optional } from '@angular/core';
|
|
3
|
+
import { isFormArray, isFormGroup, NgForm, ValueChangeEvent, StatusChangeEvent, PristineChangeEvent, FormGroup, FormArray, NgModel, NgModelGroup, FormSubmittedEvent, FormResetEvent, NG_ASYNC_VALIDATORS, ControlContainer, FormsModule } from '@angular/forms';
|
|
4
4
|
import { toSignal, outputFromObservable, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
5
|
-
import { startWith, merge,
|
|
5
|
+
import { filter, scan, startWith, merge, map, switchMap, take, of, distinctUntilChanged, timer, Observable, catchError, EMPTY, debounceTime, tap, race, from } from 'rxjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @deprecated Use NGX_ERROR_DISPLAY_MODE_TOKEN instead
|
|
@@ -113,6 +113,20 @@ const NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN = new InjectionToken('NgxValidationCo
|
|
|
113
113
|
function isPrimitive$1(value) {
|
|
114
114
|
return (value === null || (typeof value !== 'object' && typeof value !== 'function'));
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Records that we've started comparing the pair (a, b). Re-entering the same
|
|
118
|
+
* pair during recursion (cycle, or repeated DAG path) short-circuits to `true`
|
|
119
|
+
* — correct because if the prior descent had returned `false`, the outer call
|
|
120
|
+
* would already have short-circuited before we reach the second visit.
|
|
121
|
+
*/
|
|
122
|
+
function rememberPair(seen, a, b) {
|
|
123
|
+
let targets = seen.get(a);
|
|
124
|
+
if (!targets) {
|
|
125
|
+
targets = new WeakSet();
|
|
126
|
+
seen.set(a, targets);
|
|
127
|
+
}
|
|
128
|
+
targets.add(b);
|
|
129
|
+
}
|
|
116
130
|
/**
|
|
117
131
|
* @internal
|
|
118
132
|
* Internal utility for shallow equality checks.
|
|
@@ -207,11 +221,13 @@ function shallowEqual(obj1, obj2) {
|
|
|
207
221
|
* - Plain objects (with recursive deep comparison)
|
|
208
222
|
* - Date objects (by timestamp comparison)
|
|
209
223
|
* - RegExp objects (by source and flags comparison)
|
|
210
|
-
* - Set objects (
|
|
211
|
-
* - Map objects (
|
|
224
|
+
* - Set objects (reference equality only)
|
|
225
|
+
* - Map objects (reference equality only)
|
|
226
|
+
* - Functions (reference equality only — distinct function instances are never equal,
|
|
227
|
+
* even if their source code is identical)
|
|
212
228
|
*
|
|
213
229
|
* **Safety Features:**
|
|
214
|
-
* - **Circular reference
|
|
230
|
+
* - **Circular reference handling**: Tracks visited object pairs with `WeakMap<object, WeakSet<object>>`
|
|
215
231
|
* - **Type coercion prevention**: Strict type checking before comparison
|
|
216
232
|
* - **Null safety**: Proper handling of null and undefined values
|
|
217
233
|
*
|
|
@@ -223,7 +239,8 @@ function shallowEqual(obj1, obj2) {
|
|
|
223
239
|
* ///
|
|
224
240
|
* /// Memory usage:
|
|
225
241
|
* /// JSON.stringify: Creates temporary strings (high GC pressure)
|
|
226
|
-
* /// fastDeepEqual:
|
|
242
|
+
* /// fastDeepEqual: One small WeakMap of visited object pairs is allocated lazily on
|
|
243
|
+
* /// first nested-container descent; primitive-only comparisons allocate nothing.
|
|
227
244
|
* ```
|
|
228
245
|
*
|
|
229
246
|
* **Typical Usage in Forms:**
|
|
@@ -239,91 +256,222 @@ function shallowEqual(obj1, obj2) {
|
|
|
239
256
|
*
|
|
240
257
|
* @param obj1 - First object to compare
|
|
241
258
|
* @param obj2 - Second object to compare
|
|
242
|
-
*
|
|
259
|
+
*
|
|
260
|
+
* Cyclic arrays and plain objects are compared structurally by tracking visited object
|
|
261
|
+
* pairs. Distinct cyclic graphs with the same structure compare equal. `Date` and
|
|
262
|
+
* `RegExp` values compare structurally. `Map` and `Set` values compare by reference
|
|
263
|
+
* only, so distinct instances are considered different even if their contents match.
|
|
264
|
+
*
|
|
243
265
|
* @returns true if objects are deeply equal by value
|
|
244
266
|
*/
|
|
245
|
-
function fastDeepEqual(obj1, obj2
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
function fastDeepEqual(obj1, obj2) {
|
|
268
|
+
return fastDeepEqualInternal(obj1, obj2, undefined);
|
|
269
|
+
}
|
|
270
|
+
function fastDeepEqualInternal(obj1, obj2, seen) {
|
|
271
|
+
// Object.is gives correct semantics for NaN and ±0 — important for numeric
|
|
272
|
+
// form values where `fastDeepEqual(NaN, NaN)` should be true.
|
|
273
|
+
if (Object.is(obj1, obj2)) {
|
|
251
274
|
return true;
|
|
252
275
|
}
|
|
253
276
|
if (obj1 == null || obj2 == null) {
|
|
254
|
-
return
|
|
277
|
+
return false;
|
|
255
278
|
}
|
|
256
279
|
if (typeof obj1 !== typeof obj2) {
|
|
257
280
|
return false;
|
|
258
281
|
}
|
|
282
|
+
// Functions use reference-only equality. Object.is at the top already returned
|
|
283
|
+
// true for identical references, so reaching here means the references differ.
|
|
284
|
+
if (typeof obj1 === 'function') {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
259
287
|
if (isPrimitive$1(obj1) || isPrimitive$1(obj2)) {
|
|
260
|
-
return
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
// Handle Date / RegExp first — they have value semantics and can't contain cycles,
|
|
291
|
+
// so they don't need pair tracking.
|
|
292
|
+
if (obj1 instanceof Date || obj2 instanceof Date) {
|
|
293
|
+
return (obj1 instanceof Date &&
|
|
294
|
+
obj2 instanceof Date &&
|
|
295
|
+
obj1.getTime() === obj2.getTime());
|
|
296
|
+
}
|
|
297
|
+
if (obj1 instanceof RegExp || obj2 instanceof RegExp) {
|
|
298
|
+
return (obj1 instanceof RegExp &&
|
|
299
|
+
obj2 instanceof RegExp &&
|
|
300
|
+
obj1.source === obj2.source &&
|
|
301
|
+
obj1.flags === obj2.flags);
|
|
302
|
+
}
|
|
303
|
+
// Intentional contract: distinct Set / Map instances compare by reference only.
|
|
304
|
+
if (obj1 instanceof Set || obj2 instanceof Set) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (obj1 instanceof Map || obj2 instanceof Map) {
|
|
308
|
+
return false;
|
|
261
309
|
}
|
|
262
|
-
// Handle arrays early for performance (common in forms)
|
|
263
310
|
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
|
|
264
311
|
return false;
|
|
265
312
|
}
|
|
313
|
+
// Cycle / repeated-pair guard for traversable containers.
|
|
314
|
+
// Allocates lazily on first nested-object descent.
|
|
315
|
+
const a = obj1;
|
|
316
|
+
const b = obj2;
|
|
317
|
+
if (seen?.get(a)?.has(b)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
seen ??= new WeakMap();
|
|
321
|
+
rememberPair(seen, a, b);
|
|
266
322
|
if (Array.isArray(obj1)) {
|
|
267
|
-
// We know obj2 is also an array here
|
|
268
323
|
const arr2 = obj2;
|
|
269
324
|
if (obj1.length !== arr2.length) {
|
|
270
325
|
return false;
|
|
271
326
|
}
|
|
272
327
|
for (let i = 0; i < obj1.length; i++) {
|
|
273
|
-
if (!
|
|
328
|
+
if (!fastDeepEqualInternal(obj1[i], arr2[i], seen)) {
|
|
274
329
|
return false;
|
|
275
330
|
}
|
|
276
331
|
}
|
|
277
332
|
return true;
|
|
278
333
|
}
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (obj1 instanceof RegExp) {
|
|
285
|
-
return (obj2 instanceof RegExp &&
|
|
286
|
-
obj1.source === obj2.source &&
|
|
287
|
-
obj1.flags === obj2.flags);
|
|
334
|
+
// Plain objects
|
|
335
|
+
const keys1 = Object.keys(a);
|
|
336
|
+
const o2 = b;
|
|
337
|
+
if (keys1.length !== Object.keys(o2).length) {
|
|
338
|
+
return false;
|
|
288
339
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (!(obj2 instanceof Set) || obj1.size !== obj2.size) {
|
|
340
|
+
for (const key of keys1) {
|
|
341
|
+
if (!Object.hasOwn(o2, key)) {
|
|
292
342
|
return false;
|
|
293
343
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
344
|
+
if (!fastDeepEqualInternal(a[key], o2[key], seen)) {
|
|
345
|
+
return false;
|
|
298
346
|
}
|
|
299
|
-
return true;
|
|
300
347
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Injection token for the deep-equality function used internally by
|
|
353
|
+
* {@link FormDirective} for change detection — `formValueChange`
|
|
354
|
+
* `distinctUntilChanged`, the form↔model two-way sync effect, and the
|
|
355
|
+
* `formState` signal's structural comparator.
|
|
356
|
+
*
|
|
357
|
+
* The default factory returns {@link fastDeepEqual}. Override this token to
|
|
358
|
+
* plug in a smaller or differently-tuned comparator (e.g. `dequal/lite`,
|
|
359
|
+
* `lodash.isEqual`) without forking the directive.
|
|
360
|
+
*
|
|
361
|
+
* @example Bring-your-own equality at the application level
|
|
362
|
+
* ```ts
|
|
363
|
+
* import { dequal } from 'dequal/lite';
|
|
364
|
+
* import { NGX_EQUALITY_FN } from 'ngx-vest-forms';
|
|
365
|
+
*
|
|
366
|
+
* export const appConfig: ApplicationConfig = {
|
|
367
|
+
* providers: [
|
|
368
|
+
* { provide: NGX_EQUALITY_FN, useValue: dequal },
|
|
369
|
+
* ],
|
|
370
|
+
* };
|
|
371
|
+
* ```
|
|
372
|
+
*
|
|
373
|
+
* @example Per-component override (e.g. for tests)
|
|
374
|
+
* ```ts
|
|
375
|
+
* @Component({
|
|
376
|
+
* providers: [
|
|
377
|
+
* { provide: NGX_EQUALITY_FN, useValue: (a, b) => a === b },
|
|
378
|
+
* ],
|
|
379
|
+
* })
|
|
380
|
+
* export class TestFormComponent {}
|
|
381
|
+
* ```
|
|
382
|
+
*
|
|
383
|
+
* @default {@link fastDeepEqual}
|
|
384
|
+
*/
|
|
385
|
+
const NGX_EQUALITY_FN = new InjectionToken('NgxEqualityFn', {
|
|
386
|
+
providedIn: 'root',
|
|
387
|
+
factory: () => fastDeepEqual,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Schedules a callback to run after a delay, automatically cancelling it if the
|
|
392
|
+
* provided `DestroyRef` fires before the timer expires.
|
|
393
|
+
*
|
|
394
|
+
* @param callback - Function to invoke after the delay.
|
|
395
|
+
* @param delayMs - Delay in milliseconds (passed to `setTimeout`).
|
|
396
|
+
* @param destroyRef - Angular `DestroyRef` to register auto-cancellation against.
|
|
397
|
+
* @returns A cancel function; calling it before the timer fires prevents the
|
|
398
|
+
* callback from running and unregisters the destroy listener.
|
|
399
|
+
*/
|
|
400
|
+
function scheduleTimeout(callback, delayMs, destroyRef) {
|
|
401
|
+
let cancelled = false;
|
|
402
|
+
let unregisterCalled = false;
|
|
403
|
+
// eslint-disable-next-line prefer-const -- assigned after onDestroy returns
|
|
404
|
+
let unregisterDestroy;
|
|
405
|
+
// Idempotent unregister wrapper so all paths (timer-fires, explicit-cancel,
|
|
406
|
+
// and onDestroy) can call it safely without double-removal.
|
|
407
|
+
const safeUnregister = () => {
|
|
408
|
+
if (unregisterCalled)
|
|
409
|
+
return;
|
|
410
|
+
unregisterCalled = true;
|
|
411
|
+
unregisterDestroy?.();
|
|
412
|
+
};
|
|
413
|
+
const handle = setTimeout(() => {
|
|
414
|
+
safeUnregister();
|
|
415
|
+
if (!cancelled) {
|
|
416
|
+
callback();
|
|
305
417
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
418
|
+
}, delayMs);
|
|
419
|
+
unregisterDestroy = destroyRef.onDestroy(() => {
|
|
420
|
+
cancelled = true;
|
|
421
|
+
clearTimeout(handle);
|
|
422
|
+
safeUnregister();
|
|
423
|
+
});
|
|
424
|
+
return () => {
|
|
425
|
+
if (!cancelled) {
|
|
426
|
+
cancelled = true;
|
|
427
|
+
clearTimeout(handle);
|
|
428
|
+
safeUnregister();
|
|
311
429
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Schedules a microtask callback, automatically suppressing it if the provided
|
|
434
|
+
* `DestroyRef` fires before the microtask runs.
|
|
435
|
+
*
|
|
436
|
+
* Since `queueMicrotask` has no native cancellation API, this relies on a
|
|
437
|
+
* destroyed flag that is set inside the registered `onDestroy` hook.
|
|
438
|
+
*
|
|
439
|
+
* @param callback - Function to invoke in the next microtask checkpoint.
|
|
440
|
+
* @param destroyRef - Angular `DestroyRef` to register auto-cancellation against.
|
|
441
|
+
* @returns A cancel function; calling it before the microtask runs prevents the
|
|
442
|
+
* callback from executing and unregisters the destroy listener.
|
|
443
|
+
*/
|
|
444
|
+
function scheduleMicrotask(callback, destroyRef) {
|
|
445
|
+
let cancelled = false;
|
|
446
|
+
let unregisterCalled = false;
|
|
447
|
+
// Register the onDestroy listener before queuing the microtask so that
|
|
448
|
+
// `unregisterDestroy` is always defined when the microtask fires.
|
|
449
|
+
const unregisterDestroy = destroyRef.onDestroy(() => {
|
|
450
|
+
cancelled = true;
|
|
451
|
+
safeUnregister();
|
|
452
|
+
});
|
|
453
|
+
// Idempotent unregister wrapper so all paths (microtask-fires, explicit-cancel,
|
|
454
|
+
// and onDestroy) can call it safely without double-removal.
|
|
455
|
+
function safeUnregister() {
|
|
456
|
+
if (unregisterCalled)
|
|
457
|
+
return;
|
|
458
|
+
unregisterCalled = true;
|
|
459
|
+
unregisterDestroy();
|
|
319
460
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
461
|
+
queueMicrotask(() => {
|
|
462
|
+
// Always clean up the destroy listener when the microtask fires,
|
|
463
|
+
// regardless of whether the callback is suppressed.
|
|
464
|
+
safeUnregister();
|
|
465
|
+
if (!cancelled) {
|
|
466
|
+
callback();
|
|
324
467
|
}
|
|
325
|
-
}
|
|
326
|
-
return
|
|
468
|
+
});
|
|
469
|
+
return () => {
|
|
470
|
+
if (!cancelled) {
|
|
471
|
+
cancelled = true;
|
|
472
|
+
safeUnregister();
|
|
473
|
+
}
|
|
474
|
+
};
|
|
327
475
|
}
|
|
328
476
|
|
|
329
477
|
/**
|
|
@@ -345,6 +493,7 @@ function fastDeepEqual(obj1, obj2, maxDepth = 10) {
|
|
|
345
493
|
* ```
|
|
346
494
|
*/
|
|
347
495
|
const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
496
|
+
const LOG_PREFIX$1 = '[ngx-vest-forms] field-path.utils';
|
|
348
497
|
/**
|
|
349
498
|
* @internal
|
|
350
499
|
* Returns whether a path segment is unsafe for object writes/merges.
|
|
@@ -391,11 +540,26 @@ function isUnsafePathSegment(segment) {
|
|
|
391
540
|
function parseFieldPath(path) {
|
|
392
541
|
if (!path)
|
|
393
542
|
return [];
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.
|
|
543
|
+
// Normalize bracket notation to dot notation first so malformed inputs like
|
|
544
|
+
// 'a.[0]' (which becomes 'a..0') are caught alongside 'a..b', '.a', 'a.', '.'.
|
|
545
|
+
const startsWithBracket = path.startsWith('[');
|
|
546
|
+
const segments = path
|
|
547
|
+
.replaceAll(/\[(\d+)\]/g, '.$1')
|
|
548
|
+
.split('.');
|
|
549
|
+
// Empty segments after normalization signal a malformed path. The single
|
|
550
|
+
// legitimate case is a leading empty produced by a path that originally
|
|
551
|
+
// started with '[' (e.g. '[0].x' → '.0.x' → ['', '0', 'x']).
|
|
552
|
+
for (let i = 0; i < segments.length; i++) {
|
|
553
|
+
if (segments[i] === '' && !(i === 0 && startsWithBracket)) {
|
|
554
|
+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
555
|
+
console.warn(`${LOG_PREFIX$1}: Invalid field path '${path}'. Leading dots, trailing dots, consecutive dots, and '.[' separators are not allowed.`);
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return segments
|
|
561
|
+
.filter((part) => part !== '')
|
|
562
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
399
563
|
}
|
|
400
564
|
/**
|
|
401
565
|
* Converts a Standard Schema path array (e.g. `['addresses', 0, 'street']`)
|
|
@@ -654,8 +818,9 @@ function mergeValuesAndRawValues(form) {
|
|
|
654
818
|
}
|
|
655
819
|
const sourceValue = source[key];
|
|
656
820
|
const targetValue = target[key];
|
|
657
|
-
if (targetValue === undefined) {
|
|
658
|
-
//
|
|
821
|
+
if (targetValue === undefined || targetValue === null) {
|
|
822
|
+
// Key missing from target (e.g. disabled field) or set to null —
|
|
823
|
+
// copy source so raw values from disabled controls aren't dropped.
|
|
659
824
|
target[key] = sourceValue;
|
|
660
825
|
}
|
|
661
826
|
else if (isRecord(sourceValue) && isRecord(targetValue)) {
|
|
@@ -678,15 +843,30 @@ function getStringArrayError(errors, key) {
|
|
|
678
843
|
: undefined;
|
|
679
844
|
}
|
|
680
845
|
/**
|
|
681
|
-
* Performs a deep-clone of an object
|
|
682
|
-
*
|
|
846
|
+
* Performs a deep-clone of an object.
|
|
847
|
+
*
|
|
848
|
+
* @deprecated Use the standard {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone structuredClone} instead.
|
|
683
849
|
*
|
|
684
|
-
*
|
|
850
|
+
* `structuredClone` correctly handles `Map`, `Set`, `RegExp`, typed arrays, and
|
|
851
|
+
* cyclic references; this implementation silently drops `Map` / `Set` / `RegExp`
|
|
852
|
+
* data and produces incorrect results on cycles. Scheduled for removal in a
|
|
853
|
+
* future major; see `docs/prd/PRD-bug-sweep.md` (Bundle D) for tracking.
|
|
685
854
|
*
|
|
686
|
-
* Browser Support: structuredClone is available in all modern browsers
|
|
687
|
-
*
|
|
855
|
+
* Browser Support: `structuredClone` is available in all modern browsers
|
|
856
|
+
* (Chrome 98+, Firefox 94+, Safari 15.4+, Edge 98+) and Node.js 17+.
|
|
688
857
|
*/
|
|
858
|
+
let cloneDeepDeprecationWarned = false;
|
|
689
859
|
function cloneDeep(object) {
|
|
860
|
+
// NOTE: `typeof ngDevMode !== 'undefined' && ngDevMode` is kept inline
|
|
861
|
+
// (not extracted to a helper) because Angular's build optimizer relies on
|
|
862
|
+
// this exact pattern for tree-shaking dev-only code from production bundles.
|
|
863
|
+
if (!cloneDeepDeprecationWarned &&
|
|
864
|
+
typeof ngDevMode !== 'undefined' &&
|
|
865
|
+
ngDevMode) {
|
|
866
|
+
cloneDeepDeprecationWarned = true;
|
|
867
|
+
console.warn('[ngx-vest-forms] cloneDeep is deprecated and silently drops Map/Set/RegExp values. ' +
|
|
868
|
+
'Use the standard structuredClone() instead.');
|
|
869
|
+
}
|
|
690
870
|
// Handle primitives (null, undefined, boolean, string, number, function)
|
|
691
871
|
if (isPrimitive(object)) {
|
|
692
872
|
return object;
|
|
@@ -732,6 +912,7 @@ function setValueAtPath(obj, path, value) {
|
|
|
732
912
|
let current = obj;
|
|
733
913
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
734
914
|
const segment = keys[i];
|
|
915
|
+
const nextSegment = keys[i + 1];
|
|
735
916
|
if (segment === undefined) {
|
|
736
917
|
continue;
|
|
737
918
|
}
|
|
@@ -740,8 +921,10 @@ function setValueAtPath(obj, path, value) {
|
|
|
740
921
|
}
|
|
741
922
|
const key = String(segment);
|
|
742
923
|
const next = current[key];
|
|
743
|
-
if (!isRecord(next)) {
|
|
744
|
-
|
|
924
|
+
if (!Array.isArray(next) && !isRecord(next)) {
|
|
925
|
+
const shouldCreateArray = typeof nextSegment === 'number' ||
|
|
926
|
+
(typeof nextSegment === 'string' && /^\d+$/.test(nextSegment));
|
|
927
|
+
current[key] = shouldCreateArray ? [] : {};
|
|
745
928
|
}
|
|
746
929
|
current = current[key];
|
|
747
930
|
}
|
|
@@ -836,6 +1019,18 @@ function getAllFormErrors(form) {
|
|
|
836
1019
|
return errors;
|
|
837
1020
|
}
|
|
838
1021
|
|
|
1022
|
+
const NUMERIC_PATH_SEGMENT = /^\d+$/;
|
|
1023
|
+
function isOpaqueLeafValue(value) {
|
|
1024
|
+
return (value instanceof Date ||
|
|
1025
|
+
value instanceof Map ||
|
|
1026
|
+
value instanceof Set ||
|
|
1027
|
+
value instanceof RegExp ||
|
|
1028
|
+
(typeof File !== 'undefined' && value instanceof File) ||
|
|
1029
|
+
(typeof Blob !== 'undefined' && value instanceof Blob));
|
|
1030
|
+
}
|
|
1031
|
+
function isTraversableValue(value) {
|
|
1032
|
+
return typeof value === 'object' && value !== null && !isOpaqueLeafValue(value);
|
|
1033
|
+
}
|
|
839
1034
|
/**
|
|
840
1035
|
* Validates a form value against a shape to catch typos in `name` or `ngModelGroup` attributes.
|
|
841
1036
|
*
|
|
@@ -871,31 +1066,86 @@ function validateFormValueAgainstShape(formValue, shape, path = '') {
|
|
|
871
1066
|
}
|
|
872
1067
|
// For array items (numeric keys > 0), compare against the first item in shape
|
|
873
1068
|
// since we only define one example item in the shape for arrays
|
|
874
|
-
const isNumericKey =
|
|
875
|
-
|
|
1069
|
+
const isNumericKey = NUMERIC_PATH_SEGMENT.test(key);
|
|
1070
|
+
// Array shapes provide one example item at index 0, so every numeric key maps to '0'.
|
|
1071
|
+
const shapeKey = isNumericKey && key !== '0' ? '0' : key;
|
|
876
1072
|
const shapeValue = shape?.[shapeKey];
|
|
1073
|
+
const hasShapeKey = shape != null && shapeKey in shape;
|
|
877
1074
|
// Skip Date fields receiving empty strings (common in date picker libraries)
|
|
878
1075
|
if (shapeValue instanceof Date && value === '') {
|
|
879
1076
|
continue;
|
|
880
1077
|
}
|
|
881
1078
|
// Handle object values (recurse into nested objects)
|
|
882
1079
|
if (typeof value === 'object') {
|
|
1080
|
+
if (!isTraversableValue(value)) {
|
|
1081
|
+
if (!isNumericKey && !hasShapeKey) {
|
|
1082
|
+
logWarning(NGX_VEST_FORMS_ERRORS.EXTRA_PROPERTY, fieldPath);
|
|
1083
|
+
}
|
|
1084
|
+
else if (!isNumericKey &&
|
|
1085
|
+
hasShapeKey &&
|
|
1086
|
+
(shapeValue === null || typeof shapeValue !== 'object')) {
|
|
1087
|
+
// Type mismatch: formValue holds an opaque object (Date, Map, etc.)
|
|
1088
|
+
// but shape declares a primitive. Recursion is intentionally skipped
|
|
1089
|
+
// for opaque leaves, but the user still benefits from a warning.
|
|
1090
|
+
logWarning(NGX_VEST_FORMS_ERRORS.TYPE_MISMATCH, fieldPath, 'primitive', 'object');
|
|
1091
|
+
}
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
883
1094
|
// Type mismatch: formValue has object, but shape expects primitive
|
|
884
|
-
if (!isNumericKey &&
|
|
885
|
-
(typeof shapeValue !== 'object' || shapeValue === null)) {
|
|
1095
|
+
if (!isNumericKey && !isTraversableValue(shapeValue)) {
|
|
886
1096
|
logWarning(NGX_VEST_FORMS_ERRORS.TYPE_MISMATCH, fieldPath, 'primitive', 'object');
|
|
887
1097
|
}
|
|
888
1098
|
// Recurse into nested object
|
|
889
|
-
validateFormValueAgainstShape(value, shapeValue
|
|
1099
|
+
validateFormValueAgainstShape(value, isTraversableValue(shapeValue) ? shapeValue : {}, fieldPath);
|
|
890
1100
|
continue;
|
|
891
1101
|
}
|
|
892
1102
|
// Extra property: key exists in formValue but not in shape (likely a typo)
|
|
893
|
-
if (!isNumericKey &&
|
|
1103
|
+
if (!isNumericKey && !hasShapeKey) {
|
|
894
1104
|
logWarning(NGX_VEST_FORMS_ERRORS.EXTRA_PROPERTY, fieldPath);
|
|
895
1105
|
}
|
|
896
1106
|
}
|
|
897
1107
|
}
|
|
898
1108
|
|
|
1109
|
+
const formSubmittedSignals = new WeakMap();
|
|
1110
|
+
function getFormSubmittedSignal(ngForm) {
|
|
1111
|
+
let submitted = formSubmittedSignals.get(ngForm);
|
|
1112
|
+
if (!submitted) {
|
|
1113
|
+
submitted = signal(ngForm.submitted);
|
|
1114
|
+
formSubmittedSignals.set(ngForm, submitted);
|
|
1115
|
+
}
|
|
1116
|
+
return submitted;
|
|
1117
|
+
}
|
|
1118
|
+
function setAngularFormSubmittedState(ngForm, submitted) {
|
|
1119
|
+
// Angular 21.x's concrete NgForm stores submitted state on
|
|
1120
|
+
// `submittedReactive`, while AbstractFormDirective-backed implementations
|
|
1121
|
+
// expose `_submittedReactive` and may also provide a public setter on
|
|
1122
|
+
// `submitted`. This helper is verified against Angular 21.x in this
|
|
1123
|
+
// repository and falls back to the first writable `submitted` setter it can
|
|
1124
|
+
// find if a future Angular version changes the concrete field names.
|
|
1125
|
+
//
|
|
1126
|
+
// We try the concrete/internal signal first because NgForm overrides the
|
|
1127
|
+
// getter-only `submitted` property at runtime in this workspace. If neither
|
|
1128
|
+
// signal exists, fall back to the first writable `submitted` setter we can
|
|
1129
|
+
// find on the prototype chain. If no setter exists either, callers should
|
|
1130
|
+
// still update ngx-vest-forms' shared signal so error display state remains
|
|
1131
|
+
// reactive even though Angular's native submitted flag cannot be changed.
|
|
1132
|
+
const signalHost = ngForm;
|
|
1133
|
+
const angularSignal = signalHost.submittedReactive ?? signalHost._submittedReactive;
|
|
1134
|
+
if (angularSignal) {
|
|
1135
|
+
angularSignal.set(submitted);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
let prototype = Object.getPrototypeOf(ngForm);
|
|
1139
|
+
while (prototype) {
|
|
1140
|
+
const submittedDescriptor = Object.getOwnPropertyDescriptor(prototype, 'submitted');
|
|
1141
|
+
if (submittedDescriptor?.set) {
|
|
1142
|
+
submittedDescriptor.set.call(ngForm, submitted);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
899
1149
|
/**
|
|
900
1150
|
* Duration (in milliseconds) to keep fields marked as "in-progress" after validation.
|
|
901
1151
|
* This prevents immediate re-triggering of bidirectional validations.
|
|
@@ -941,12 +1191,22 @@ const VALIDATION_IN_PROGRESS_TIMEOUT_MS = 500;
|
|
|
941
1191
|
* @publicApi
|
|
942
1192
|
*/
|
|
943
1193
|
class FormDirective {
|
|
944
|
-
|
|
945
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Deep-equality comparator. Defaults to `fastDeepEqual`; can be overridden
|
|
1196
|
+
* application-wide or per-component via {@link NGX_EQUALITY_FN}.
|
|
1197
|
+
*/
|
|
1198
|
+
#equal;
|
|
1199
|
+
/**
|
|
1200
|
+
* Set to true by the onDestroy hook. Used to guard async callbacks
|
|
1201
|
+
* (e.g. Vest `done()`) that cannot be cancelled via RxJS operators.
|
|
1202
|
+
*/
|
|
1203
|
+
#destroyed;
|
|
946
1204
|
#lastSyncedFormValue;
|
|
947
1205
|
#lastSyncedModelValue;
|
|
948
|
-
// Internal signal tracking
|
|
949
|
-
|
|
1206
|
+
// Internal signal tracking changes that can affect the merged form snapshot.
|
|
1207
|
+
// ValueChangeEvent keeps the cache fresh for blur-driven consumers like
|
|
1208
|
+
// draft auto-save, even when a value update doesn't change form validity.
|
|
1209
|
+
#formSnapshotTick;
|
|
950
1210
|
/**
|
|
951
1211
|
* LinkedSignal that computes form values from Angular form state.
|
|
952
1212
|
* This eliminates timing issues with the previous dual-effect pattern.
|
|
@@ -973,36 +1233,43 @@ class FormDirective {
|
|
|
973
1233
|
this.destroyRef = inject(DestroyRef);
|
|
974
1234
|
this.cdr = inject(ChangeDetectorRef);
|
|
975
1235
|
this.configDebounceTime = inject(NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN);
|
|
1236
|
+
/**
|
|
1237
|
+
* Deep-equality comparator. Defaults to `fastDeepEqual`; can be overridden
|
|
1238
|
+
* application-wide or per-component via {@link NGX_EQUALITY_FN}.
|
|
1239
|
+
*/
|
|
1240
|
+
this.#equal = inject(NGX_EQUALITY_FN);
|
|
976
1241
|
/**
|
|
977
1242
|
* Public signal storing field warnings keyed by field path.
|
|
978
1243
|
* This allows warnings to be stored and displayed without affecting field validity.
|
|
979
1244
|
* Angular's control.errors !== null marks a field as invalid, so we store warnings
|
|
980
1245
|
* separately when they exist without errors.
|
|
981
1246
|
*/
|
|
982
|
-
this.fieldWarnings = signal(new Map(), ...(ngDevMode ? [{ debugName: "fieldWarnings" }] : []));
|
|
983
|
-
|
|
984
|
-
|
|
1247
|
+
this.fieldWarnings = signal(new Map(), ...(ngDevMode ? [{ debugName: "fieldWarnings" }] : /* istanbul ignore next */ []));
|
|
1248
|
+
/**
|
|
1249
|
+
* Set to true by the onDestroy hook. Used to guard async callbacks
|
|
1250
|
+
* (e.g. Vest `done()`) that cannot be cancelled via RxJS operators.
|
|
1251
|
+
*/
|
|
1252
|
+
this.#destroyed = false;
|
|
985
1253
|
this.#lastSyncedFormValue = null;
|
|
986
1254
|
this.#lastSyncedModelValue = null;
|
|
987
|
-
// Internal signal tracking
|
|
988
|
-
|
|
1255
|
+
// Internal signal tracking changes that can affect the merged form snapshot.
|
|
1256
|
+
// ValueChangeEvent keeps the cache fresh for blur-driven consumers like
|
|
1257
|
+
// draft auto-save, even when a value update doesn't change form validity.
|
|
1258
|
+
this.#formSnapshotTick = toSignal(this.ngForm.form.events.pipe(filter((event) => event instanceof ValueChangeEvent || event instanceof StatusChangeEvent), scan((count) => count + 1, 0), startWith(0)), { initialValue: 0 });
|
|
989
1259
|
/**
|
|
990
1260
|
* LinkedSignal that computes form values from Angular form state.
|
|
991
1261
|
* This eliminates timing issues with the previous dual-effect pattern.
|
|
992
1262
|
*/
|
|
993
1263
|
this.#formValueSignal = linkedSignal(() => {
|
|
994
|
-
// Track form
|
|
995
|
-
this.#
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
return
|
|
1000
|
-
}
|
|
1001
|
-
else if (this.#lastLinkedValue !== null) {
|
|
1002
|
-
return this.#lastLinkedValue;
|
|
1264
|
+
// Track changes that affect the merged form snapshot.
|
|
1265
|
+
this.#formSnapshotTick();
|
|
1266
|
+
if (Object.keys(this.ngForm.form.controls).length === 0) {
|
|
1267
|
+
// No controls remain (e.g. dynamic group removal): expose `null` so
|
|
1268
|
+
// consumers don't see ghost data from a previous form shape.
|
|
1269
|
+
return null;
|
|
1003
1270
|
}
|
|
1004
|
-
return
|
|
1005
|
-
}, ...(ngDevMode ? [{ debugName: "#formValueSignal" }] : []));
|
|
1271
|
+
return mergeValuesAndRawValues(this.ngForm.form);
|
|
1272
|
+
}, ...(ngDevMode ? [{ debugName: "#formValueSignal" }] : /* istanbul ignore next */ []));
|
|
1006
1273
|
/**
|
|
1007
1274
|
* Track the Angular form status as a signal for advanced status flags
|
|
1008
1275
|
*/
|
|
@@ -1012,7 +1279,7 @@ class FormDirective {
|
|
|
1012
1279
|
* This guarantees recomputation for every blur/tab interaction,
|
|
1013
1280
|
* even when the form's aggregate touched flag is already true.
|
|
1014
1281
|
*/
|
|
1015
|
-
this.#blurTick = signal(0, ...(ngDevMode ? [{ debugName: "#blurTick" }] : []));
|
|
1282
|
+
this.#blurTick = signal(0, ...(ngDevMode ? [{ debugName: "#blurTick" }] : /* istanbul ignore next */ []));
|
|
1016
1283
|
/**
|
|
1017
1284
|
* Computed signal that returns field paths for all touched (or submitted) leaf controls.
|
|
1018
1285
|
* Updates reactively when controls are touched (blur) or when form status changes.
|
|
@@ -1026,7 +1293,7 @@ class FormDirective {
|
|
|
1026
1293
|
this.#blurTick();
|
|
1027
1294
|
this.#statusSignal();
|
|
1028
1295
|
return this.#collectTouchedPaths(this.ngForm.form, this.ngForm.submitted);
|
|
1029
|
-
}, ...(ngDevMode ? [{ debugName: "touchedFieldPaths" }] : []));
|
|
1296
|
+
}, ...(ngDevMode ? [{ debugName: "touchedFieldPaths" }] : /* istanbul ignore next */ []));
|
|
1030
1297
|
/**
|
|
1031
1298
|
* Computed signal for form state with validity and errors.
|
|
1032
1299
|
* Used by templates and tests as vestForm.formState().valid/errors
|
|
@@ -1043,7 +1310,7 @@ class FormDirective {
|
|
|
1043
1310
|
errors: getAllFormErrors(this.ngForm.form),
|
|
1044
1311
|
value: this.#formValueSignal(),
|
|
1045
1312
|
};
|
|
1046
|
-
}, { ...(ngDevMode ? { debugName: "formState" } : {}), equal: (a, b) => {
|
|
1313
|
+
}, { ...(ngDevMode ? { debugName: "formState" } : /* istanbul ignore next */ {}), equal: (a, b) => {
|
|
1047
1314
|
// Fast path: reference equality
|
|
1048
1315
|
if (a === b)
|
|
1049
1316
|
return true;
|
|
@@ -1052,29 +1319,29 @@ class FormDirective {
|
|
|
1052
1319
|
return false;
|
|
1053
1320
|
// Deep equality check for form state properties
|
|
1054
1321
|
return (a.valid === b.valid &&
|
|
1055
|
-
|
|
1056
|
-
|
|
1322
|
+
this.#equal(a.errors, b.errors) &&
|
|
1323
|
+
this.#equal(a.value, b.value));
|
|
1057
1324
|
} });
|
|
1058
1325
|
/**
|
|
1059
1326
|
* The value of the form, this is needed for the validation part.
|
|
1060
1327
|
* Using input() here because two-way binding is provided via formValueChange output.
|
|
1061
1328
|
* In the minimal core directive (form-core.directive.ts), this would be model() instead.
|
|
1062
1329
|
*/
|
|
1063
|
-
this.formValue = input(null, ...(ngDevMode ? [{ debugName: "formValue" }] : []));
|
|
1330
|
+
this.formValue = input(null, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
1064
1331
|
/**
|
|
1065
1332
|
* Static vest suite that will be used to feed our angular validators.
|
|
1066
1333
|
* Accepts both NgxVestSuite and NgxTypedVestSuite through compatible type signatures.
|
|
1067
1334
|
* NgxTypedVestSuite<T> is assignable to NgxVestSuite<T> due to bivariance and
|
|
1068
1335
|
* FormFieldName<T> (string literal union) being assignable to string.
|
|
1069
1336
|
*/
|
|
1070
|
-
this.suite = input(null, ...(ngDevMode ? [{ debugName: "suite" }] : []));
|
|
1337
|
+
this.suite = input(null, ...(ngDevMode ? [{ debugName: "suite" }] : /* istanbul ignore next */ []));
|
|
1071
1338
|
/**
|
|
1072
1339
|
* The shape of our form model. This is a deep required version of the form model
|
|
1073
1340
|
* The goal is to add default values to the shape so when the template-driven form
|
|
1074
1341
|
* contains values that shouldn't be there (typo's) that the developer gets run-time
|
|
1075
1342
|
* errors in dev mode
|
|
1076
1343
|
*/
|
|
1077
|
-
this.formShape = input(null, ...(ngDevMode ? [{ debugName: "formShape" }] : []));
|
|
1344
|
+
this.formShape = input(null, ...(ngDevMode ? [{ debugName: "formShape" }] : /* istanbul ignore next */ []));
|
|
1078
1345
|
/**
|
|
1079
1346
|
* Updates the validation config which is a dynamic object that will be used to
|
|
1080
1347
|
* trigger validations on the dependant fields
|
|
@@ -1088,7 +1355,7 @@ class FormDirective {
|
|
|
1088
1355
|
*
|
|
1089
1356
|
* @param v
|
|
1090
1357
|
*/
|
|
1091
|
-
this.validationConfig = input(null, ...(ngDevMode ? [{ debugName: "validationConfig" }] : []));
|
|
1358
|
+
this.validationConfig = input(null, ...(ngDevMode ? [{ debugName: "validationConfig" }] : /* istanbul ignore next */ []));
|
|
1092
1359
|
/**
|
|
1093
1360
|
* Emits whenever validation feedback may have changed, even if the aggregate
|
|
1094
1361
|
* root form status string stays the same.
|
|
@@ -1119,7 +1386,7 @@ class FormDirective {
|
|
|
1119
1386
|
*/
|
|
1120
1387
|
this.formValueChange = outputFromObservable(this.ngForm.form.events.pipe(filter((v) => v instanceof ValueChangeEvent), map((v) => v.value), distinctUntilChanged((prev, curr) => {
|
|
1121
1388
|
// Use efficient deep equality instead of JSON.stringify for better performance
|
|
1122
|
-
return
|
|
1389
|
+
return this.#equal(prev, curr);
|
|
1123
1390
|
}), map(() => mergeValuesAndRawValues(this.ngForm.form)), takeUntilDestroyed(this.destroyRef)));
|
|
1124
1391
|
/**
|
|
1125
1392
|
* Emits an object with all the errors of the form
|
|
@@ -1147,11 +1414,18 @@ class FormDirective {
|
|
|
1147
1414
|
* Cleanup is handled automatically by the directive when it's destroyed.
|
|
1148
1415
|
*/
|
|
1149
1416
|
this.validChange = outputFromObservable(this.statusChanges$.pipe(filter((e) => e === 'VALID' || e === 'INVALID'), map((v) => v === 'VALID'), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)));
|
|
1417
|
+
/**
|
|
1418
|
+
* Emits when a named control inside the form loses focus.
|
|
1419
|
+
*
|
|
1420
|
+
* Useful for application-level workflows such as draft auto-save on blur.
|
|
1421
|
+
*/
|
|
1422
|
+
this.fieldBlur = output();
|
|
1150
1423
|
/**
|
|
1151
1424
|
* Track validation in progress to prevent circular triggering (Issue #19)
|
|
1152
1425
|
*/
|
|
1153
1426
|
this.validationInProgress = new Set();
|
|
1154
1427
|
this.destroyRef.onDestroy(() => {
|
|
1428
|
+
this.#destroyed = true;
|
|
1155
1429
|
this.fieldWarnings.set(new Map());
|
|
1156
1430
|
});
|
|
1157
1431
|
/**
|
|
@@ -1187,8 +1461,8 @@ class FormDirective {
|
|
|
1187
1461
|
if (!formValue && !modelValue)
|
|
1188
1462
|
return;
|
|
1189
1463
|
// Compute change flags first
|
|
1190
|
-
const formChanged = !
|
|
1191
|
-
const modelChanged = !
|
|
1464
|
+
const formChanged = !this.#equal(formValue, this.#lastSyncedFormValue);
|
|
1465
|
+
const modelChanged = !this.#equal(modelValue, this.#lastSyncedModelValue);
|
|
1192
1466
|
// Early return if nothing changed
|
|
1193
1467
|
if (!formChanged && !modelChanged) {
|
|
1194
1468
|
return;
|
|
@@ -1222,7 +1496,7 @@ class FormDirective {
|
|
|
1222
1496
|
else if (formChanged && modelChanged) {
|
|
1223
1497
|
// Both form and model changed simultaneously
|
|
1224
1498
|
// Check if they changed to the same value (synchronized change) or different values (conflict)
|
|
1225
|
-
const valuesEqual =
|
|
1499
|
+
const valuesEqual = this.#equal(formValue, modelValue);
|
|
1226
1500
|
if (valuesEqual) {
|
|
1227
1501
|
// Both changed to the same value - this is a synchronized change, not a conflict
|
|
1228
1502
|
// Just update tracking to acknowledge the change
|
|
@@ -1364,6 +1638,52 @@ class FormDirective {
|
|
|
1364
1638
|
this.ngForm.form.markAllAsTouched();
|
|
1365
1639
|
this.#blurTick.update((v) => v + 1);
|
|
1366
1640
|
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Clears the current submit cycle without resetting control values or metadata.
|
|
1643
|
+
*
|
|
1644
|
+
* Unlike {@link resetForm}, this only flips the submitted gate back to `false`.
|
|
1645
|
+
* Touched/dirty/pristine state is preserved so consumers can end `'on-submit'`
|
|
1646
|
+
* error visibility without a full form reset.
|
|
1647
|
+
*
|
|
1648
|
+
* **When to use:**
|
|
1649
|
+
* - You use submit-gated error visibility such as `'on-submit'`
|
|
1650
|
+
* - A submit attempt already happened
|
|
1651
|
+
* - The user resolved the current submit-time errors
|
|
1652
|
+
* - You want future untouched fields to wait for the next submit before showing errors
|
|
1653
|
+
*
|
|
1654
|
+
* **Why this exists:**
|
|
1655
|
+
* `resetForm()` would also clear touched/dirty/pristine metadata, which is often
|
|
1656
|
+
* too disruptive for long-form, multi-form, or mixed error-display flows.
|
|
1657
|
+
*
|
|
1658
|
+
* **What it does NOT do:**
|
|
1659
|
+
* - Does not change field values
|
|
1660
|
+
* - Does not mark controls pristine or untouched
|
|
1661
|
+
* - Does not re-run validation
|
|
1662
|
+
*
|
|
1663
|
+
* @example
|
|
1664
|
+
* ```typescript
|
|
1665
|
+
* submitAll(): void {
|
|
1666
|
+
* for (const form of this.submitForms()) {
|
|
1667
|
+
* form.ngForm.onSubmit(new Event('submit'));
|
|
1668
|
+
* }
|
|
1669
|
+
*
|
|
1670
|
+
* if (this.submitForms().every((form) => form.formState().valid)) {
|
|
1671
|
+
* for (const form of this.submitForms()) {
|
|
1672
|
+
* form.clearSubmittedState();
|
|
1673
|
+
* }
|
|
1674
|
+
* }
|
|
1675
|
+
* }
|
|
1676
|
+
* ```
|
|
1677
|
+
*
|
|
1678
|
+
* @see {@link resetForm} to fully reset values and control metadata
|
|
1679
|
+
* @see {@link markAllAsTouched} to manually show all errors
|
|
1680
|
+
* @see {@link triggerFormValidation} to re-run validation after structure changes
|
|
1681
|
+
*/
|
|
1682
|
+
clearSubmittedState() {
|
|
1683
|
+
setAngularFormSubmittedState(this.ngForm, false);
|
|
1684
|
+
getFormSubmittedSignal(this.ngForm).set(false);
|
|
1685
|
+
this.#blurTick.update((v) => v + 1);
|
|
1686
|
+
}
|
|
1367
1687
|
/**
|
|
1368
1688
|
* Finds the first invalid element in this form, scrolls it into view, and focuses it.
|
|
1369
1689
|
*
|
|
@@ -1408,13 +1728,98 @@ class FormDirective {
|
|
|
1408
1728
|
* Host handler: called whenever any descendant field loses focus.
|
|
1409
1729
|
* Used to make touched-path tracking react immediately on blur/tab.
|
|
1410
1730
|
*/
|
|
1411
|
-
onFormFocusOut() {
|
|
1731
|
+
onFormFocusOut(event) {
|
|
1412
1732
|
// Run on the next microtask to ensure Angular has already applied
|
|
1413
1733
|
// control.touched changes for the field that just blurred.
|
|
1414
|
-
|
|
1734
|
+
scheduleMicrotask(() => {
|
|
1415
1735
|
this.#blurTick.update((v) => v + 1);
|
|
1736
|
+
this.#emitFieldBlurEvent(event);
|
|
1737
|
+
}, this.destroyRef);
|
|
1738
|
+
}
|
|
1739
|
+
#emitFieldBlurEvent(event) {
|
|
1740
|
+
const resolved = this.#resolveFieldFromFocusEvent(event);
|
|
1741
|
+
if (!resolved) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const { field, control, element } = resolved;
|
|
1745
|
+
// Read the latest value directly from the DOM element when it can be
|
|
1746
|
+
// trusted. For radio groups Angular keeps the bound `control.value` in
|
|
1747
|
+
// sync with the *selected* option, while a focused-but-unchecked radio
|
|
1748
|
+
// would expose its own option value via the DOM — so radios must always
|
|
1749
|
+
// fall back to `control.value`. For text/textarea/select we prefer the
|
|
1750
|
+
// element value to avoid `ngModelOptions.updateOn: 'submit'` staleness.
|
|
1751
|
+
const domValue = readElementValueForBlur(element);
|
|
1752
|
+
const value = domValue !== undefined ? domValue : control.value;
|
|
1753
|
+
// Prefer the cached linked-signal snapshot when it exists. Both code
|
|
1754
|
+
// paths produce a deep-cloned snapshot, but reusing the cached value
|
|
1755
|
+
// saves one of the two `structuredClone` passes performed by
|
|
1756
|
+
// `mergeValuesAndRawValues()` on every blur.
|
|
1757
|
+
const cachedSnapshot = this.#formValueSignal();
|
|
1758
|
+
const formValue = cachedSnapshot !== null
|
|
1759
|
+
? structuredClone(cachedSnapshot)
|
|
1760
|
+
: mergeValuesAndRawValues(this.ngForm.form);
|
|
1761
|
+
setValueAtPath(formValue, field, value);
|
|
1762
|
+
this.fieldBlur.emit({
|
|
1763
|
+
field,
|
|
1764
|
+
value,
|
|
1765
|
+
formValue,
|
|
1766
|
+
dirty: control.dirty,
|
|
1767
|
+
touched: control.touched,
|
|
1768
|
+
valid: control.valid,
|
|
1769
|
+
pending: control.pending,
|
|
1416
1770
|
});
|
|
1417
1771
|
}
|
|
1772
|
+
#resolveFieldFromFocusEvent(event) {
|
|
1773
|
+
const target = event.target;
|
|
1774
|
+
if (!(target instanceof Element)) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
const fieldElement = target.closest('[name]');
|
|
1778
|
+
if (!(fieldElement instanceof HTMLElement)) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const name = fieldElement.getAttribute('name')?.trim();
|
|
1782
|
+
if (!name) {
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
const formEl = this.elementRef.nativeElement;
|
|
1786
|
+
// Authoritative path: ask the registered NgModel directive whose value
|
|
1787
|
+
// accessor is bound to this exact element. This handles all forms of
|
|
1788
|
+
// grouping uniformly — static `ngModelGroup="key"`, dynamic
|
|
1789
|
+
// `[ngModelGroup]="expr"`, repeated leaf names across siblings — because
|
|
1790
|
+
// the directive's `path` is computed from the live ControlContainer tree.
|
|
1791
|
+
const directiveMatch = resolveControlPathByNgModelDirective(this.ngForm, fieldElement);
|
|
1792
|
+
if (directiveMatch) {
|
|
1793
|
+
return {
|
|
1794
|
+
field: directiveMatch.path,
|
|
1795
|
+
control: directiveMatch.control,
|
|
1796
|
+
element: fieldElement,
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
// Fallback: walk DOM ancestors collecting any `ngModelGroup` attribute
|
|
1800
|
+
// values, producing the canonical dotted path for the static attribute
|
|
1801
|
+
// form (e.g. `<div ngModelGroup="passwords"><input name="password">` →
|
|
1802
|
+
// `passwords.password`). Used when the directive lookup misses (e.g. the
|
|
1803
|
+
// value accessor doesn't expose its element ref in some custom CVAs).
|
|
1804
|
+
const staticGroups = collectNgModelGroupAttributes(fieldElement, formEl);
|
|
1805
|
+
const staticPath = [...staticGroups, name].join('.');
|
|
1806
|
+
const staticControl = this.ngForm.form.get(staticPath);
|
|
1807
|
+
if (staticControl) {
|
|
1808
|
+
return { field: staticPath, control: staticControl, element: fieldElement };
|
|
1809
|
+
}
|
|
1810
|
+
// Last-resort fallback for ambiguous DOM structures: probe each ancestor
|
|
1811
|
+
// element as a potential group boundary, querying the form tree until we
|
|
1812
|
+
// find a child that owns this DOM element.
|
|
1813
|
+
const dynamicMatch = resolveControlPathByDomAncestors(this.ngForm.form, fieldElement, formEl, name);
|
|
1814
|
+
if (dynamicMatch) {
|
|
1815
|
+
return {
|
|
1816
|
+
field: dynamicMatch.path,
|
|
1817
|
+
control: dynamicMatch.control,
|
|
1818
|
+
element: fieldElement,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1418
1823
|
/**
|
|
1419
1824
|
* Resets the form to a pristine, untouched state with optional new values.
|
|
1420
1825
|
*
|
|
@@ -1475,7 +1880,6 @@ class FormDirective {
|
|
|
1475
1880
|
// is treated as a model change (not a conflict with stale form values)
|
|
1476
1881
|
this.#lastSyncedFormValue = null;
|
|
1477
1882
|
this.#lastSyncedModelValue = null;
|
|
1478
|
-
this.#lastLinkedValue = null;
|
|
1479
1883
|
// Force change detection to ensure DOM updates are reflected
|
|
1480
1884
|
// Note: This is still needed even with signals because we're modifying NgForm
|
|
1481
1885
|
// (reactive forms), not signals. The formValue signal updates happen in the
|
|
@@ -1516,6 +1920,18 @@ class FormDirective {
|
|
|
1516
1920
|
// Both NgxVestSuite and NgxTypedVestSuite work with string at runtime
|
|
1517
1921
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1518
1922
|
suite(snap, field).done((result) => {
|
|
1923
|
+
// Guard: bail out if the directive was destroyed while
|
|
1924
|
+
// validation was in flight to avoid writing to disposed
|
|
1925
|
+
// signals or a torn-down view.
|
|
1926
|
+
if (this.#destroyed) {
|
|
1927
|
+
// Emit a neutral `null` before completing so async
|
|
1928
|
+
// validators always emit exactly once. Completing without
|
|
1929
|
+
// emission can leave consumers (e.g. a control's status)
|
|
1930
|
+
// in an unexpected `PENDING` state.
|
|
1931
|
+
observer.next(null);
|
|
1932
|
+
observer.complete();
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1519
1935
|
const errors = result.getErrors()[field];
|
|
1520
1936
|
const warnings = result.getWarnings()[field];
|
|
1521
1937
|
// Store warnings in the fieldWarnings signal for access by control wrappers.
|
|
@@ -1552,8 +1968,9 @@ class FormDirective {
|
|
|
1552
1968
|
// visual state (even though the control status has updated).
|
|
1553
1969
|
//
|
|
1554
1970
|
// We schedule a detectChanges() on the next microtask to avoid calling it
|
|
1555
|
-
// synchronously inside Angular's own validation pipeline.
|
|
1556
|
-
|
|
1971
|
+
// synchronously inside Angular's own validation pipeline. The scheduleMicrotask
|
|
1972
|
+
// primitive auto-cancels if the directive is destroyed before it fires.
|
|
1973
|
+
scheduleMicrotask(() => {
|
|
1557
1974
|
try {
|
|
1558
1975
|
this.cdr.detectChanges();
|
|
1559
1976
|
}
|
|
@@ -1562,7 +1979,7 @@ class FormDirective {
|
|
|
1562
1979
|
// This keeps behavior resilient in edge cases.
|
|
1563
1980
|
this.cdr.markForCheck();
|
|
1564
1981
|
}
|
|
1565
|
-
});
|
|
1982
|
+
}, this.destroyRef);
|
|
1566
1983
|
observer.next(out);
|
|
1567
1984
|
observer.complete();
|
|
1568
1985
|
});
|
|
@@ -1601,10 +2018,7 @@ class FormDirective {
|
|
|
1601
2018
|
this.validationInProgress.clear();
|
|
1602
2019
|
return EMPTY;
|
|
1603
2020
|
}
|
|
1604
|
-
const streams = Object.
|
|
1605
|
-
const dependents = config[triggerField] || [];
|
|
1606
|
-
return this.#createTriggerStream(form, triggerField, dependents);
|
|
1607
|
-
});
|
|
2021
|
+
const streams = Object.entries(config).map(([triggerField, dependents]) => this.#createTriggerStream(form, triggerField, dependents || []));
|
|
1608
2022
|
return streams.length > 0 ? merge(...streams) : EMPTY;
|
|
1609
2023
|
}
|
|
1610
2024
|
/**
|
|
@@ -1719,20 +2133,6 @@ class FormDirective {
|
|
|
1719
2133
|
// CRITICAL: Mark the dependent field as in-progress BEFORE calling updateValueAndValidity
|
|
1720
2134
|
// This prevents the dependent field's valueChanges from triggering its own validationConfig
|
|
1721
2135
|
this.validationInProgress.add(depField);
|
|
1722
|
-
// NOTE: Touch propagation removed (PR #78)
|
|
1723
|
-
// Previously, we propagated touch state from trigger to dependent fields.
|
|
1724
|
-
// This caused UX issues where dependent fields showed errors immediately
|
|
1725
|
-
// after being revealed by a toggle, even though the user never interacted with them.
|
|
1726
|
-
//
|
|
1727
|
-
// With this change:
|
|
1728
|
-
// - Errors on dependent fields only show after the user directly touches/blurs them
|
|
1729
|
-
// - ARIA attributes (aria-invalid) still work correctly via isInvalid check
|
|
1730
|
-
// - Warnings still show after validation via hasBeenValidated check
|
|
1731
|
-
//
|
|
1732
|
-
// The removed code was:
|
|
1733
|
-
// if (control.touched && !dependentControl.touched) {
|
|
1734
|
-
// dependentControl.markAsTouched({ onlySelf: true });
|
|
1735
|
-
// }
|
|
1736
2136
|
// emitEvent: true is REQUIRED for async validators to actually run
|
|
1737
2137
|
// The validationInProgress Set prevents infinite loops:
|
|
1738
2138
|
// 1. Field A changes → triggers validation on dependent field B
|
|
@@ -1756,13 +2156,14 @@ class FormDirective {
|
|
|
1756
2156
|
}
|
|
1757
2157
|
}
|
|
1758
2158
|
// Keep fields marked as in-progress for a short time to prevent immediate re-triggering
|
|
1759
|
-
// Use
|
|
1760
|
-
|
|
2159
|
+
// Use scheduleTimeout to ensure async validators have time to complete before allowing
|
|
2160
|
+
// new triggers. The timer auto-cancels on directive destroy so no timers leak.
|
|
2161
|
+
scheduleTimeout(() => {
|
|
1761
2162
|
this.validationInProgress.delete(triggerField);
|
|
1762
2163
|
for (const depField of dependents) {
|
|
1763
2164
|
this.validationInProgress.delete(depField);
|
|
1764
2165
|
}
|
|
1765
|
-
}, VALIDATION_IN_PROGRESS_TIMEOUT_MS);
|
|
2166
|
+
}, VALIDATION_IN_PROGRESS_TIMEOUT_MS, this.destroyRef);
|
|
1766
2167
|
}
|
|
1767
2168
|
/**
|
|
1768
2169
|
* Collects field paths of all touched (or submitted) leaf controls
|
|
@@ -1790,19 +2191,180 @@ class FormDirective {
|
|
|
1790
2191
|
collect(control, []);
|
|
1791
2192
|
return fields;
|
|
1792
2193
|
}
|
|
1793
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1794
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
2194
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2195
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: FormDirective, isStandalone: true, selector: "form[scVestForm], form[ngxVestForm]", 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", errorsChange: "errorsChange", dirtyChange: "dirtyChange", validChange: "validChange", fieldBlur: "fieldBlur" }, host: { listeners: { "focusout": "onFormFocusOut($event)" } }, exportAs: ["scVestForm", "ngxVestForm"], ngImport: i0 }); }
|
|
1795
2196
|
}
|
|
1796
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
2197
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormDirective, decorators: [{
|
|
1797
2198
|
type: Directive,
|
|
1798
2199
|
args: [{
|
|
1799
2200
|
selector: 'form[scVestForm], form[ngxVestForm]',
|
|
1800
2201
|
exportAs: 'scVestForm, ngxVestForm',
|
|
1801
2202
|
host: {
|
|
1802
|
-
'(focusout)': 'onFormFocusOut()',
|
|
2203
|
+
'(focusout)': 'onFormFocusOut($event)',
|
|
1803
2204
|
},
|
|
1804
2205
|
}]
|
|
1805
|
-
}], ctorParameters: () => [], propDecorators: { formValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "formValue", required: false }] }], suite: [{ type: i0.Input, args: [{ isSignal: true, alias: "suite", required: false }] }], formShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "formShape", required: false }] }], validationConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationConfig", required: false }] }], formValueChange: [{ type: i0.Output, args: ["formValueChange"] }], errorsChange: [{ type: i0.Output, args: ["errorsChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], validChange: [{ type: i0.Output, args: ["validChange"] }] } });
|
|
2206
|
+
}], ctorParameters: () => [], propDecorators: { formValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "formValue", required: false }] }], suite: [{ type: i0.Input, args: [{ isSignal: true, alias: "suite", required: false }] }], formShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "formShape", required: false }] }], validationConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationConfig", required: false }] }], formValueChange: [{ type: i0.Output, args: ["formValueChange"] }], errorsChange: [{ type: i0.Output, args: ["errorsChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], validChange: [{ type: i0.Output, args: ["validChange"] }], fieldBlur: [{ type: i0.Output, args: ["fieldBlur"] }] } });
|
|
2207
|
+
/**
|
|
2208
|
+
* Reads the user-entered value from a blurred form element. Returns
|
|
2209
|
+
* `undefined` for radio inputs (caller must fall back to the bound
|
|
2210
|
+
* `control.value`, since the focused radio is not necessarily the
|
|
2211
|
+
* group's selected option) and for elements we don't handle.
|
|
2212
|
+
*/
|
|
2213
|
+
function readElementValueForBlur(element) {
|
|
2214
|
+
if (element instanceof HTMLInputElement) {
|
|
2215
|
+
if (element.type === 'radio')
|
|
2216
|
+
return undefined;
|
|
2217
|
+
if (element.type === 'checkbox')
|
|
2218
|
+
return element.checked;
|
|
2219
|
+
if (element.type === 'number') {
|
|
2220
|
+
return element.value === '' ? null : element.valueAsNumber;
|
|
2221
|
+
}
|
|
2222
|
+
return element.value;
|
|
2223
|
+
}
|
|
2224
|
+
if (element instanceof HTMLTextAreaElement ||
|
|
2225
|
+
element instanceof HTMLSelectElement) {
|
|
2226
|
+
return element.value;
|
|
2227
|
+
}
|
|
2228
|
+
return undefined;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Walks DOM ancestors between `start` (exclusive) and `formEl` (exclusive),
|
|
2232
|
+
* collecting any preserved `ngModelGroup` attribute values into a path
|
|
2233
|
+
* suitable for `FormGroup.get()`. Only the static attribute form is
|
|
2234
|
+
* preserved on the DOM; dynamically-bound `[ngModelGroup]` is handled by
|
|
2235
|
+
* `resolveControlPathByDomAncestors`.
|
|
2236
|
+
*/
|
|
2237
|
+
function collectNgModelGroupAttributes(start, formEl) {
|
|
2238
|
+
const groups = [];
|
|
2239
|
+
let current = start.parentElement;
|
|
2240
|
+
while (current && current !== formEl && formEl.contains(current)) {
|
|
2241
|
+
const groupName = current.getAttribute('ngModelGroup')?.trim();
|
|
2242
|
+
if (groupName) {
|
|
2243
|
+
groups.unshift(groupName);
|
|
2244
|
+
}
|
|
2245
|
+
current = current.parentElement;
|
|
2246
|
+
}
|
|
2247
|
+
return groups;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Resolves the dotted control path for a blurred element when the static
|
|
2251
|
+
* `ngModelGroup` attribute walk failed (typically because the host used
|
|
2252
|
+
* `[ngModelGroup]="expr"`, which Angular does not always preserve as a
|
|
2253
|
+
* DOM attribute). Walks the control tree top-down and at each FormGroup
|
|
2254
|
+
* boundary tries to descend into a child whose subtree contains an element
|
|
2255
|
+
* with the matching `name` — disambiguating repeated leaf names by DOM
|
|
2256
|
+
* containment instead of giving up.
|
|
2257
|
+
*/
|
|
2258
|
+
function resolveControlPathByDomAncestors(root, fieldElement, formEl, leafName) {
|
|
2259
|
+
const descend = (frame) => {
|
|
2260
|
+
if (frame.control instanceof FormGroup) {
|
|
2261
|
+
for (const [key, child] of Object.entries(frame.control.controls)) {
|
|
2262
|
+
if (key === leafName && !(child instanceof FormGroup)) {
|
|
2263
|
+
return { control: child, path: [...frame.path, key] };
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const candidates = [];
|
|
2267
|
+
for (const [key, child] of Object.entries(frame.control.controls)) {
|
|
2268
|
+
if (child instanceof FormGroup || child instanceof FormArray) {
|
|
2269
|
+
if (subtreeContainsElement(child, fieldElement, formEl, key)) {
|
|
2270
|
+
candidates.push({ control: child, path: [...frame.path, key] });
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (candidates.length === 1 && candidates[0])
|
|
2275
|
+
return descend(candidates[0]);
|
|
2276
|
+
return null;
|
|
2277
|
+
}
|
|
2278
|
+
if (frame.control instanceof FormArray) {
|
|
2279
|
+
const candidates = [];
|
|
2280
|
+
frame.control.controls.forEach((child, index) => {
|
|
2281
|
+
if (child instanceof FormGroup || child instanceof FormArray) {
|
|
2282
|
+
if (subtreeContainsElement(child, fieldElement, formEl, index)) {
|
|
2283
|
+
candidates.push({ control: child, path: [...frame.path, index] });
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
if (candidates.length === 1 && candidates[0])
|
|
2288
|
+
return descend(candidates[0]);
|
|
2289
|
+
}
|
|
2290
|
+
return null;
|
|
2291
|
+
};
|
|
2292
|
+
const result = descend({ control: root, path: [] });
|
|
2293
|
+
if (!result)
|
|
2294
|
+
return null;
|
|
2295
|
+
return { path: stringifyFieldPath(result.path), control: result.control };
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Best-effort check: does the DOM subtree rooted at any element annotated
|
|
2299
|
+
* with `ngModelGroup="<key>"` contain `fieldElement`? Used by
|
|
2300
|
+
* `resolveControlPathByDomAncestors` to disambiguate repeated leaf names.
|
|
2301
|
+
* For dynamic `[ngModelGroup]` with no preserved attribute we cannot
|
|
2302
|
+
* disambiguate from DOM alone — those cases return `false`, matching the
|
|
2303
|
+
* documented limitation.
|
|
2304
|
+
*/
|
|
2305
|
+
function subtreeContainsElement(_child, fieldElement, formEl, key) {
|
|
2306
|
+
if (typeof key !== 'string')
|
|
2307
|
+
return false;
|
|
2308
|
+
const selector = `[ngModelGroup="${CSS.escape(key)}"]`;
|
|
2309
|
+
const candidates = formEl.querySelectorAll(selector);
|
|
2310
|
+
for (const candidate of candidates) {
|
|
2311
|
+
if (candidate.contains(fieldElement))
|
|
2312
|
+
return true;
|
|
2313
|
+
}
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Resolves the control + dotted path for a blurred element by consulting the
|
|
2318
|
+
* `NgModel` directives Angular registered with this `NgForm`. Each registered
|
|
2319
|
+
* directive carries a live `path` (the full ControlContainer chain) and a
|
|
2320
|
+
* value accessor whose element ref is the input the directive is hosted on.
|
|
2321
|
+
*
|
|
2322
|
+
* This handles all grouping shapes uniformly — static `ngModelGroup="key"`,
|
|
2323
|
+
* dynamic `[ngModelGroup]="expr"`, and repeated leaf names across siblings —
|
|
2324
|
+
* because the path comes from the live form tree rather than DOM heuristics.
|
|
2325
|
+
* Returns `null` when the directive can't be matched (e.g. a custom CVA that
|
|
2326
|
+
* doesn't store an element ref), so the caller can fall back to DOM probes.
|
|
2327
|
+
*/
|
|
2328
|
+
function resolveControlPathByNgModelDirective(ngForm, fieldElement) {
|
|
2329
|
+
const directives = readNgFormDirectives(ngForm);
|
|
2330
|
+
if (!directives)
|
|
2331
|
+
return null;
|
|
2332
|
+
for (const directive of directives) {
|
|
2333
|
+
const accessorEl = readValueAccessorElement(directive.valueAccessor);
|
|
2334
|
+
if (accessorEl !== fieldElement)
|
|
2335
|
+
continue;
|
|
2336
|
+
const control = directive.control ?? ngForm.form.get(directive.path);
|
|
2337
|
+
if (!control)
|
|
2338
|
+
return null;
|
|
2339
|
+
return { path: directive.path.join('.'), control };
|
|
2340
|
+
}
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Reads the registered `NgModel` directives from `NgForm`. Angular forms keeps
|
|
2345
|
+
* them in a private `_directives: Set<NgModel>`. The field name is stable
|
|
2346
|
+
* across all Angular versions that ship `ngModel`, but is not part of the
|
|
2347
|
+
* public type — callers must tolerate `null`.
|
|
2348
|
+
*/
|
|
2349
|
+
function readNgFormDirectives(ngForm) {
|
|
2350
|
+
const set = ngForm._directives;
|
|
2351
|
+
return set ?? null;
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Reads the host element of a `ControlValueAccessor`. The standard accessors
|
|
2355
|
+
* shipped by Angular forms (default, number, select, radio, checkbox, range)
|
|
2356
|
+
* all store an `ElementRef` injected at construction as `_elementRef`. This
|
|
2357
|
+
* is private but stable; custom accessors that don't follow the convention
|
|
2358
|
+
* will simply miss the fast path.
|
|
2359
|
+
*/
|
|
2360
|
+
function readValueAccessorElement(accessor) {
|
|
2361
|
+
if (!accessor)
|
|
2362
|
+
return null;
|
|
2363
|
+
const elementRef = accessor
|
|
2364
|
+
._elementRef;
|
|
2365
|
+
const native = elementRef?.nativeElement;
|
|
2366
|
+
return native instanceof HTMLElement ? native : null;
|
|
2367
|
+
}
|
|
1806
2368
|
|
|
1807
2369
|
const INITIAL_FORM_CONTROL_STATE = {
|
|
1808
2370
|
status: 'INVALID',
|
|
@@ -1843,9 +2405,22 @@ class FormControlStateDirective {
|
|
|
1843
2405
|
* Internal signal for control state (updated reactively)
|
|
1844
2406
|
*/
|
|
1845
2407
|
#controlStateSignal;
|
|
2408
|
+
/**
|
|
2409
|
+
* Tick bumped once via `afterNextRender` if `control.control` is undefined
|
|
2410
|
+
* at the first effect run (NgModel registers asynchronously). Reading this
|
|
2411
|
+
* signal inside the effect causes a re-evaluation after the next render so
|
|
2412
|
+
* we pick up the late-attached `FormControl`. Bookkeeping below ensures we
|
|
2413
|
+
* only schedule a single retry — no permanent polling.
|
|
2414
|
+
*/
|
|
2415
|
+
#controlAttachTick;
|
|
2416
|
+
// Latch is scoped to the *current* `#activeControl` instance. When the
|
|
2417
|
+
// active control changes (e.g. host swaps an `@if` block, NgModel
|
|
2418
|
+
// recreates), the latch resets so a fresh late-attach gets one retry.
|
|
2419
|
+
#controlAttachRetryScheduled;
|
|
2420
|
+
#lastSeenActiveControl;
|
|
1846
2421
|
constructor() {
|
|
1847
|
-
this.contentNgModel = contentChild(NgModel, ...(ngDevMode ? [{ debugName: "contentNgModel" }] : []));
|
|
1848
|
-
this.contentNgModelGroup = contentChild(NgModelGroup, ...(ngDevMode ? [{ debugName: "contentNgModelGroup" }] : []));
|
|
2422
|
+
this.contentNgModel = contentChild(NgModel, ...(ngDevMode ? [{ debugName: "contentNgModel" }] : /* istanbul ignore next */ []));
|
|
2423
|
+
this.contentNgModelGroup = contentChild(NgModelGroup, ...(ngDevMode ? [{ debugName: "contentNgModelGroup" }] : /* istanbul ignore next */ []));
|
|
1849
2424
|
this.#hostNgModel = inject(NgModel, { self: true, optional: true });
|
|
1850
2425
|
this.#hostNgModelGroup = inject(NgModelGroup, {
|
|
1851
2426
|
self: true,
|
|
@@ -1864,7 +2439,7 @@ class FormControlStateDirective {
|
|
|
1864
2439
|
this.#hostNgModelGroup ||
|
|
1865
2440
|
this.contentNgModel() ||
|
|
1866
2441
|
this.contentNgModelGroup() ||
|
|
1867
|
-
null, ...(ngDevMode ? [{ debugName: "#activeControl" }] : []));
|
|
2442
|
+
null, ...(ngDevMode ? [{ debugName: "#activeControl" }] : /* istanbul ignore next */ []));
|
|
1868
2443
|
/**
|
|
1869
2444
|
* Consolidated internal signal for interaction state tracking.
|
|
1870
2445
|
* Combines touched, dirty, and hasBeenValidated into a single signal
|
|
@@ -1874,7 +2449,7 @@ class FormControlStateDirective {
|
|
|
1874
2449
|
isTouched: false,
|
|
1875
2450
|
isDirty: false,
|
|
1876
2451
|
hasBeenValidated: false,
|
|
1877
|
-
}, ...(ngDevMode ? [{ debugName: "#interactionState" }] : []));
|
|
2452
|
+
}, ...(ngDevMode ? [{ debugName: "#interactionState" }] : /* istanbul ignore next */ []));
|
|
1878
2453
|
/**
|
|
1879
2454
|
* Track the previous status to detect actual status changes (not just status emissions).
|
|
1880
2455
|
* This helps distinguish between initial control creation and actual re-validation.
|
|
@@ -1883,11 +2458,24 @@ class FormControlStateDirective {
|
|
|
1883
2458
|
/**
|
|
1884
2459
|
* Internal signal for control state (updated reactively)
|
|
1885
2460
|
*/
|
|
1886
|
-
this.#controlStateSignal = signal(INITIAL_FORM_CONTROL_STATE, ...(ngDevMode ? [{ debugName: "#controlStateSignal" }] : []));
|
|
2461
|
+
this.#controlStateSignal = signal(INITIAL_FORM_CONTROL_STATE, ...(ngDevMode ? [{ debugName: "#controlStateSignal" }] : /* istanbul ignore next */ []));
|
|
2462
|
+
/**
|
|
2463
|
+
* Tick bumped once via `afterNextRender` if `control.control` is undefined
|
|
2464
|
+
* at the first effect run (NgModel registers asynchronously). Reading this
|
|
2465
|
+
* signal inside the effect causes a re-evaluation after the next render so
|
|
2466
|
+
* we pick up the late-attached `FormControl`. Bookkeeping below ensures we
|
|
2467
|
+
* only schedule a single retry — no permanent polling.
|
|
2468
|
+
*/
|
|
2469
|
+
this.#controlAttachTick = signal(0, ...(ngDevMode ? [{ debugName: "#controlAttachTick" }] : /* istanbul ignore next */ []));
|
|
2470
|
+
// Latch is scoped to the *current* `#activeControl` instance. When the
|
|
2471
|
+
// active control changes (e.g. host swaps an `@if` block, NgModel
|
|
2472
|
+
// recreates), the latch resets so a fresh late-attach gets one retry.
|
|
2473
|
+
this.#controlAttachRetryScheduled = false;
|
|
2474
|
+
this.#lastSeenActiveControl = null;
|
|
1887
2475
|
/**
|
|
1888
2476
|
* Main control state computed signal (merges robust touched/dirty)
|
|
1889
2477
|
*/
|
|
1890
|
-
this.controlState = computed(() => this.#controlStateSignal(), ...(ngDevMode ? [{ debugName: "controlState" }] : []));
|
|
2478
|
+
this.controlState = computed(() => this.#controlStateSignal(), ...(ngDevMode ? [{ debugName: "controlState" }] : /* istanbul ignore next */ []));
|
|
1891
2479
|
/**
|
|
1892
2480
|
* Extracts error messages from Angular/Vest errors (recursively flattens)
|
|
1893
2481
|
*/
|
|
@@ -1907,20 +2495,20 @@ class FormControlStateDirective {
|
|
|
1907
2495
|
const errorsWithoutWarnings = { ...errors };
|
|
1908
2496
|
delete errorsWithoutWarnings['warnings'];
|
|
1909
2497
|
return this.#flattenAngularErrors(errorsWithoutWarnings);
|
|
1910
|
-
}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : []));
|
|
2498
|
+
}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
|
|
1911
2499
|
/**
|
|
1912
2500
|
* ADVANCED: updateOn strategy (change/blur/submit) if available
|
|
1913
2501
|
*/
|
|
1914
2502
|
this.updateOn = computed(() => {
|
|
1915
2503
|
const ngModel = this.contentNgModel() || this.#hostNgModel;
|
|
1916
2504
|
return ngModel?.options?.updateOn ?? 'change';
|
|
1917
|
-
}, ...(ngDevMode ? [{ debugName: "updateOn" }] : []));
|
|
2505
|
+
}, ...(ngDevMode ? [{ debugName: "updateOn" }] : /* istanbul ignore next */ []));
|
|
1918
2506
|
/**
|
|
1919
2507
|
* ADVANCED: Composite/derived signals for advanced error display logic
|
|
1920
2508
|
*/
|
|
1921
|
-
this.isValidTouched = computed(() => this.isValid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isValidTouched" }] : []));
|
|
1922
|
-
this.isInvalidTouched = computed(() => this.isInvalid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isInvalidTouched" }] : []));
|
|
1923
|
-
this.shouldShowErrors = computed(() => this.isInvalid() && this.isTouched() && !this.isPending(), ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : []));
|
|
2509
|
+
this.isValidTouched = computed(() => this.isValid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isValidTouched" }] : /* istanbul ignore next */ []));
|
|
2510
|
+
this.isInvalidTouched = computed(() => this.isInvalid() && this.isTouched(), ...(ngDevMode ? [{ debugName: "isInvalidTouched" }] : /* istanbul ignore next */ []));
|
|
2511
|
+
this.shouldShowErrors = computed(() => this.isInvalid() && this.isTouched() && !this.isPending(), ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : /* istanbul ignore next */ []));
|
|
1924
2512
|
/**
|
|
1925
2513
|
* Extracts warning messages from Vest validation results.
|
|
1926
2514
|
* Checks two sources:
|
|
@@ -1955,36 +2543,59 @@ class FormControlStateDirective {
|
|
|
1955
2543
|
}
|
|
1956
2544
|
}
|
|
1957
2545
|
return [];
|
|
1958
|
-
}, ...(ngDevMode ? [{ debugName: "warningMessages" }] : []));
|
|
2546
|
+
}, ...(ngDevMode ? [{ debugName: "warningMessages" }] : /* istanbul ignore next */ []));
|
|
1959
2547
|
/**
|
|
1960
2548
|
* Whether async validation is in progress
|
|
1961
2549
|
*/
|
|
1962
|
-
this.hasPendingValidation = computed(() => this.controlState().isPending, ...(ngDevMode ? [{ debugName: "hasPendingValidation" }] : []));
|
|
2550
|
+
this.hasPendingValidation = computed(() => this.controlState().isPending, ...(ngDevMode ? [{ debugName: "hasPendingValidation" }] : /* istanbul ignore next */ []));
|
|
1963
2551
|
/**
|
|
1964
2552
|
* Convenience signals for common state checks
|
|
1965
2553
|
*/
|
|
1966
|
-
this.isValid = computed(() => this.controlState().isValid, ...(ngDevMode ? [{ debugName: "isValid" }] : []));
|
|
1967
|
-
this.isInvalid = computed(() => this.controlState().isInvalid, ...(ngDevMode ? [{ debugName: "isInvalid" }] : []));
|
|
1968
|
-
this.isPending = computed(() => this.controlState().isPending, ...(ngDevMode ? [{ debugName: "isPending" }] : []));
|
|
1969
|
-
this.isTouched = computed(() => this.controlState().isTouched, ...(ngDevMode ? [{ debugName: "isTouched" }] : []));
|
|
1970
|
-
this.isDirty = computed(() => this.controlState().isDirty, ...(ngDevMode ? [{ debugName: "isDirty" }] : []));
|
|
1971
|
-
this.isPristine = computed(() => this.controlState().isPristine, ...(ngDevMode ? [{ debugName: "isPristine" }] : []));
|
|
1972
|
-
this.isDisabled = computed(() => this.controlState().isDisabled, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
1973
|
-
this.hasErrors = computed(() => this.errorMessages().length > 0, ...(ngDevMode ? [{ debugName: "hasErrors" }] : []));
|
|
2554
|
+
this.isValid = computed(() => this.controlState().isValid, ...(ngDevMode ? [{ debugName: "isValid" }] : /* istanbul ignore next */ []));
|
|
2555
|
+
this.isInvalid = computed(() => this.controlState().isInvalid, ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
2556
|
+
this.isPending = computed(() => this.controlState().isPending, ...(ngDevMode ? [{ debugName: "isPending" }] : /* istanbul ignore next */ []));
|
|
2557
|
+
this.isTouched = computed(() => this.controlState().isTouched, ...(ngDevMode ? [{ debugName: "isTouched" }] : /* istanbul ignore next */ []));
|
|
2558
|
+
this.isDirty = computed(() => this.controlState().isDirty, ...(ngDevMode ? [{ debugName: "isDirty" }] : /* istanbul ignore next */ []));
|
|
2559
|
+
this.isPristine = computed(() => this.controlState().isPristine, ...(ngDevMode ? [{ debugName: "isPristine" }] : /* istanbul ignore next */ []));
|
|
2560
|
+
this.isDisabled = computed(() => this.controlState().isDisabled, ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
2561
|
+
this.hasErrors = computed(() => this.errorMessages().length > 0, ...(ngDevMode ? [{ debugName: "hasErrors" }] : /* istanbul ignore next */ []));
|
|
1974
2562
|
/**
|
|
1975
2563
|
* Whether this control has been validated at least once.
|
|
1976
2564
|
* True after the first validation completes, even if the user hasn't touched the field.
|
|
1977
|
-
* This
|
|
2565
|
+
* This is primarily used for warning display and other derived state that should react
|
|
2566
|
+
* to validationConfig-triggered validation even before the user touches the field.
|
|
1978
2567
|
*/
|
|
1979
|
-
this.hasBeenValidated = computed(() => this.#interactionState().hasBeenValidated, ...(ngDevMode ? [{ debugName: "hasBeenValidated" }] : []));
|
|
2568
|
+
this.hasBeenValidated = computed(() => this.#interactionState().hasBeenValidated, ...(ngDevMode ? [{ debugName: "hasBeenValidated" }] : /* istanbul ignore next */ []));
|
|
1980
2569
|
// Update control state reactively with proper cleanup
|
|
1981
2570
|
effect((onCleanup) => {
|
|
1982
2571
|
const control = this.#activeControl();
|
|
1983
2572
|
const interaction = this.#interactionState();
|
|
2573
|
+
// Track the retry tick so a late-attached `control.control` re-runs this
|
|
2574
|
+
// effect after the next render.
|
|
2575
|
+
this.#controlAttachTick();
|
|
2576
|
+
// Re-arm the late-attach latch whenever the active control identity
|
|
2577
|
+
// changes — including transitions to/from null — so a newly mounted
|
|
2578
|
+
// directive whose `FormControl` registers asynchronously gets its own
|
|
2579
|
+
// single retry.
|
|
2580
|
+
if (control !== this.#lastSeenActiveControl) {
|
|
2581
|
+
this.#lastSeenActiveControl = control;
|
|
2582
|
+
this.#controlAttachRetryScheduled = false;
|
|
2583
|
+
}
|
|
1984
2584
|
if (!control) {
|
|
1985
2585
|
this.#controlStateSignal.set(INITIAL_FORM_CONTROL_STATE);
|
|
1986
2586
|
return;
|
|
1987
2587
|
}
|
|
2588
|
+
// NgModel attaches its `FormControl` during its own ngOnInit, which can
|
|
2589
|
+
// run after this effect's first execution in some host orderings. If
|
|
2590
|
+
// `control.control` isn't there yet, schedule a single `afterNextRender`
|
|
2591
|
+
// retry so we re-evaluate once Angular finishes wiring directives.
|
|
2592
|
+
if (!control.control && !this.#controlAttachRetryScheduled) {
|
|
2593
|
+
this.#controlAttachRetryScheduled = true;
|
|
2594
|
+
afterNextRender(() => {
|
|
2595
|
+
this.#controlAttachTick.update((v) => v + 1);
|
|
2596
|
+
}, { injector: this.#injector });
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
1988
2599
|
// Listen to control changes
|
|
1989
2600
|
const sub = control.control?.statusChanges?.subscribe(() => {
|
|
1990
2601
|
const { status, valid, invalid, pending, disabled, pristine, errors, touched, } = control;
|
|
@@ -2082,6 +2693,23 @@ class FormControlStateDirective {
|
|
|
2082
2693
|
? true
|
|
2083
2694
|
: state.hasBeenValidated,
|
|
2084
2695
|
}));
|
|
2696
|
+
// Keep the derived control-state signal in sync even when blur/dirty
|
|
2697
|
+
// changes do not produce a statusChanges emission.
|
|
2698
|
+
//
|
|
2699
|
+
// This happens when a control is already INVALID due to dependent-field
|
|
2700
|
+
// validation and the user then blurs it. Error display modes that depend
|
|
2701
|
+
// on `isTouched()` must still update immediately in that case.
|
|
2702
|
+
this.#controlStateSignal.set({
|
|
2703
|
+
status: control.status,
|
|
2704
|
+
isValid: control.valid ?? false,
|
|
2705
|
+
isInvalid: control.invalid ?? false,
|
|
2706
|
+
isPending: control.pending ?? false,
|
|
2707
|
+
isDisabled: control.disabled ?? false,
|
|
2708
|
+
isTouched: newTouched,
|
|
2709
|
+
isDirty: newDirty,
|
|
2710
|
+
isPristine: control.pristine ?? true,
|
|
2711
|
+
errors: control.errors,
|
|
2712
|
+
});
|
|
2085
2713
|
}
|
|
2086
2714
|
// Sync pending state only when it transitions from true to false
|
|
2087
2715
|
// This fixes "Validating..." being stuck when statusChanges misses the transition
|
|
@@ -2132,10 +2760,10 @@ class FormControlStateDirective {
|
|
|
2132
2760
|
}
|
|
2133
2761
|
return result;
|
|
2134
2762
|
}
|
|
2135
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
2136
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.
|
|
2763
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormControlStateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2764
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.2.11", type: FormControlStateDirective, isStandalone: true, selector: "[formControlState], [ngxControlState]", queries: [{ propertyName: "contentNgModel", first: true, predicate: NgModel, descendants: true, isSignal: true }, { propertyName: "contentNgModelGroup", first: true, predicate: NgModelGroup, descendants: true, isSignal: true }], exportAs: ["formControlState", "ngxControlState"], ngImport: i0 }); }
|
|
2137
2765
|
}
|
|
2138
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
2766
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormControlStateDirective, decorators: [{
|
|
2139
2767
|
type: Directive,
|
|
2140
2768
|
args: [{
|
|
2141
2769
|
selector: '[formControlState], [ngxControlState]',
|
|
@@ -2178,23 +2806,27 @@ class FormErrorDisplayDirective {
|
|
|
2178
2806
|
#formControlState;
|
|
2179
2807
|
// Optionally inject NgForm for form submission tracking
|
|
2180
2808
|
#ngForm;
|
|
2809
|
+
#formSubmittedState;
|
|
2181
2810
|
constructor() {
|
|
2182
2811
|
this.#formControlState = inject(FormControlStateDirective);
|
|
2183
2812
|
// Optionally inject NgForm for form submission tracking
|
|
2184
2813
|
this.#ngForm = inject(NgForm, { optional: true });
|
|
2814
|
+
this.#formSubmittedState = this.#ngForm
|
|
2815
|
+
? getFormSubmittedSignal(this.#ngForm)
|
|
2816
|
+
: signal(false);
|
|
2185
2817
|
/**
|
|
2186
2818
|
* Input signal for error display mode.
|
|
2187
2819
|
* Works seamlessly with hostDirectives in Angular 19+.
|
|
2188
2820
|
*/
|
|
2189
2821
|
this.errorDisplayMode = input(inject(NGX_ERROR_DISPLAY_MODE_TOKEN, { optional: true }) ??
|
|
2190
2822
|
inject(SC_ERROR_DISPLAY_MODE_TOKEN, { optional: true }) ??
|
|
2191
|
-
SC_ERROR_DISPLAY_MODE_DEFAULT, ...(ngDevMode ? [{ debugName: "errorDisplayMode" }] : []));
|
|
2823
|
+
SC_ERROR_DISPLAY_MODE_DEFAULT, ...(ngDevMode ? [{ debugName: "errorDisplayMode" }] : /* istanbul ignore next */ []));
|
|
2192
2824
|
/**
|
|
2193
2825
|
* Input signal for warning display mode.
|
|
2194
2826
|
* Controls whether warnings are shown only after touch or also after validation.
|
|
2195
2827
|
*/
|
|
2196
2828
|
this.warningDisplayMode = input(inject(NGX_WARNING_DISPLAY_MODE_TOKEN, { optional: true }) ??
|
|
2197
|
-
SC_WARNING_DISPLAY_MODE_DEFAULT, ...(ngDevMode ? [{ debugName: "warningDisplayMode" }] : []));
|
|
2829
|
+
SC_WARNING_DISPLAY_MODE_DEFAULT, ...(ngDevMode ? [{ debugName: "warningDisplayMode" }] : /* istanbul ignore next */ []));
|
|
2198
2830
|
// Expose state signals from FormControlStateDirective
|
|
2199
2831
|
this.controlState = this.#formControlState.controlState;
|
|
2200
2832
|
this.errorMessages = this.#formControlState.errorMessages;
|
|
@@ -2219,27 +2851,25 @@ class FormErrorDisplayDirective {
|
|
|
2219
2851
|
* This keeps programmatic `NgForm.onSubmit()` reactive in zoneless mode and
|
|
2220
2852
|
* avoids depending on `NgForm.submitted`, whose getter intentionally reads an
|
|
2221
2853
|
* internal signal with `untracked()`.
|
|
2854
|
+
*
|
|
2855
|
+
* Note: when this directive is used outside an `NgForm` (no parent form), no
|
|
2856
|
+
* subscription is wired up and this signal stays `false` for the lifetime of
|
|
2857
|
+
* the directive. Consumers relying on submitted state must host the field
|
|
2858
|
+
* inside an `NgForm` (or `ngxVestForm`).
|
|
2222
2859
|
*/
|
|
2223
|
-
this.formSubmitted = this.#
|
|
2224
|
-
? (() => {
|
|
2225
|
-
const ngForm = this.#ngForm;
|
|
2226
|
-
return toSignal(ngForm.form.events.pipe(filter((event) => event.source === ngForm.form &&
|
|
2227
|
-
(event instanceof FormSubmittedEvent ||
|
|
2228
|
-
event instanceof FormResetEvent)), map((event) => event instanceof FormSubmittedEvent), startWith(ngForm.submitted)), { initialValue: ngForm.submitted });
|
|
2229
|
-
})()
|
|
2230
|
-
: signal(false);
|
|
2860
|
+
this.formSubmitted = this.#formSubmittedState;
|
|
2231
2861
|
/**
|
|
2232
2862
|
* Determines if errors should be shown based on the specified display mode
|
|
2233
|
-
* and the control's state (touched/submitted/
|
|
2863
|
+
* and the control's state (touched/submitted/dirty).
|
|
2234
2864
|
*
|
|
2235
2865
|
* Note: We check both hasErrors (extracted error messages) AND isInvalid (Angular's validation state)
|
|
2236
2866
|
* because in some cases (like conditional validations via validationConfig), the control is marked
|
|
2237
2867
|
* as invalid by Angular before error messages are extracted from Vest. This ensures aria-invalid
|
|
2238
2868
|
* is set correctly even during the validation propagation delay.
|
|
2239
2869
|
*
|
|
2240
|
-
* For validationConfig-triggered validations
|
|
2241
|
-
*
|
|
2242
|
-
*
|
|
2870
|
+
* For validationConfig-triggered validations, a field may become invalid before it has been
|
|
2871
|
+
* touched. Error visibility still respects the field's own `errorDisplayMode`, so untouched
|
|
2872
|
+
* dependent fields can remain visually quiet until blur or submit.
|
|
2243
2873
|
*/
|
|
2244
2874
|
this.shouldShowErrors = computed(() => {
|
|
2245
2875
|
const mode = this.errorDisplayMode();
|
|
@@ -2275,7 +2905,7 @@ class FormErrorDisplayDirective {
|
|
|
2275
2905
|
// Show after blur (touch) OR submit (default behavior)
|
|
2276
2906
|
return !!((isTouched || formSubmitted) && hasErrorState);
|
|
2277
2907
|
}
|
|
2278
|
-
}, ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : []));
|
|
2908
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : /* istanbul ignore next */ []));
|
|
2279
2909
|
/**
|
|
2280
2910
|
* Errors to display (filtered for pending state)
|
|
2281
2911
|
*/
|
|
@@ -2283,7 +2913,7 @@ class FormErrorDisplayDirective {
|
|
|
2283
2913
|
if (this.hasPendingValidation())
|
|
2284
2914
|
return [];
|
|
2285
2915
|
return this.errorMessages();
|
|
2286
|
-
}, ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
2916
|
+
}, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
2287
2917
|
/**
|
|
2288
2918
|
* Warnings to display (filtered for pending state)
|
|
2289
2919
|
*/
|
|
@@ -2291,7 +2921,7 @@ class FormErrorDisplayDirective {
|
|
|
2291
2921
|
if (this.hasPendingValidation())
|
|
2292
2922
|
return [];
|
|
2293
2923
|
return this.warningMessages();
|
|
2294
|
-
}, ...(ngDevMode ? [{ debugName: "warnings" }] : []));
|
|
2924
|
+
}, ...(ngDevMode ? [{ debugName: "warnings" }] : /* istanbul ignore next */ []));
|
|
2295
2925
|
/**
|
|
2296
2926
|
* Whether the control is currently being validated (pending)
|
|
2297
2927
|
* Excludes pristine+untouched controls to prevent "Validating..." on initial load
|
|
@@ -2304,7 +2934,7 @@ class FormErrorDisplayDirective {
|
|
|
2304
2934
|
return false;
|
|
2305
2935
|
}
|
|
2306
2936
|
return this.hasPendingValidation();
|
|
2307
|
-
}, ...(ngDevMode ? [{ debugName: "isPending" }] : []));
|
|
2937
|
+
}, ...(ngDevMode ? [{ debugName: "isPending" }] : /* istanbul ignore next */ []));
|
|
2308
2938
|
/**
|
|
2309
2939
|
* Determines if warnings should be shown based on the specified display mode
|
|
2310
2940
|
* and the control's state (touched/validated/dirty).
|
|
@@ -2345,7 +2975,17 @@ class FormErrorDisplayDirective {
|
|
|
2345
2975
|
// Show after validation runs or after touch/submit (default behavior)
|
|
2346
2976
|
return hasBeenValidated || isTouched || formSubmitted;
|
|
2347
2977
|
}
|
|
2348
|
-
}, ...(ngDevMode ? [{ debugName: "shouldShowWarnings" }] : []));
|
|
2978
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShowWarnings" }] : /* istanbul ignore next */ []));
|
|
2979
|
+
const ngForm = this.#ngForm;
|
|
2980
|
+
if (ngForm) {
|
|
2981
|
+
ngForm.form.events
|
|
2982
|
+
.pipe(filter((event) => event.source === ngForm.form &&
|
|
2983
|
+
(event instanceof FormSubmittedEvent ||
|
|
2984
|
+
event instanceof FormResetEvent)), map((event) => event instanceof FormSubmittedEvent), startWith(ngForm.submitted), takeUntilDestroyed())
|
|
2985
|
+
.subscribe((submitted) => {
|
|
2986
|
+
this.#formSubmittedState.set(submitted);
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2349
2989
|
// Warn about problematic combinations of updateOn and errorDisplayMode
|
|
2350
2990
|
effect(() => {
|
|
2351
2991
|
const mode = this.errorDisplayMode();
|
|
@@ -2355,10 +2995,10 @@ class FormErrorDisplayDirective {
|
|
|
2355
2995
|
}
|
|
2356
2996
|
});
|
|
2357
2997
|
}
|
|
2358
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
2359
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
2998
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormErrorDisplayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2999
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: FormErrorDisplayDirective, isStandalone: true, selector: "[formErrorDisplay], [ngxErrorDisplay]", inputs: { errorDisplayMode: { classPropertyName: "errorDisplayMode", publicName: "errorDisplayMode", isSignal: true, isRequired: false, transformFunction: null }, warningDisplayMode: { classPropertyName: "warningDisplayMode", publicName: "warningDisplayMode", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["formErrorDisplay", "ngxErrorDisplay"], hostDirectives: [{ directive: FormControlStateDirective }], ngImport: i0 }); }
|
|
2360
3000
|
}
|
|
2361
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3001
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormErrorDisplayDirective, decorators: [{
|
|
2362
3002
|
type: Directive,
|
|
2363
3003
|
args: [{
|
|
2364
3004
|
selector: '[formErrorDisplay], [ngxErrorDisplay]',
|
|
@@ -2443,9 +3083,17 @@ function resolveAssociationTargets(controls, mode) {
|
|
|
2443
3083
|
* @returns Object containing the debounced showPendingMessage signal and cleanup function
|
|
2444
3084
|
*/
|
|
2445
3085
|
function createDebouncedPendingState(isPending, options = {}) {
|
|
2446
|
-
|
|
3086
|
+
// Reading the options inside the effect makes the timings reactive: when
|
|
3087
|
+
// a consumer passes an `input()` accessor (a Signal), changes propagate at
|
|
3088
|
+
// runtime rather than getting captured once at construction.
|
|
3089
|
+
const optionsSignal = isSignal(options)
|
|
3090
|
+
? options
|
|
3091
|
+
: null;
|
|
3092
|
+
const staticOptions = optionsSignal
|
|
3093
|
+
? {}
|
|
3094
|
+
: options;
|
|
2447
3095
|
// Create writable signal for debounced state
|
|
2448
|
-
const showPendingMessageSignal = signal(false, ...(ngDevMode ? [{ debugName: "showPendingMessageSignal" }] : []));
|
|
3096
|
+
const showPendingMessageSignal = signal(false, ...(ngDevMode ? [{ debugName: "showPendingMessageSignal" }] : /* istanbul ignore next */ []));
|
|
2449
3097
|
// Track timeouts
|
|
2450
3098
|
let pendingTimeout = null;
|
|
2451
3099
|
let minimumDisplayTimeout = null;
|
|
@@ -2463,6 +3111,9 @@ function createDebouncedPendingState(isPending, options = {}) {
|
|
|
2463
3111
|
// Effect to manage debounced pending message display
|
|
2464
3112
|
effect((onCleanup) => {
|
|
2465
3113
|
const pending = isPending();
|
|
3114
|
+
const current = optionsSignal ? optionsSignal() : staticOptions;
|
|
3115
|
+
const showAfter = current.showAfter ?? 200;
|
|
3116
|
+
const minimumDisplay = current.minimumDisplay ?? 500;
|
|
2466
3117
|
if (pending) {
|
|
2467
3118
|
// Clear any existing minimum display timeout
|
|
2468
3119
|
if (minimumDisplayTimeout) {
|
|
@@ -2647,16 +3298,16 @@ class ControlWrapperComponent {
|
|
|
2647
3298
|
* across multiple child controls.
|
|
2648
3299
|
* - This does not affect whether messages render; it only affects ARIA wiring.
|
|
2649
3300
|
*/
|
|
2650
|
-
this.ariaAssociationMode = input('all-controls', ...(ngDevMode ? [{ debugName: "ariaAssociationMode" }] : []));
|
|
3301
|
+
this.ariaAssociationMode = input('all-controls', ...(ngDevMode ? [{ debugName: "ariaAssociationMode" }] : /* istanbul ignore next */ []));
|
|
2651
3302
|
// Generate unique IDs for ARIA associations
|
|
2652
3303
|
this.uniqueId = `ngx-control-wrapper-${nextUniqueId$2++}`;
|
|
2653
3304
|
this.errorId = `${this.uniqueId}-error`;
|
|
2654
3305
|
this.warningId = `${this.uniqueId}-warning`;
|
|
2655
3306
|
this.pendingId = `${this.uniqueId}-pending`;
|
|
2656
3307
|
// Track form controls found in the wrapper
|
|
2657
|
-
this.formControls = signal([], ...(ngDevMode ? [{ debugName: "formControls" }] : []));
|
|
3308
|
+
this.formControls = signal([], ...(ngDevMode ? [{ debugName: "formControls" }] : /* istanbul ignore next */ []));
|
|
2658
3309
|
// Signals when content is initialized so effects can safely touch the DOM.
|
|
2659
|
-
this.contentInitialized = signal(false, ...(ngDevMode ? [{ debugName: "contentInitialized" }] : []));
|
|
3310
|
+
this.contentInitialized = signal(false, ...(ngDevMode ? [{ debugName: "contentInitialized" }] : /* istanbul ignore next */ []));
|
|
2660
3311
|
// MutationObserver to detect dynamically added/removed controls
|
|
2661
3312
|
this.mutationObserver = null;
|
|
2662
3313
|
/**
|
|
@@ -2688,7 +3339,7 @@ class ControlWrapperComponent {
|
|
|
2688
3339
|
ids.push(this.pendingId);
|
|
2689
3340
|
}
|
|
2690
3341
|
return ids.length > 0 ? ids.join(' ') : null;
|
|
2691
|
-
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
3342
|
+
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
2692
3343
|
/**
|
|
2693
3344
|
* IDs managed by this wrapper when composing aria-describedby.
|
|
2694
3345
|
*
|
|
@@ -2781,10 +3432,10 @@ class ControlWrapperComponent {
|
|
|
2781
3432
|
const controls = this.elementRef.nativeElement.querySelectorAll('input, select, textarea');
|
|
2782
3433
|
this.formControls.set(Array.from(controls));
|
|
2783
3434
|
}
|
|
2784
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
2785
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
3435
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3436
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: ControlWrapperComponent, isStandalone: true, selector: "ngx-control-wrapper, sc-control-wrapper, [scControlWrapper], [ngxControlWrapper], [ngx-control-wrapper], [sc-control-wrapper]", inputs: { ariaAssociationMode: { classPropertyName: "ariaAssociationMode", publicName: "ariaAssociationMode", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.ngx-control-wrapper--invalid": "errorDisplay.shouldShowErrors()", "attr.aria-busy": "errorDisplay.isPending() ? 'true' : null" }, classAttribute: "ngx-control-wrapper sc-control-wrapper" }, hostDirectives: [{ directive: FormErrorDisplayDirective, inputs: ["errorDisplayMode", "errorDisplayMode", "warningDisplayMode", "warningDisplayMode"] }], ngImport: i0, template: "<div class=\"ngx-control-wrapper__content\">\n <ng-content />\n</div>\n\n<!--\n WCAG 2.2 AA Compliance for Inline Field Validation:\n\n IMPORTANT DISTINCTION - Field-level vs Form-level errors:\n - FIELD-LEVEL errors (this component): Use role=\"status\" with aria-live=\"polite\"\n These are inline validation messages that appear as users interact with fields.\n Using \"assertive\" would be too disruptive when users are filling out multiple fields.\n - FORM-LEVEL blocking errors (e.g., submission failed): Should use role=\"alert\" with\n aria-live=\"assertive\" in a separate form-level error summary component.\n\n This component handles FIELD-LEVEL messages:\n - Errors: role=\"status\" with aria-live=\"polite\" (non-disruptive, field is already\n marked with aria-invalid=\"true\" and aria-describedby points here)\n - Warnings: role=\"status\" with aria-live=\"polite\" (informational, doesn't block)\n - Pending: role=\"status\" with aria-live=\"polite\" (status update)\n\n The aria-invalid=\"true\" + aria-describedby association on the input provides\n the primary accessibility pathway; the live region is supplementary.\n\n @see https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA21 - Using aria-invalid\n @see https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA22 - Using role=status\n-->\n<div\n [id]=\"errorId\"\n class=\"text-sm text-red-600\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n>\n @if (errorDisplay.shouldShowErrors()) {\n <ul class=\"m-0 mt-1 list-none space-y-1 p-0\">\n @for (error of errorDisplay.errors(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n }\n</div>\n\n<!--\n Warnings: Non-blocking validation messages (Vest warn())\n - Use role=\"status\" with aria-live=\"polite\" per WCAG ARIA22\n - Default: show after validation runs or touch; configurable to require touch only\n - Warnings do NOT affect field validity - they're informational only\n-->\n<div\n [id]=\"warningId\"\n class=\"text-sm text-yellow-700\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n>\n @if (shouldShowWarnings()) {\n <ul class=\"m-0 mt-1 list-none space-y-1 p-0\">\n @for (warn of errorDisplay.warnings(); track warn) {\n <li>{{ warn }}</li>\n }\n </ul>\n }\n</div>\n\n<!-- Pending state is also stable; content appears only after the debounce delay -->\n<div\n [id]=\"pendingId\"\n class=\"absolute top-0 right-0 flex items-center gap-1 text-xs text-gray-500\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n>\n @if (showPendingMessage()) {\n <span\n class=\"inline-block h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent\"\n aria-hidden=\"true\"\n ></span>\n Validating\u2026\n }\n</div>\n", styles: [":host{display:block;position:relative}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2786
3437
|
}
|
|
2787
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3438
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ControlWrapperComponent, decorators: [{
|
|
2788
3439
|
type: Component,
|
|
2789
3440
|
args: [{ selector: 'ngx-control-wrapper, sc-control-wrapper, [scControlWrapper], [ngxControlWrapper], [ngx-control-wrapper], [sc-control-wrapper]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2790
3441
|
class: 'ngx-control-wrapper sc-control-wrapper',
|
|
@@ -2820,12 +3471,12 @@ class FormGroupWrapperComponent {
|
|
|
2820
3471
|
this.pendingDebounce = input({
|
|
2821
3472
|
showAfter: 500,
|
|
2822
3473
|
minimumDisplay: 500,
|
|
2823
|
-
}, ...(ngDevMode ? [{ debugName: "pendingDebounce" }] : []));
|
|
3474
|
+
}, ...(ngDevMode ? [{ debugName: "pendingDebounce" }] : /* istanbul ignore next */ []));
|
|
2824
3475
|
this.uniqueId = `ngx-form-group-wrapper-${nextUniqueId$1++}`;
|
|
2825
3476
|
this.errorId = `${this.uniqueId}-error`;
|
|
2826
3477
|
this.warningId = `${this.uniqueId}-warning`;
|
|
2827
3478
|
this.pendingId = `${this.uniqueId}-pending`;
|
|
2828
|
-
this.pendingState = createDebouncedPendingState(this.errorDisplay.isPending, this.pendingDebounce
|
|
3479
|
+
this.pendingState = createDebouncedPendingState(this.errorDisplay.isPending, this.pendingDebounce);
|
|
2829
3480
|
this.showPendingMessage = this.pendingState.showPendingMessage;
|
|
2830
3481
|
/**
|
|
2831
3482
|
* Helpful if consumers want to wire aria-describedby manually (e.g. fieldset/legend pattern).
|
|
@@ -2842,12 +3493,12 @@ class FormGroupWrapperComponent {
|
|
|
2842
3493
|
ids.push(this.pendingId);
|
|
2843
3494
|
}
|
|
2844
3495
|
return ids.length > 0 ? ids.join(' ') : null;
|
|
2845
|
-
}, ...(ngDevMode ? [{ debugName: "describedByIds" }] : []));
|
|
3496
|
+
}, ...(ngDevMode ? [{ debugName: "describedByIds" }] : /* istanbul ignore next */ []));
|
|
2846
3497
|
}
|
|
2847
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
2848
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
3498
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormGroupWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3499
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: FormGroupWrapperComponent, isStandalone: true, selector: "ngx-form-group-wrapper, sc-form-group-wrapper, [ngxFormGroupWrapper], [scFormGroupWrapper]", inputs: { pendingDebounce: { classPropertyName: "pendingDebounce", publicName: "pendingDebounce", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.ngx-form-group-wrapper--invalid": "errorDisplay.shouldShowErrors()", "attr.aria-busy": "errorDisplay.isPending() ? 'true' : null" }, classAttribute: "ngx-form-group-wrapper sc-form-group-wrapper" }, exportAs: ["ngxFormGroupWrapper"], hostDirectives: [{ directive: FormErrorDisplayDirective, inputs: ["errorDisplayMode", "errorDisplayMode", "warningDisplayMode", "warningDisplayMode"] }], ngImport: i0, template: "<div class=\"ngx-form-group-wrapper__content\">\n <ng-content />\n</div>\n\n<!--\n Keep regions stable in the DOM so IDs are always valid targets.\n This wrapper does NOT modify descendant controls.\n-->\n<div [id]=\"errorId\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n @if (errorDisplay.shouldShowErrors() && errorDisplay.errors().length > 0) {\n <ul>\n @for (error of errorDisplay.errors(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n }\n</div>\n\n<div [id]=\"warningId\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n @if (errorDisplay.shouldShowWarnings()) {\n <ul>\n @for (warn of errorDisplay.warnings(); track warn) {\n <li>{{ warn }}</li>\n }\n </ul>\n }\n</div>\n\n<div [id]=\"pendingId\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n @if (showPendingMessage()) {\n <span aria-hidden=\"true\">Validating\u2026</span>\n }\n</div>\n", styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2849
3500
|
}
|
|
2850
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3501
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormGroupWrapperComponent, decorators: [{
|
|
2851
3502
|
type: Component,
|
|
2852
3503
|
args: [{ selector: 'ngx-form-group-wrapper, sc-form-group-wrapper, [ngxFormGroupWrapper], [scFormGroupWrapper]', exportAs: 'ngxFormGroupWrapper', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2853
3504
|
class: 'ngx-form-group-wrapper sc-form-group-wrapper',
|
|
@@ -2884,7 +3535,7 @@ class FormErrorControlDirective {
|
|
|
2884
3535
|
* - `single-control`: apply ARIA attributes only when exactly one control is found.
|
|
2885
3536
|
* - `none`: do not mutate descendant controls.
|
|
2886
3537
|
*/
|
|
2887
|
-
this.ariaAssociationMode = input('all-controls', ...(ngDevMode ? [{ debugName: "ariaAssociationMode" }] : []));
|
|
3538
|
+
this.ariaAssociationMode = input('all-controls', ...(ngDevMode ? [{ debugName: "ariaAssociationMode" }] : /* istanbul ignore next */ []));
|
|
2888
3539
|
/**
|
|
2889
3540
|
* Unique ID prefix for this instance.
|
|
2890
3541
|
* Use these IDs to render message regions and to support aria-describedby.
|
|
@@ -2893,8 +3544,8 @@ class FormErrorControlDirective {
|
|
|
2893
3544
|
this.errorId = `${this.uniqueId}-error`;
|
|
2894
3545
|
this.warningId = `${this.uniqueId}-warning`;
|
|
2895
3546
|
this.pendingId = `${this.uniqueId}-pending`;
|
|
2896
|
-
this.formControls = signal([], ...(ngDevMode ? [{ debugName: "formControls" }] : []));
|
|
2897
|
-
this.contentInitialized = signal(false, ...(ngDevMode ? [{ debugName: "contentInitialized" }] : []));
|
|
3547
|
+
this.formControls = signal([], ...(ngDevMode ? [{ debugName: "formControls" }] : /* istanbul ignore next */ []));
|
|
3548
|
+
this.contentInitialized = signal(false, ...(ngDevMode ? [{ debugName: "contentInitialized" }] : /* istanbul ignore next */ []));
|
|
2898
3549
|
this.mutationObserver = null;
|
|
2899
3550
|
this.pendingState = createDebouncedPendingState(this.errorDisplay.isPending, { showAfter: 500, minimumDisplay: 500 });
|
|
2900
3551
|
this.showPendingMessage = this.pendingState.showPendingMessage;
|
|
@@ -2913,7 +3564,7 @@ class FormErrorControlDirective {
|
|
|
2913
3564
|
ids.push(this.pendingId);
|
|
2914
3565
|
}
|
|
2915
3566
|
return ids.length > 0 ? ids.join(' ') : null;
|
|
2916
|
-
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
3567
|
+
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
2917
3568
|
this.ownedDescribedByIds = [
|
|
2918
3569
|
this.errorId,
|
|
2919
3570
|
this.warningId,
|
|
@@ -2987,10 +3638,10 @@ class FormErrorControlDirective {
|
|
|
2987
3638
|
const controls = this.elementRef.nativeElement.querySelectorAll('input, select, textarea');
|
|
2988
3639
|
this.formControls.set(Array.from(controls));
|
|
2989
3640
|
}
|
|
2990
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
2991
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
3641
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormErrorControlDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3642
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: FormErrorControlDirective, isStandalone: true, selector: "[formErrorControl], [ngxErrorControl]", inputs: { ariaAssociationMode: { classPropertyName: "ariaAssociationMode", publicName: "ariaAssociationMode", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["formErrorControl", "ngxErrorControl"], hostDirectives: [{ directive: FormErrorDisplayDirective, inputs: ["errorDisplayMode", "errorDisplayMode", "warningDisplayMode", "warningDisplayMode"] }], ngImport: i0 }); }
|
|
2992
3643
|
}
|
|
2993
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3644
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormErrorControlDirective, decorators: [{
|
|
2994
3645
|
type: Directive,
|
|
2995
3646
|
args: [{
|
|
2996
3647
|
selector: '[formErrorControl], [ngxErrorControl]',
|
|
@@ -3054,7 +3705,7 @@ class FormModelGroupDirective {
|
|
|
3054
3705
|
*
|
|
3055
3706
|
* Defaults to no debounce (`{ debounceTime: 0 }`).
|
|
3056
3707
|
*/
|
|
3057
|
-
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : []));
|
|
3708
|
+
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : /* istanbul ignore next */ []));
|
|
3058
3709
|
this.formDirective = inject(FormDirective, { optional: true });
|
|
3059
3710
|
}
|
|
3060
3711
|
/**
|
|
@@ -3070,8 +3721,8 @@ class FormModelGroupDirective {
|
|
|
3070
3721
|
return getFormGroupField(context.ngForm.control, currentControl);
|
|
3071
3722
|
}, this.validationOptions(), 'FormModelGroupDirective');
|
|
3072
3723
|
}
|
|
3073
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
3074
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
3724
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormModelGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3725
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: FormModelGroupDirective, isStandalone: true, selector: "[ngModelGroup],[ngxModelGroup]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
3075
3726
|
{
|
|
3076
3727
|
provide: NG_ASYNC_VALIDATORS,
|
|
3077
3728
|
useExisting: FormModelGroupDirective,
|
|
@@ -3079,7 +3730,7 @@ class FormModelGroupDirective {
|
|
|
3079
3730
|
},
|
|
3080
3731
|
], ngImport: i0 }); }
|
|
3081
3732
|
}
|
|
3082
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3733
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormModelGroupDirective, decorators: [{
|
|
3083
3734
|
type: Directive,
|
|
3084
3735
|
args: [{
|
|
3085
3736
|
selector: '[ngModelGroup],[ngxModelGroup]',
|
|
@@ -3104,7 +3755,7 @@ class FormModelDirective {
|
|
|
3104
3755
|
*
|
|
3105
3756
|
* Defaults to no debounce (`{ debounceTime: 0 }`).
|
|
3106
3757
|
*/
|
|
3107
|
-
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : []));
|
|
3758
|
+
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : /* istanbul ignore next */ []));
|
|
3108
3759
|
/**
|
|
3109
3760
|
* Reference to the form that needs to be validated
|
|
3110
3761
|
* Injected optionally so that using ngModel outside of an ngxVestForm
|
|
@@ -3127,8 +3778,8 @@ class FormModelDirective {
|
|
|
3127
3778
|
return getFormControlField(context.ngForm.control, currentControl);
|
|
3128
3779
|
}, this.validationOptions(), 'FormModelDirective');
|
|
3129
3780
|
}
|
|
3130
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
3131
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
3781
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
3782
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: FormModelDirective, isStandalone: true, selector: "[ngModel],[ngxModel]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
3132
3783
|
{
|
|
3133
3784
|
provide: NG_ASYNC_VALIDATORS,
|
|
3134
3785
|
useExisting: FormModelDirective,
|
|
@@ -3136,7 +3787,7 @@ class FormModelDirective {
|
|
|
3136
3787
|
},
|
|
3137
3788
|
], ngImport: i0 }); }
|
|
3138
3789
|
}
|
|
3139
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
3790
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FormModelDirective, decorators: [{
|
|
3140
3791
|
type: Directive,
|
|
3141
3792
|
args: [{
|
|
3142
3793
|
selector: '[ngModel],[ngxModel]',
|
|
@@ -3225,26 +3876,30 @@ class ValidateRootFormDirective {
|
|
|
3225
3876
|
constructor() {
|
|
3226
3877
|
this.injector = inject(Injector);
|
|
3227
3878
|
this.destroyRef = inject(DestroyRef);
|
|
3228
|
-
this.lastControl = signal(null, ...(ngDevMode ? [{ debugName: "lastControl" }] : []));
|
|
3229
|
-
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : []));
|
|
3230
|
-
this.hasSubmitted = signal(false, ...(ngDevMode ? [{ debugName: "hasSubmitted" }] : []));
|
|
3231
|
-
this.formValue = input(null, ...(ngDevMode ? [{ debugName: "formValue" }] : []));
|
|
3232
|
-
this.suite = input(null, ...(ngDevMode ? [{ debugName: "suite" }] : []));
|
|
3879
|
+
this.lastControl = signal(null, ...(ngDevMode ? [{ debugName: "lastControl" }] : /* istanbul ignore next */ []));
|
|
3880
|
+
this.validationOptions = input({ debounceTime: 0 }, ...(ngDevMode ? [{ debugName: "validationOptions" }] : /* istanbul ignore next */ []));
|
|
3881
|
+
this.hasSubmitted = signal(false, ...(ngDevMode ? [{ debugName: "hasSubmitted" }] : /* istanbul ignore next */ []));
|
|
3882
|
+
this.formValue = input(null, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
3883
|
+
this.suite = input(null, ...(ngDevMode ? [{ debugName: "suite" }] : /* istanbul ignore next */ []));
|
|
3233
3884
|
/**
|
|
3234
3885
|
* Whether the root form should be validated or not
|
|
3235
3886
|
* This will use the field rootForm
|
|
3236
3887
|
* Accepts both validateRootForm and ngxValidateRootForm
|
|
3237
3888
|
*/
|
|
3238
|
-
this.validateRootForm = input(false, { ...(ngDevMode ? { debugName: "validateRootForm" } : {}), transform: booleanAttribute });
|
|
3239
|
-
this.ngxValidateRootForm = input(false, { ...(ngDevMode ? { debugName: "ngxValidateRootForm" } : {}), transform: booleanAttribute });
|
|
3889
|
+
this.validateRootForm = input(false, { ...(ngDevMode ? { debugName: "validateRootForm" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
3890
|
+
this.ngxValidateRootForm = input(false, { ...(ngDevMode ? { debugName: "ngxValidateRootForm" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
3240
3891
|
/**
|
|
3241
3892
|
* Validation mode:
|
|
3242
|
-
* - 'submit' (default): Only validates after form submission
|
|
3243
|
-
* - 'live'
|
|
3244
|
-
*
|
|
3893
|
+
* - `'submit'` (effective default): Only validates after form submission.
|
|
3894
|
+
* - `'live'`: Validates on every value change.
|
|
3895
|
+
*
|
|
3896
|
+
* Both inputs default to `undefined` so we can detect whether the consumer
|
|
3897
|
+
* set them explicitly. Precedence is `ngx ?? legacy ?? 'submit'`, which
|
|
3898
|
+
* matches the documented behavior — observable only when both attributes
|
|
3899
|
+
* are set explicitly on the same form.
|
|
3245
3900
|
*/
|
|
3246
|
-
this.validateRootFormMode = input(
|
|
3247
|
-
this.ngxValidateRootFormMode = input(
|
|
3901
|
+
this.validateRootFormMode = input(undefined, ...(ngDevMode ? [{ debugName: "validateRootFormMode" }] : /* istanbul ignore next */ []));
|
|
3902
|
+
this.ngxValidateRootFormMode = input(undefined, ...(ngDevMode ? [{ debugName: "ngxValidateRootFormMode" }] : /* istanbul ignore next */ []));
|
|
3248
3903
|
// Convert signals to Observables in injection context
|
|
3249
3904
|
this.hasSubmitted$ = toObservable(this.hasSubmitted);
|
|
3250
3905
|
this.formValue$ = toObservable(this.formValue);
|
|
@@ -3269,7 +3924,9 @@ class ValidateRootFormDirective {
|
|
|
3269
3924
|
if (ngForm?.control) {
|
|
3270
3925
|
// Defer to the next microtask so Angular has a chance to finish
|
|
3271
3926
|
// wiring up controls/groups (ngModel/ngModelGroup) on initial render.
|
|
3272
|
-
|
|
3927
|
+
// The scheduleMicrotask primitive auto-cancels if the directive is
|
|
3928
|
+
// destroyed before the microtask fires.
|
|
3929
|
+
scheduleMicrotask(() => ngForm.control.updateValueAndValidity(), this.destroyRef);
|
|
3273
3930
|
}
|
|
3274
3931
|
});
|
|
3275
3932
|
}
|
|
@@ -3291,7 +3948,7 @@ class ValidateRootFormDirective {
|
|
|
3291
3948
|
// Ensure we run at least one validation pass after the form is ready.
|
|
3292
3949
|
// This matters for 'live' mode root-form errors that should appear
|
|
3293
3950
|
// without requiring a user interaction.
|
|
3294
|
-
|
|
3951
|
+
scheduleMicrotask(() => ngForm.control.updateValueAndValidity(), this.destroyRef);
|
|
3295
3952
|
// Subscribe to form submission to set hasSubmitted flag
|
|
3296
3953
|
ngForm.ngSubmit
|
|
3297
3954
|
.pipe(tap(() => {
|
|
@@ -3309,10 +3966,12 @@ class ValidateRootFormDirective {
|
|
|
3309
3966
|
if (!isEnabled) {
|
|
3310
3967
|
return of(null);
|
|
3311
3968
|
}
|
|
3312
|
-
//
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3969
|
+
// Mode precedence: ngx-prefixed input wins over legacy input; both default
|
|
3970
|
+
// to `undefined` so the precedence rule is implementable without losing
|
|
3971
|
+
// the legacy attribute when the new one is not set.
|
|
3972
|
+
const mode = this.ngxValidateRootFormMode() ??
|
|
3973
|
+
this.validateRootFormMode() ??
|
|
3974
|
+
'submit';
|
|
3316
3975
|
// In 'submit' mode, skip validation until form is submitted
|
|
3317
3976
|
if (mode === 'submit' && !this.hasSubmitted()) {
|
|
3318
3977
|
return of(null);
|
|
@@ -3373,8 +4032,8 @@ class ValidateRootFormDirective {
|
|
|
3373
4032
|
}), take(1), takeUntilDestroyed(this.destroyRef));
|
|
3374
4033
|
};
|
|
3375
4034
|
}
|
|
3376
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
3377
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
4035
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ValidateRootFormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
4036
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: ValidateRootFormDirective, isStandalone: true, selector: "form[validateRootForm], form[ngxValidateRootForm]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null }, 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 }, ngxValidateRootForm: { classPropertyName: "ngxValidateRootForm", publicName: "ngxValidateRootForm", isSignal: true, isRequired: false, transformFunction: null }, validateRootFormMode: { classPropertyName: "validateRootFormMode", publicName: "validateRootFormMode", isSignal: true, isRequired: false, transformFunction: null }, ngxValidateRootFormMode: { classPropertyName: "ngxValidateRootFormMode", publicName: "ngxValidateRootFormMode", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
3378
4037
|
{
|
|
3379
4038
|
provide: NG_ASYNC_VALIDATORS,
|
|
3380
4039
|
useExisting: ValidateRootFormDirective,
|
|
@@ -3382,7 +4041,7 @@ class ValidateRootFormDirective {
|
|
|
3382
4041
|
},
|
|
3383
4042
|
], ngImport: i0 }); }
|
|
3384
4043
|
}
|
|
3385
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
4044
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ValidateRootFormDirective, decorators: [{
|
|
3386
4045
|
type: Directive,
|
|
3387
4046
|
args: [{
|
|
3388
4047
|
selector: 'form[validateRootForm], form[ngxValidateRootForm]',
|
|
@@ -3609,7 +4268,7 @@ class ValidationConfigBuilder {
|
|
|
3609
4268
|
*/
|
|
3610
4269
|
whenChanged(trigger, revalidate) {
|
|
3611
4270
|
const deps = Array.isArray(revalidate) ? revalidate : [revalidate];
|
|
3612
|
-
const existing = this.config[trigger] || [];
|
|
4271
|
+
const existing = (this.config[trigger] || []);
|
|
3613
4272
|
// Development mode warning for duplicate dependents
|
|
3614
4273
|
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
3615
4274
|
const duplicates = deps.filter((d) => existing.includes(d));
|
|
@@ -4231,7 +4890,7 @@ function keepFieldsWhen(currentState, conditions) {
|
|
|
4231
4890
|
const result = {};
|
|
4232
4891
|
for (const fieldName of Object.keys(conditions)) {
|
|
4233
4892
|
const shouldKeep = conditions[fieldName];
|
|
4234
|
-
if (shouldKeep && fieldName
|
|
4893
|
+
if (shouldKeep && Object.hasOwn(currentState, fieldName)) {
|
|
4235
4894
|
result[fieldName] = currentState[fieldName];
|
|
4236
4895
|
}
|
|
4237
4896
|
}
|
|
@@ -4246,5 +4905,5 @@ function keepFieldsWhen(currentState, conditions) {
|
|
|
4246
4905
|
* Generated bundle index. Do not edit.
|
|
4247
4906
|
*/
|
|
4248
4907
|
|
|
4249
|
-
export { ControlWrapperComponent, DEFAULT_FOCUS_SELECTOR, DEFAULT_INVALID_SELECTOR, FormControlStateDirective, FormDirective, FormErrorControlDirective, FormErrorDisplayDirective, FormGroupWrapperComponent, FormModelDirective, FormModelGroupDirective, NGX_ERROR_DISPLAY_MODE_TOKEN, NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, NGX_WARNING_DISPLAY_MODE_TOKEN, NgxVestForms, ROOT_FORM, ROOT_FORM as ROOT_FORM_CONSTANT, SC_ERROR_DISPLAY_MODE_TOKEN, ValidateRootFormDirective, ValidationConfigBuilder, arrayToObject, clearFields, clearFieldsWhen, cloneDeep, createDebouncedPendingState, createEmptyFormState, createValidationConfig, deepArrayToObject, fastDeepEqual, getAllFormErrors, getFormControlField, getFormGroupField, keepFieldsWhen, mergeAriaDescribedBy, mergeValuesAndRawValues, objectToArray, parseAriaIdTokens, parseFieldPath, resolveAssociationTargets, set, setValueAtPath, shallowEqual, stringifyFieldPath, validateShape, vestForms, vestFormsViewProviders };
|
|
4908
|
+
export { ControlWrapperComponent, DEFAULT_FOCUS_SELECTOR, DEFAULT_INVALID_SELECTOR, FormControlStateDirective, FormDirective, FormErrorControlDirective, FormErrorDisplayDirective, FormGroupWrapperComponent, FormModelDirective, FormModelGroupDirective, NGX_EQUALITY_FN, NGX_ERROR_DISPLAY_MODE_TOKEN, NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN, NGX_WARNING_DISPLAY_MODE_TOKEN, NgxVestForms, ROOT_FORM, ROOT_FORM as ROOT_FORM_CONSTANT, SC_ERROR_DISPLAY_MODE_TOKEN, ValidateRootFormDirective, ValidationConfigBuilder, arrayToObject, clearFields, clearFieldsWhen, cloneDeep, createDebouncedPendingState, createEmptyFormState, createValidationConfig, deepArrayToObject, fastDeepEqual, getAllFormErrors, getFormControlField, getFormGroupField, keepFieldsWhen, mergeAriaDescribedBy, mergeValuesAndRawValues, objectToArray, parseAriaIdTokens, parseFieldPath, resolveAssociationTargets, set, setValueAtPath, shallowEqual, stringifyFieldPath, validateShape, vestForms, vestFormsViewProviders };
|
|
4250
4909
|
//# sourceMappingURL=ngx-vest-forms.mjs.map
|