@ng-forge/dynamic-forms 0.6.0 → 0.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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, isSignal, untracked, isDevMode, isWritableSignal, Injector, DestroyRef, computed, input, linkedSignal, ChangeDetectionStrategy, Component, signal, runInInjectionContext, afterNextRender, model, makeEnvironmentProviders, Pipe } from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, isSignal, untracked, isWritableSignal, isDevMode, Injector, DestroyRef, computed, input, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, signal, runInInjectionContext, afterNextRender, model, EnvironmentInjector, makeEnvironmentProviders, Pipe } from '@angular/core';
3
3
  import { NgComponentOutlet } from '@angular/common';
4
- import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
4
+ import { toObservable, toSignal, takeUntilDestroyed, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
5
5
  import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateStandardSchema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
6
- import { isObservable, firstValueFrom, switchMap, EMPTY, from, Subject, filter, debounceTime, distinctUntilChanged, tap, startWith, pairwise, map as map$1, Observable, combineLatestWith, auditTime, exhaustMap, scheduled, queueScheduler, of, merge, timer, catchError as catchError$1, concat, defer, combineLatest, pipe, forkJoin, scan as scan$1, concatMap, take as take$1 } from 'rxjs';
7
- import { scan, map, filter as filter$1, take, switchMap as switchMap$1, shareReplay, catchError } from 'rxjs/operators';
6
+ import { isObservable, firstValueFrom, exhaustMap, EMPTY, from, catchError, Subject, filter, TimeoutError, of, throwError, debounceTime, distinctUntilChanged, tap, take as take$1, startWith, pairwise, map as map$1, switchMap as switchMap$1, Observable, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer, defer, combineLatest, pipe, forkJoin, scan as scan$1, concatMap } from 'rxjs';
7
+ import { scan, map, filter as filter$1, take, switchMap, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
8
8
  import { explicitEffect } from 'ngxtension/explicit-effect';
9
9
  import { HttpClient } from '@angular/common/http';
10
10
  import { isStandardSchemaMarker } from '@ng-forge/dynamic-forms/schema';
@@ -14,12 +14,11 @@ import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
14
14
  * Wraps a submission action to handle both Promise and Observable returns.
15
15
  * Converts Observables to Promises for compatibility with Angular Signal Forms' submit().
16
16
  *
17
- * The return type uses `void` to match Angular Signal Forms' expected action signature.
18
- * The actual result from the action is preserved but typed as void since Angular's
19
- * submit() function handles the result internally.
17
+ * Errors are NOT caught here — they propagate so that submit() can reject its Promise,
18
+ * allowing the caller's catchError to log and keep the submission stream alive.
20
19
  *
21
20
  * @param action - The submission action function
22
- * @returns A wrapped function that always returns a Promise
21
+ * @returns A wrapped function that returns a Promise
23
22
  */
24
23
  function wrapSubmissionAction(action) {
25
24
  return async (formTree) => {
@@ -39,7 +38,7 @@ function wrapSubmissionAction(action) {
39
38
  * - Listens for submit events from the event bus
40
39
  * - If a submission.action is configured, wraps it and uses Angular Signal Forms' submit()
41
40
  * - Handles both Promise and Observable returns from the action
42
- * - Uses switchMap to cancel any in-flight submission when a new one starts
41
+ * - Uses exhaustMap to ignore new submissions while one is in-flight (first-submit-wins)
43
42
  *
44
43
  * The returned Observable should be subscribed to with takeUntilDestroyed() in the component.
45
44
  *
@@ -59,14 +58,24 @@ function wrapSubmissionAction(action) {
59
58
  * ```
60
59
  */
61
60
  function createSubmissionHandler(options) {
62
- const { eventBus, configSignal, formSignal } = options;
63
- return eventBus.on('submit').pipe(switchMap(() => {
61
+ const { eventBus, configSignal, formSignal, validSignal, logger } = options;
62
+ // exhaustMap ensures first-submit-wins: a second submit event while the first
63
+ // is in-flight is silently dropped rather than cancelling the running Promise.
64
+ // switchMap would unsubscribe the Observable wrapper but cannot cancel the
65
+ // underlying Promise, causing both side effects to execute.
66
+ return eventBus.on('submit').pipe(exhaustMap(() => {
64
67
  const submissionConfig = configSignal().submission;
65
68
  // If no submission action is configured, let the submitted output handle it
66
69
  // This maintains backward compatibility for users handling submission manually
67
70
  if (!submissionConfig?.action) {
68
71
  return EMPTY;
69
72
  }
73
+ // Guard: match the (submitted) output's safety contract — reject submission
74
+ // when the form is invalid or has pending async validators.
75
+ if (!validSignal()) {
76
+ logger.debug('Submission action skipped: form is not valid (invalid or pending async validators)');
77
+ return EMPTY;
78
+ }
70
79
  // Type assertion needed: submission.action accepts the form tree but its signature
71
80
  // is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.
72
81
  const wrappedAction = wrapSubmissionAction(submissionConfig.action);
@@ -75,7 +84,12 @@ function createSubmissionHandler(options) {
75
84
  // - Sets form.submitting() to true during execution
76
85
  // - Applies server errors to form fields on completion
77
86
  // - Sets form.submitting() to false when done
78
- return from(submit(formSignal(), wrappedAction));
87
+ // catchError keeps the exhaustMap stream alive after action failure —
88
+ // without it, an unhandled error would terminate all future submissions.
89
+ return from(submit(formSignal(), wrappedAction)).pipe(catchError((error) => {
90
+ logger.error('Submission action failed:', error);
91
+ return EMPTY;
92
+ }));
79
93
  }));
80
94
  }
81
95
 
@@ -331,6 +345,33 @@ function createArrayItemIdGenerator() {
331
345
  return () => `item-${counter++}`;
332
346
  }
333
347
 
348
+ /**
349
+ * Injection token for the dynamic forms logger.
350
+ *
351
+ * Provided by provideDynamicForm() with ConsoleLogger as default.
352
+ * Override with withLoggerConfig(false) to disable logging.
353
+ */
354
+ const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
355
+
356
+ /**
357
+ * No-operation logger implementation.
358
+ * All methods are no-ops - used in production by default.
359
+ */
360
+ class NoopLogger {
361
+ debug() {
362
+ // Intentionally empty
363
+ }
364
+ info() {
365
+ // Intentionally empty
366
+ }
367
+ warn() {
368
+ // Intentionally empty
369
+ }
370
+ error() {
371
+ // Intentionally empty
372
+ }
373
+ }
374
+
334
375
  /**
335
376
  * Safely attempts to inject a dependency using an InjectionToken.
336
377
  * Returns the default value if called outside an injection context.
@@ -364,77 +405,83 @@ function attachFormValue(event, formValue) {
364
405
  return Object.assign(Object.create(prototype), event, { formValue });
365
406
  }
366
407
  /**
367
- * Event bus service for form-wide event communication.
408
+ * Event bus for form-wide event communication between field components.
368
409
  *
369
- * Provides type-safe event dispatching and subscription for form components
370
- * to communicate without direct coupling. Uses RxJS observables for reactive
371
- * event handling and supports both single event type and multiple event type subscriptions.
410
+ * **Intended for use inside DynamicForm** — i.e. within custom field components,
411
+ * field mappers, or other services that live inside the form's component injector.
412
+ * Inject it directly to dispatch or observe events from within a field:
372
413
  *
373
- * @example
374
414
  * ```typescript
375
- * @Component({
376
- * providers: [EventBus]
377
- * })
378
- * export class MyFormComponent {
379
- * constructor(private eventBus: EventBus) {
380
- * // Subscribe to form submission events
381
- * this.eventBus.on<SubmitEvent>('submit').subscribe(() => {
382
- * console.log('Form was submitted');
383
- * });
384
- *
385
- * // Subscribe to multiple event types
386
- * this.eventBus.on<SubmitEvent | FormResetEvent>(['submit', 'form-reset']).subscribe(event => {
387
- * console.log('Form event:', event.type);
388
- * });
389
- * }
415
+ * // Inside a custom field component
416
+ * export class MyFieldComponent {
417
+ * private readonly eventBus = inject(EventBus);
390
418
  *
391
- * onSubmit() {
392
- * // Dispatch a submit event
393
- * this.eventBus.dispatch(SubmitEvent);
419
+ * onAction() {
420
+ * this.eventBus.dispatch(arrayEvent('items').append(template));
394
421
  * }
395
422
  * }
396
423
  * ```
424
+ *
425
+ * **If you are outside DynamicForm** (e.g. in a host component or a service that wraps
426
+ * the form), use {@link EventDispatcher} instead. Injecting `EventBus` in a parent
427
+ * component gives you a *different instance* that the form knows nothing about.
428
+ *
429
+ * `EventBus` is scoped to each `DynamicForm` instance via `provideDynamicFormDI()`.
430
+ * It provides type-safe dispatching and RxJS-based subscription for all form events.
431
+ *
432
+ * @see EventDispatcher — for dispatching events from outside the form
397
433
  */
434
+ const FALLBACK_LOGGER = new NoopLogger();
398
435
  class EventBus {
399
436
  pipeline$ = new Subject();
400
437
  globalEmitFormValue = safeInjectToken(EMIT_FORM_VALUE_ON_EVENTS, false);
401
438
  rootFormRegistry = safeInjectClass(RootFormRegistryService, null);
402
439
  formOptions = safeInjectToken(FORM_OPTIONS, null);
440
+ logger = safeInjectToken(DynamicFormLogger, FALLBACK_LOGGER);
403
441
  events$ = this.pipeline$.asObservable();
442
+ dispatch(eventOrConstructor, ...args) {
443
+ if (typeof eventOrConstructor !== 'function' && 'type' in eventOrConstructor) {
444
+ this.emit(eventOrConstructor);
445
+ }
446
+ else {
447
+ this.emit(new eventOrConstructor(...args));
448
+ }
449
+ }
404
450
  /**
405
- * Dispatches an event to all subscribers.
406
- *
407
- * Creates an instance of the provided event constructor and broadcasts it
408
- * through the event pipeline to all active subscribers.
409
- *
410
- * If `withEventFormValue()` is enabled globally or `options.emitFormValueOnEvents`
411
- * is set to `true` in the form config, the current form value will be attached
412
- * to the event's `formValue` property.
413
- *
414
- * @param eventConstructor - Constructor function for the event to dispatch
415
- *
416
- * @example
417
- * ```typescript
418
- * // Dispatch a submit event
419
- * eventBus.dispatch(SubmitEvent);
420
- *
421
- * // Dispatch a custom event
422
- * eventBus.dispatch(CustomFormEvent);
423
- * ```
451
+ * Dispatches a pre-created event instance directly.
452
+ * Used internally by EventDispatcher to forward events into the bus.
453
+ * @internal
454
+ */
455
+ emitInstance(event) {
456
+ this.emit(event);
457
+ }
458
+ /**
459
+ * Shared emit path for both dispatch() and emitInstance().
460
+ * Attaches form value if configured, then pushes to the pipeline.
424
461
  */
425
- // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor
426
- dispatch(eventConstructor, ...args) {
427
- const event = new eventConstructor(...args);
462
+ emit(event) {
428
463
  if (this.shouldEmitFormValue()) {
429
464
  const formValue = this.rootFormRegistry?.formValue();
430
465
  // Only attach if form value exists and has at least one field.
431
466
  // Empty objects {} are not attached - use hasFormValue() to check.
432
467
  if (formValue && Object.keys(formValue).length > 0) {
433
- this.pipeline$.next(attachFormValue(event, formValue));
468
+ this.safeEmit(attachFormValue(event, formValue));
434
469
  return;
435
470
  }
436
471
  }
437
- this.pipeline$.next(event);
472
+ this.safeEmit(event);
473
+ }
474
+ /**
475
+ * Emits an event through the pipeline. Catches any synchronous exception that escapes
476
+ * RxJS's own error handling so dispatch() callers are never disrupted by a failing subscriber.
477
+ */
478
+ safeEmit(event) {
479
+ try {
480
+ this.pipeline$.next(event);
481
+ }
482
+ catch (err) {
483
+ this.logger.error('[Dynamic Forms] Exception in EventBus subscriber during dispatch', err);
484
+ }
438
485
  }
439
486
  /**
440
487
  * Determines whether form value should be attached to events.
@@ -485,10 +532,10 @@ class EventBus {
485
532
  }
486
533
  return this.pipeline$.pipe(filter((event) => event.type === eventType));
487
534
  }
488
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
489
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: EventBus });
535
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
536
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventBus });
490
537
  }
491
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: EventBus, decorators: [{
538
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventBus, decorators: [{
492
539
  type: Injectable
493
540
  }] });
494
541
 
@@ -506,6 +553,15 @@ class ComponentInitializedEvent {
506
553
  }
507
554
  }
508
555
 
556
+ /**
557
+ * Injection token for configuring the initialization timeout in milliseconds.
558
+ * Defaults to 10 seconds. When the timeout is reached, a warning is logged
559
+ * and (initialized) emits true as a best-effort fallback.
560
+ */
561
+ const INITIALIZATION_TIMEOUT_MS = new InjectionToken('INITIALIZATION_TIMEOUT_MS', {
562
+ providedIn: 'root',
563
+ factory: () => 10_000,
564
+ });
509
565
  /**
510
566
  * Creates an observable that tracks component initialization progress.
511
567
  *
@@ -568,8 +624,10 @@ function createDetailedInitializationTracker(eventBus, expectedCount) {
568
624
  * Uses shareReplay({ bufferSize: 1, refCount: false }) to ensure exactly one emission
569
625
  * that can be received by late subscribers and keeps the subscription alive.
570
626
  *
571
- * This utility function replaces the manual ReplaySubject pattern for tracking initialization.
572
- * The count includes container components (dynamic-form, pages, rows, groups).
627
+ * Includes a configurable timeout (default 10s via INITIALIZATION_TIMEOUT_MS)
628
+ * so that (initialized) does not hang forever if a container component throws
629
+ * before emitting its initialization event. On timeout, a warning is logged
630
+ * and true is emitted as a best-effort fallback.
573
631
  *
574
632
  * @param options - Configuration options for initialization tracking
575
633
  * @returns Observable<boolean> that emits true when all components are initialized
@@ -589,12 +647,29 @@ function createDetailedInitializationTracker(eventBus, expectedCount) {
589
647
  */
590
648
  function setupInitializationTracking(options) {
591
649
  const { eventBus, totalComponentsCount, injector, componentId } = options;
592
- return toObservable(totalComponentsCount, { injector }).pipe(take(1), switchMap$1((count) => {
650
+ const timeoutMs = injector.get(INITIALIZATION_TIMEOUT_MS);
651
+ const logger = injector.get(DynamicFormLogger);
652
+ return toObservable(totalComponentsCount, { injector }).pipe(take(1), switchMap((count) => {
653
+ let tracking$;
593
654
  if (count === 1) {
594
655
  // Only dynamic-form component, emit immediately when it initializes
595
- return eventBus.on('component-initialized').pipe(filter$1((event) => event.componentType === 'dynamic-form' && event.componentId === componentId), map(() => true), take(1));
656
+ tracking$ = eventBus.on('component-initialized').pipe(filter$1((event) => event.componentType === 'dynamic-form' && event.componentId === componentId), map(() => true), take(1));
596
657
  }
597
- return createInitializationTracker(eventBus, count);
658
+ else {
659
+ tracking$ = createInitializationTracker(eventBus, count);
660
+ }
661
+ // Timeout guard: emit best-effort true if a container throws before initializing
662
+ return tracking$.pipe(timeout(timeoutMs), catchError$1((error) => {
663
+ if (error instanceof TimeoutError) {
664
+ logger.warn(`Initialization timed out after ${timeoutMs}ms. ` +
665
+ `Expected ${count} component(s) to initialize but not all reported in time. ` +
666
+ 'This may indicate a container component threw during initialization. ' +
667
+ 'Emitting (initialized) as best-effort.');
668
+ // Emit true as best-effort so consumers are not stuck waiting forever
669
+ return of(true);
670
+ }
671
+ return throwError(() => error);
672
+ }));
598
673
  }), shareReplay({ bufferSize: 1, refCount: false }));
599
674
  }
600
675
 
@@ -747,14 +822,6 @@ function isDisplayOnlyField(field) {
747
822
  return field.type === 'text' || isContainerField(field);
748
823
  }
749
824
 
750
- /**
751
- * Injection token for the dynamic forms logger.
752
- *
753
- * Provided by provideDynamicForm() with ConsoleLogger as default.
754
- * Override with withLoggerConfig(false) to disable logging.
755
- */
756
- const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
757
-
758
825
  /**
759
826
  * Default debounce duration in milliseconds.
760
827
  */
@@ -1173,12 +1240,12 @@ function assertNever(value) {
1173
1240
  * - Example: `noSpaces: (ctx) => ctx.value().includes(' ') ? { kind: 'noSpaces' } : null`
1174
1241
  *
1175
1242
  * 3. **Async Validators** - For asynchronous validation (debouncing, database lookups, etc.)
1176
- * - Used in: validators array on fields with type 'customAsync'
1243
+ * - Used in: validators array on fields with type 'async'
1177
1244
  * - Return type: Observable<ValidationError | ValidationError[] | null>
1178
1245
  * - Example: `checkUsername: (ctx) => userService.checkAvailability(ctx.value())`
1179
1246
  *
1180
1247
  * 4. **HTTP Validators** - For HTTP-based validation with automatic request cancellation
1181
- * - Used in: validators array on fields with type 'customHttp'
1248
+ * - Used in: validators array on fields with type 'http'
1182
1249
  * - Configuration object with url, method, mapResponse, etc.
1183
1250
  * - Example: `{ url: '/api/check', method: 'GET', mapResponse: ... }`
1184
1251
  *
@@ -1225,8 +1292,6 @@ class FunctionRegistryService {
1225
1292
  customFunctions = new Map();
1226
1293
  customFunctionScopes = new Map();
1227
1294
  derivationFunctions = new Map();
1228
- // TODO(@ng-forge): remove deprecated code in next minor
1229
- propertyDerivationFunctions = new Map();
1230
1295
  asyncDerivationFunctions = new Map();
1231
1296
  asyncConditionFunctions = new Map();
1232
1297
  validators = new Map();
@@ -1338,50 +1403,6 @@ class FunctionRegistryService {
1338
1403
  this.derivationFunctions.clear();
1339
1404
  }
1340
1405
  // ─────────────────────────────────────────────────────────────────────────────
1341
- // Property Derivation Functions
1342
- // TODO(@ng-forge): remove deprecated code in next minor
1343
- // ─────────────────────────────────────────────────────────────────────────────
1344
- /**
1345
- * Register a property derivation function.
1346
- *
1347
- * Property derivation functions compute derived values for field properties
1348
- * (like `minDate`, `options`, `label`) and are called when a
1349
- * `PropertyDerivationLogicConfig` references them by `functionName`.
1350
- *
1351
- * @param name - Unique identifier for the function
1352
- * @param fn - Function that receives EvaluationContext and returns the derived property value
1353
- */
1354
- registerPropertyDerivationFunction(name, fn) {
1355
- this.propertyDerivationFunctions.set(name, fn);
1356
- }
1357
- /**
1358
- * Get a property derivation function by name
1359
- */
1360
- getPropertyDerivationFunction(name) {
1361
- return this.propertyDerivationFunctions.get(name) ?? this.derivationFunctions.get(name);
1362
- }
1363
- /**
1364
- * Get all property derivation functions as an object
1365
- */
1366
- getPropertyDerivationFunctions() {
1367
- return Object.fromEntries(this.propertyDerivationFunctions);
1368
- }
1369
- /**
1370
- * Set property derivation functions from a config object.
1371
- * Only updates functions if their references have changed.
1372
- *
1373
- * @param functions - Object mapping function names to property derivation functions
1374
- */
1375
- setPropertyDerivationFunctions(functions) {
1376
- this.setRegistryIfChanged(this.propertyDerivationFunctions, functions);
1377
- }
1378
- /**
1379
- * Clear all property derivation functions
1380
- */
1381
- clearPropertyDerivationFunctions() {
1382
- this.propertyDerivationFunctions.clear();
1383
- }
1384
- // ─────────────────────────────────────────────────────────────────────────────
1385
1406
  // Async Derivation Functions
1386
1407
  // ─────────────────────────────────────────────────────────────────────────────
1387
1408
  /**
@@ -1682,17 +1703,16 @@ class FunctionRegistryService {
1682
1703
  clearAll() {
1683
1704
  this.clearCustomFunctions();
1684
1705
  this.clearDerivationFunctions();
1685
- this.clearPropertyDerivationFunctions();
1686
1706
  this.clearAsyncDerivationFunctions();
1687
1707
  this.clearAsyncConditionFunctions();
1688
1708
  this.clearValidators();
1689
1709
  this.clearAsyncValidators();
1690
1710
  this.clearHttpValidators();
1691
1711
  }
1692
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1693
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FunctionRegistryService });
1712
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1713
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FunctionRegistryService });
1694
1714
  }
1695
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FunctionRegistryService, decorators: [{
1715
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FunctionRegistryService, decorators: [{
1696
1716
  type: Injectable
1697
1717
  }] });
1698
1718
 
@@ -1731,10 +1751,10 @@ class SchemaRegistryService {
1731
1751
  }
1732
1752
  return schema;
1733
1753
  }
1734
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1735
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SchemaRegistryService });
1754
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1755
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: SchemaRegistryService });
1736
1756
  }
1737
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SchemaRegistryService, decorators: [{
1757
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: SchemaRegistryService, decorators: [{
1738
1758
  type: Injectable
1739
1759
  }] });
1740
1760
 
@@ -2234,6 +2254,9 @@ class FieldContextRegistryService {
2234
2254
  }
2235
2255
  const resolved = {};
2236
2256
  for (const [key, value] of Object.entries(externalDataRecord)) {
2257
+ if (!isSignal(value)) {
2258
+ throw new DynamicFormError(`externalData["${key}"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);
2259
+ }
2237
2260
  resolved[key] = reactive ? value() : untracked(() => value());
2238
2261
  }
2239
2262
  return resolved;
@@ -2312,10 +2335,10 @@ class FieldContextRegistryService {
2312
2335
  },
2313
2336
  };
2314
2337
  }
2315
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2316
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FieldContextRegistryService });
2338
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2339
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FieldContextRegistryService });
2317
2340
  }
2318
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FieldContextRegistryService, decorators: [{
2341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FieldContextRegistryService, decorators: [{
2319
2342
  type: Injectable
2320
2343
  }] });
2321
2344
 
@@ -2643,24 +2666,6 @@ function countArrayPlaceholders(path) {
2643
2666
  return path.split('.').filter((part) => part === '$').length;
2644
2667
  }
2645
2668
 
2646
- /**
2647
- * Emits a deprecation warning once per unique key in dev mode only.
2648
- * Subsequent calls with the same `id` are silently ignored.
2649
- *
2650
- * @param logger - The DynamicFormLogger instance for output
2651
- * @param tracker - DI-scoped tracker to deduplicate warnings
2652
- * @param id - Unique identifier for this deprecation (e.g., 'type:propertyDerivation')
2653
- * @param message - Human-readable deprecation message
2654
- *
2655
- * @public
2656
- */
2657
- function warnDeprecated(logger, tracker, id, message) {
2658
- if (isDevMode() && !tracker.warnedKeys.has(id)) {
2659
- tracker.warnedKeys.add(id);
2660
- logger.warn(`[Dynamic Forms] DEPRECATED: ${message}`);
2661
- }
2662
- }
2663
-
2664
2669
  /**
2665
2670
  * Token types for expression parsing
2666
2671
  */
@@ -2812,6 +2817,9 @@ class Tokenizer {
2812
2817
  value += this.expression[this.position];
2813
2818
  this.position++;
2814
2819
  }
2820
+ if (!/^\d+(\.\d+)?$/.test(value)) {
2821
+ throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);
2822
+ }
2815
2823
  return { type: TokenType.NUMBER, value, position: start };
2816
2824
  }
2817
2825
  readIdentifierOrKeyword() {
@@ -2934,7 +2942,12 @@ class Parser {
2934
2942
  if (this.isAtEnd()) {
2935
2943
  throw new ExpressionParserError('Empty expression', 0, this.expression);
2936
2944
  }
2937
- return this.parseExpression();
2945
+ const result = this.parseExpression();
2946
+ if (!this.isAtEnd()) {
2947
+ const token = this.peek();
2948
+ throw new ExpressionParserError(`Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`, token.position, this.expression);
2949
+ }
2950
+ return result;
2938
2951
  }
2939
2952
  parseExpression() {
2940
2953
  return this.parseLogicalOr();
@@ -3509,16 +3522,22 @@ function evaluateCondition(expression, context) {
3509
3522
  switch (expression.type) {
3510
3523
  case 'fieldValue':
3511
3524
  return evaluateFieldValueCondition(expression, context);
3512
- case 'formValue':
3513
- // TODO(@ng-forge): remove deprecated code in next minor
3514
- if (context.deprecationTracker) {
3515
- warnDeprecated(context.logger, context.deprecationTracker, 'condition:formValue', "Condition type 'formValue' is deprecated. Use 'fieldValue' with a specific fieldPath, or 'javascript' for complex form-level comparisons.");
3516
- }
3517
- return evaluateFormValueCondition(expression, context);
3518
3525
  case 'javascript':
3519
3526
  return evaluateJavaScriptExpression(expression, context);
3520
- case 'custom':
3521
- return evaluateCustomFunction(expression, context);
3527
+ case 'custom': {
3528
+ const customFn = context.customFunctions?.[expression.functionName];
3529
+ if (!customFn) {
3530
+ context.logger.error('Custom function not found:', expression.functionName);
3531
+ return false;
3532
+ }
3533
+ try {
3534
+ return !!customFn(context);
3535
+ }
3536
+ catch (error) {
3537
+ context.logger.error('Error executing custom function:', expression.functionName, error);
3538
+ return false;
3539
+ }
3540
+ }
3522
3541
  case 'and':
3523
3542
  return evaluateAndCondition(expression, context);
3524
3543
  case 'or':
@@ -3551,12 +3570,9 @@ function evaluateFieldValueCondition(expression, context) {
3551
3570
  : undefined;
3552
3571
  return compareValues(fieldValue, expression.value, expression.operator);
3553
3572
  }
3554
- function evaluateFormValueCondition(expression, context) {
3555
- return compareValues(context.formValue, expression.value, expression.operator);
3556
- }
3557
3573
  function evaluateJavaScriptExpression(expression, context) {
3558
3574
  try {
3559
- // Use secure AST-based expression parser instead of new Function()
3575
+ // Use secure AST-based expression parser instead of dynamic code execution
3560
3576
  const result = ExpressionParser.evaluate(expression.expression, context);
3561
3577
  return !!result;
3562
3578
  }
@@ -3565,25 +3581,6 @@ function evaluateJavaScriptExpression(expression, context) {
3565
3581
  return false;
3566
3582
  }
3567
3583
  }
3568
- function evaluateCustomFunction(expression, context) {
3569
- const fnName = 'functionName' in expression ? expression.functionName : expression.expression;
3570
- // TODO(@ng-forge): remove deprecated code in next minor
3571
- if (!('functionName' in expression) && context.deprecationTracker) {
3572
- warnDeprecated(context.logger, context.deprecationTracker, 'condition:custom:expression', "CustomCondition 'expression' field is deprecated. Use 'functionName' instead.");
3573
- }
3574
- const customFn = context.customFunctions?.[fnName];
3575
- if (!customFn) {
3576
- context.logger.error('Custom function not found:', fnName);
3577
- return false;
3578
- }
3579
- try {
3580
- return !!customFn(context);
3581
- }
3582
- catch (error) {
3583
- context.logger.error('Error executing custom function:', fnName, error);
3584
- return false;
3585
- }
3586
- }
3587
3584
  function evaluateAndCondition(expression, context) {
3588
3585
  if (expression.conditions.length === 0)
3589
3586
  return false;
@@ -4492,21 +4489,6 @@ function isStateLogicConfig(config) {
4492
4489
  function isDerivationLogicConfig(config) {
4493
4490
  return config.type === 'derivation';
4494
4491
  }
4495
- // TODO(@ng-forge): remove deprecated code in next minor
4496
- /**
4497
- * Type guard to check if a logic config is a PropertyDerivationLogicConfig.
4498
- *
4499
- * @deprecated Use `isDerivationLogicConfig(config) && hasTargetProperty(config)` instead.
4500
- * Will be removed in a future minor version.
4501
- *
4502
- * @param config - The logic config to check
4503
- * @returns true if the config is for property derivation
4504
- *
4505
- * @public
4506
- */
4507
- function isPropertyDerivationLogicConfig(config) {
4508
- return config.type === 'propertyDerivation';
4509
- }
4510
4492
  /**
4511
4493
  * Type guard to check if a derivation config targets a property rather than the field value.
4512
4494
  *
@@ -4565,18 +4547,13 @@ function isCrossFieldExpression(expr, context) {
4565
4547
  case 'fieldValue':
4566
4548
  // fieldPath means it references another field's value
4567
4549
  return !!expr.fieldPath;
4568
- case 'formValue':
4569
- // formValue type always accesses the form state
4570
- return true;
4571
4550
  case 'javascript':
4572
4551
  // Check for formValue.* patterns in the expression string
4573
4552
  return FORM_VALUE_ACCESS_PATTERN.test(expr.expression || '');
4574
4553
  case 'custom': {
4575
4554
  // For custom functions, determine scope from registry or default to cross-field.
4576
- // Support both new API (functionName) and deprecated API (expression).
4577
- //
4578
4555
  // Look up function scope from registry if context is provided
4579
- const functionName = 'functionName' in expr ? expr.functionName : expr.expression;
4556
+ const functionName = expr.functionName;
4580
4557
  if (context?.getFunctionScope && functionName) {
4581
4558
  const scope = context.getFunctionScope(functionName);
4582
4559
  if (scope === 'field') {
@@ -4622,10 +4599,6 @@ function extractExpressionDependencies(expr) {
4622
4599
  deps.add(expr.fieldPath.split('.')[0]);
4623
4600
  }
4624
4601
  break;
4625
- case 'formValue':
4626
- // Full form access - depends on everything
4627
- deps.add('*');
4628
- break;
4629
4602
  case 'javascript':
4630
4603
  // Extract from formValue.* patterns in expression
4631
4604
  extractFromExpressionString(expr.expression || '', deps);
@@ -4740,7 +4713,7 @@ function isCrossFieldBuiltInValidator(config) {
4740
4713
  * because they require field-level registration.
4741
4714
  */
4742
4715
  function isResourceBasedValidator(config) {
4743
- return config.type === 'customAsync' || config.type === 'customHttp' || config.type === 'http';
4716
+ return config.type === 'async' || config.type === 'http';
4744
4717
  }
4745
4718
  /**
4746
4719
  * Detects if a validator's `when` condition is cross-field.
@@ -4771,23 +4744,6 @@ function hasCrossFieldWhenCondition(config, context) {
4771
4744
  function isCrossFieldStateLogic(config, context) {
4772
4745
  return isCrossFieldExpression(config.condition, context);
4773
4746
  }
4774
- /**
4775
- * Detects if a LogicConfig has a cross-field condition.
4776
- *
4777
- * @param config The logic configuration to check
4778
- * @param context Optional context providing function scope lookup
4779
- * @returns true if the logic condition references other fields
4780
- *
4781
- * @deprecated Use `isCrossFieldStateLogic` for state logic configs.
4782
- * Derivation logic is handled by the derivation system.
4783
- */
4784
- function isCrossFieldLogic(config, context) {
4785
- // Only check state logic configs
4786
- if (!isStateLogicConfig(config)) {
4787
- return false;
4788
- }
4789
- return isCrossFieldExpression(config.condition, context);
4790
- }
4791
4747
  // ============================================================================
4792
4748
  // Schema Detection
4793
4749
  // ============================================================================
@@ -5504,6 +5460,9 @@ function createDerivationWarningTracker() {
5504
5460
  * Resolves an `HttpRequestConfig` into an `HttpResourceRequest` by evaluating
5505
5461
  * expression-based query params and (optionally) body values.
5506
5462
  *
5463
+ * Returns `null` if any path parameter resolves to `undefined` or `null`,
5464
+ * signaling that the request should be suppressed (the URL would be malformed).
5465
+ *
5507
5466
  * `debounceMs` is intentionally ignored — it's used by HTTP derivations/conditions (PRs 3-4),
5508
5467
  * not by validators.
5509
5468
  */
@@ -5512,7 +5471,12 @@ function resolveHttpRequest(config, context) {
5512
5471
  if (config.params) {
5513
5472
  for (const [key, expression] of Object.entries(config.params)) {
5514
5473
  const value = ExpressionParser.evaluate(expression, context);
5515
- url = url.replace(`:${key}`, encodeURIComponent(value != null ? String(value) : ''));
5474
+ if (value == null) {
5475
+ context.logger.debug(`HTTP request suppressed: path param '${key}' resolved to ${String(value)} ` +
5476
+ `(expression: '${expression}'). The request will not be sent.`);
5477
+ return null;
5478
+ }
5479
+ url = url.replace(`:${key}`, encodeURIComponent(String(value)));
5516
5480
  }
5517
5481
  }
5518
5482
  if (config.queryParams) {
@@ -5553,6 +5517,20 @@ function resolveHttpRequest(config, context) {
5553
5517
  return request;
5554
5518
  }
5555
5519
 
5520
+ /**
5521
+ * Creates a staleness guard for HTTP/async derivation streams.
5522
+ *
5523
+ * - Call `invalidate()` when tearing down the current generation of streams.
5524
+ * - Pipe HTTP requests through `takeUntil(guard$)` to automatically discard
5525
+ * in-flight responses that arrive after the generation has been invalidated.
5526
+ */
5527
+ function createStreamGuard() {
5528
+ const subject = new Subject();
5529
+ return {
5530
+ invalidate: () => subject.next(),
5531
+ guard$: subject.asObservable().pipe(take$1(1)),
5532
+ };
5533
+ }
5556
5534
  const LOG_PREFIX$1 = 'HTTP Derivation -';
5557
5535
  const DEFAULT_HTTP_DEBOUNCE_MS = 300;
5558
5536
  /**
@@ -5597,7 +5575,7 @@ function createHttpDerivationStream(entry, formValue$, context) {
5597
5575
  if (changedFields.size === 0)
5598
5576
  return false;
5599
5577
  return entry.dependsOn.some((dep) => changedFields.has(dep));
5600
- }), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
5578
+ }), debounceTime(debounceMs), switchMap$1(({ current, changedFields }) => {
5601
5579
  return new Observable((subscriber) => {
5602
5580
  const formAccessor = untracked(() => context.form());
5603
5581
  // Check stopOnUserOverride — skip if the user has manually edited the target field
@@ -5668,15 +5646,28 @@ function createHttpDerivationStream(entry, formValue$, context) {
5668
5646
  return;
5669
5647
  }
5670
5648
  }
5671
- // Resolve the HTTP request (evaluates expressions in queryParams/body)
5649
+ // Returns null when a path param is undefined — suppress the request
5672
5650
  const resolvedRequest = resolveHttpRequest(entry.http, evalContext);
5673
- // Make the HTTP request
5651
+ if (!resolvedRequest) {
5652
+ const derivationLogger = untracked(() => context.derivationLogger());
5653
+ derivationLogger.evaluation({
5654
+ debugName: entry.debugName,
5655
+ fieldKey: entry.fieldKey,
5656
+ result: 'skipped',
5657
+ skipReason: 'condition-false',
5658
+ });
5659
+ subscriber.complete();
5660
+ return;
5661
+ }
5662
+ // Make the HTTP request. takeUntil(guard$) automatically discards responses
5663
+ // that arrive after the config has changed and the guard has been invalidated.
5674
5664
  const method = (resolvedRequest.method ?? 'GET').toUpperCase();
5675
5665
  const httpSub = context.httpClient
5676
5666
  .request(method, resolvedRequest.url, {
5677
5667
  body: resolvedRequest.body,
5678
5668
  headers: resolvedRequest.headers,
5679
5669
  })
5670
+ .pipe(takeUntil(context.guard$))
5680
5671
  .subscribe({
5681
5672
  next: (response) => {
5682
5673
  try {
@@ -5721,12 +5712,14 @@ function createHttpDerivationStream(entry, formValue$, context) {
5721
5712
  subscriber.complete();
5722
5713
  },
5723
5714
  });
5724
- // Cleanup subscription on unsubscribe (switchMap cancellation)
5715
+ // Cleanup: RxJS unsubscription cancels the in-flight HTTP request.
5716
+ // Angular's HttpClient uses an internal AbortController when operating with
5717
+ // the fetch backend, so unsubscribing is sufficient for network cancellation.
5725
5718
  return () => {
5726
5719
  httpSub.unsubscribe();
5727
5720
  };
5728
5721
  });
5729
- }), catchError((error) => {
5722
+ }), catchError$1((error) => {
5730
5723
  // Safety net — should not normally be reached since inner errors are caught
5731
5724
  const message = error instanceof Error ? error.message : String(error);
5732
5725
  context.logger.warn(`${LOG_PREFIX$1} Unexpected stream error for '${entry.fieldKey}': ${message}`);
@@ -5776,7 +5769,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
5776
5769
  if (changedFields.size === 0)
5777
5770
  return false;
5778
5771
  return entry.dependsOn.some((dep) => changedFields.has(dep));
5779
- }), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
5772
+ }), debounceTime(debounceMs), switchMap$1(({ current, changedFields }) => {
5780
5773
  return new Observable((subscriber) => {
5781
5774
  const formAccessor = untracked(() => context.form());
5782
5775
  // Check stopOnUserOverride — skip if the user has manually edited the target field
@@ -5897,7 +5890,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
5897
5890
  asyncSub.unsubscribe();
5898
5891
  };
5899
5892
  });
5900
- }), catchError((error) => {
5893
+ }), catchError$1((error) => {
5901
5894
  // Safety net — should not normally be reached since inner errors are caught
5902
5895
  const message = error instanceof Error ? error.message : String(error);
5903
5896
  context.logger.warn(`${LOG_PREFIX} Unexpected stream error for '${entry.fieldKey}': ${message}`);
@@ -5934,6 +5927,12 @@ class DerivationOrchestrator {
5934
5927
  asyncFunctionSubscriptions = [];
5935
5928
  /** Identity keys of current async entries for smart teardown comparison */
5936
5929
  lastAsyncEntryKeys = null;
5930
+ /**
5931
+ * Guard for detecting stale HTTP responses. Call `invalidate()` on teardown to cancel
5932
+ * in-flight requests from the previous generation. Each HTTP stream subscribes via
5933
+ * `takeUntil(guard$)` — when `invalidate()` fires, all in-flight requests are discarded.
5934
+ */
5935
+ streamGuard = createStreamGuard();
5937
5936
  /**
5938
5937
  * Computed signal containing the collected and validated derivations.
5939
5938
  * Returns null if no derivations are defined.
@@ -5954,7 +5953,7 @@ class DerivationOrchestrator {
5954
5953
  this.warnAboutWildcardDependencies(collection.entries, fields.length);
5955
5954
  this.warnAboutMisconfiguredReEngagement(collection.entries);
5956
5955
  return collection;
5957
- }, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : []));
5956
+ }, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : /* istanbul ignore next */ []));
5958
5957
  this.setupOnChangeStream();
5959
5958
  this.setupDebouncedStream();
5960
5959
  this.setupHttpStreams();
@@ -6007,7 +6006,7 @@ class DerivationOrchestrator {
6007
6006
  // switchMap: For debounced derivations, it's OK to cancel pending work
6008
6007
  // if new changes come in - we want the latest debounced values.
6009
6008
  // (Unlike onChange which uses exhaustMap to prevent cancellation)
6010
- switchMap(({ changedFields }) => {
6009
+ switchMap$1(({ changedFields }) => {
6011
6010
  const collection = untracked(() => this.derivationCollection());
6012
6011
  const formAccessor = untracked(() => this.config.form());
6013
6012
  if (!collection || !formAccessor)
@@ -6126,6 +6125,7 @@ class DerivationOrchestrator {
6126
6125
  customFunctions: () => this.functionRegistry.getCustomFunctions(),
6127
6126
  externalData: () => this.resolveExternalData(),
6128
6127
  warningTracker: this.warningTracker,
6128
+ guard$: this.streamGuard.guard$,
6129
6129
  };
6130
6130
  const stream = createHttpDerivationStream(entry, formValue$, context).pipe(takeUntilDestroyed(this.destroyRef));
6131
6131
  this.httpSubscriptions.push(stream.subscribe({
@@ -6135,9 +6135,11 @@ class DerivationOrchestrator {
6135
6135
  });
6136
6136
  }
6137
6137
  /**
6138
- * Tears down all active HTTP derivation streams.
6138
+ * Tears down all active HTTP derivation streams. Invalidating the stream guard
6139
+ * cancels any in-flight HTTP responses from the old generation via `takeUntil`.
6139
6140
  */
6140
6141
  teardownHttpStreams() {
6142
+ this.streamGuard.invalidate();
6141
6143
  for (const sub of this.httpSubscriptions) {
6142
6144
  sub.unsubscribe();
6143
6145
  }
@@ -6526,9 +6528,9 @@ function computePropertyValue(entry, evalContext, applicatorContext) {
6526
6528
  }
6527
6529
  // Custom function
6528
6530
  if (entry.functionName) {
6529
- const fn = applicatorContext.propertyDerivationFunctions?.[entry.functionName];
6531
+ const fn = applicatorContext.derivationFunctions?.[entry.functionName];
6530
6532
  if (!fn) {
6531
- throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.propertyDerivations`);
6533
+ throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
6532
6534
  }
6533
6535
  return fn(evalContext);
6534
6536
  }
@@ -6575,7 +6577,7 @@ function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
6575
6577
  /**
6576
6578
  * Collects all property derivation entries from field definitions.
6577
6579
  *
6578
- * Traverses the field definition tree and extracts `type: 'propertyDerivation'`
6580
+ * Traverses the field definition tree and extracts `type: 'derivation'` with `targetProperty`
6579
6581
  * entries from each field's `logic` array.
6580
6582
  *
6581
6583
  * No topological sort is needed because property derivations don't chain —
@@ -6635,46 +6637,13 @@ function collectFromField$1(field, entries, context) {
6635
6637
  const validationField = field;
6636
6638
  if (validationField.logic) {
6637
6639
  for (const logicConfig of validationField.logic) {
6638
- if (isPropertyDerivationLogicConfig(logicConfig)) {
6639
- // TODO(@ng-forge): remove deprecated code in next minor
6640
- // Deprecated path — emit warning, then process normally
6641
- warnDeprecated(context.logger, context.tracker, 'type:propertyDerivation', "Logic type 'propertyDerivation' is deprecated. Use type: 'derivation' with targetProperty instead.");
6642
- const entry = createPropertyDerivationEntry(fieldKey, logicConfig, context);
6643
- entries.push(entry);
6644
- }
6645
- else if (isDerivationLogicConfig(logicConfig) && hasTargetProperty(logicConfig)) {
6646
- // New unified path: type: 'derivation' + targetProperty → property derivation pipeline
6640
+ if (isDerivationLogicConfig(logicConfig) && hasTargetProperty(logicConfig)) {
6647
6641
  const entry = createPropertyDerivationEntryFromDerivation(fieldKey, logicConfig, context);
6648
6642
  entries.push(entry);
6649
6643
  }
6650
6644
  }
6651
6645
  }
6652
6646
  }
6653
- /**
6654
- * Creates a property derivation entry from a `PropertyDerivationLogicConfig`.
6655
- *
6656
- * @internal
6657
- */
6658
- function createPropertyDerivationEntry(fieldKey, config, context) {
6659
- const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
6660
- const dependsOn = extractDependencies(config);
6661
- const condition = config.condition ?? true;
6662
- const trigger = config.trigger ?? 'onChange';
6663
- const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
6664
- return {
6665
- fieldKey: effectiveFieldKey,
6666
- targetProperty: config.targetProperty,
6667
- dependsOn,
6668
- condition,
6669
- value: config.value,
6670
- expression: config.expression,
6671
- functionName: config.functionName,
6672
- trigger,
6673
- debounceMs,
6674
- debugName: config.debugName,
6675
- originalConfig: config,
6676
- };
6677
- }
6678
6647
  /**
6679
6648
  * Creates a property derivation entry from a `DerivationLogicConfig` that has `targetProperty`.
6680
6649
  *
@@ -6767,7 +6736,7 @@ class PropertyDerivationOrchestrator {
6767
6736
  return null;
6768
6737
  }
6769
6738
  return collection;
6770
- }, ...(ngDevMode ? [{ debugName: "propertyDerivationCollection" }] : []));
6739
+ }, ...(ngDevMode ? [{ debugName: "propertyDerivationCollection" }] : /* istanbul ignore next */ []));
6771
6740
  // Side effects react to collection changes: clear the store, register fields, warn.
6772
6741
  // schemaFields is included in the dependency array to ensure the field count is always
6773
6742
  // in sync with the collection (avoids reading a stale value via untracked).
@@ -6800,7 +6769,7 @@ class PropertyDerivationOrchestrator {
6800
6769
  .pipe(debounceTime(DEFAULT_DEBOUNCE_MS), startWith(null), pairwise(), filter((pair) => pair[1] !== null), map$1(([previous, current]) => ({
6801
6770
  current,
6802
6771
  changedFields: getChangedKeys(previous, current),
6803
- })), filter(({ changedFields }) => changedFields.size > 0), switchMap(({ changedFields }) => {
6772
+ })), filter(({ changedFields }) => changedFields.size > 0), switchMap$1(({ changedFields }) => {
6804
6773
  const collection = untracked(() => this.propertyDerivationCollection());
6805
6774
  if (!collection)
6806
6775
  return of(null);
@@ -6827,7 +6796,7 @@ class PropertyDerivationOrchestrator {
6827
6796
  const applicatorContext = {
6828
6797
  formValue: this.config.formValue,
6829
6798
  store: this.config.store,
6830
- propertyDerivationFunctions: this.functionRegistry.getPropertyDerivationFunctions(),
6799
+ derivationFunctions: this.functionRegistry.getDerivationFunctions(),
6831
6800
  customFunctions: this.functionRegistry.getCustomFunctions(),
6832
6801
  externalData: this.resolveExternalData(),
6833
6802
  logger: this.logger,
@@ -6843,7 +6812,7 @@ class PropertyDerivationOrchestrator {
6843
6812
  const applicatorContext = {
6844
6813
  formValue: this.config.formValue,
6845
6814
  store: this.config.store,
6846
- propertyDerivationFunctions: this.functionRegistry.getPropertyDerivationFunctions(),
6815
+ derivationFunctions: this.functionRegistry.getDerivationFunctions(),
6847
6816
  customFunctions: this.functionRegistry.getCustomFunctions(),
6848
6817
  externalData: this.resolveExternalData(),
6849
6818
  logger: this.logger,
@@ -7233,20 +7202,21 @@ class PageOrchestratorComponent {
7233
7202
  eventBus = inject(EventBus);
7234
7203
  fieldContextRegistry = inject(FieldContextRegistryService);
7235
7204
  functionRegistry = inject(FunctionRegistryService);
7205
+ formOptions = inject(FORM_OPTIONS, { optional: true });
7236
7206
  /**
7237
7207
  * Array of page field definitions to render
7238
7208
  */
7239
- pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : []));
7209
+ pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : /* istanbul ignore next */ []));
7240
7210
  /**
7241
7211
  * Root form instance from parent DynamicForm.
7242
7212
  * Uses FieldTree<unknown> to accept any form type.
7243
7213
  */
7244
- form = input.required(...(ngDevMode ? [{ debugName: "form" }] : []));
7214
+ form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
7245
7215
  /**
7246
7216
  * Field signal context for child fields
7247
7217
  */
7248
7218
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FieldSignalContext is contravariant in TModel, using any allows accepting any form type
7249
- fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
7219
+ fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : /* istanbul ignore next */ []));
7250
7220
  /**
7251
7221
  * Computed signal that tracks which pages are hidden.
7252
7222
  * Returns an array of booleans where true means the page is hidden.
@@ -7255,7 +7225,7 @@ class PageOrchestratorComponent {
7255
7225
  pageHiddenStates = computed(() => {
7256
7226
  const pages = this.pageFields();
7257
7227
  return pages.map((page) => this.evaluatePageHidden(page));
7258
- }, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : []));
7228
+ }, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : /* istanbul ignore next */ []));
7259
7229
  /**
7260
7230
  * Computed signal that returns indices of visible (non-hidden) pages.
7261
7231
  * This is used for navigation to skip hidden pages.
@@ -7266,7 +7236,7 @@ class PageOrchestratorComponent {
7266
7236
  .map((hidden, index) => ({ index, hidden }))
7267
7237
  .filter((item) => !item.hidden)
7268
7238
  .map((item) => item.index);
7269
- }, ...(ngDevMode ? [{ debugName: "visiblePageIndices" }] : []));
7239
+ }, ...(ngDevMode ? [{ debugName: "visiblePageIndices" }] : /* istanbul ignore next */ []));
7270
7240
  /**
7271
7241
  * Internal signal for current page index that tracks with page fields.
7272
7242
  * This tracks the actual page index, not the visible page index.
@@ -7281,7 +7251,7 @@ class PageOrchestratorComponent {
7281
7251
  return 0;
7282
7252
  // Start on page 0 (will be adjusted if page 0 is hidden by initial navigation)
7283
7253
  return 0;
7284
- }, ...(ngDevMode ? [{ debugName: "currentPageIndex" }] : []));
7254
+ }, ...(ngDevMode ? [{ debugName: "currentPageIndex" }] : /* istanbul ignore next */ []));
7285
7255
  /**
7286
7256
  * Computed state for the orchestrator
7287
7257
  */
@@ -7300,7 +7270,7 @@ class PageOrchestratorComponent {
7300
7270
  isLastPage: isLastVisiblePage,
7301
7271
  navigationDisabled: false,
7302
7272
  };
7303
- }, ...(ngDevMode ? [{ debugName: "state" }] : []));
7273
+ }, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
7304
7274
  /**
7305
7275
  * Signal indicating whether all fields on the current page are valid.
7306
7276
  *
@@ -7320,13 +7290,11 @@ class PageOrchestratorComponent {
7320
7290
  }
7321
7291
  const currentPage = pages[currentIndex];
7322
7292
  const pageFields = currentPage.fields || [];
7323
- // Check validity of each field on the current page
7293
+ // Collect all leaf field keys, recursively traversing group/row containers
7294
+ const leafKeys = collectLeafFieldKeys(pageFields);
7295
+ // Check validity of each leaf field on the current page
7324
7296
  // Fields are stored at root level in the form (pages don't add nesting)
7325
- for (const fieldDef of pageFields) {
7326
- const fieldKey = fieldDef.key;
7327
- if (!fieldKey)
7328
- continue;
7329
- // Access the field from the form using bracket notation
7297
+ for (const fieldKey of leafKeys) {
7330
7298
  const field = form[fieldKey];
7331
7299
  if (field && typeof field === 'function') {
7332
7300
  const fieldState = field();
@@ -7336,7 +7304,7 @@ class PageOrchestratorComponent {
7336
7304
  }
7337
7305
  }
7338
7306
  return true;
7339
- }, ...(ngDevMode ? [{ debugName: "currentPageValid" }] : []));
7307
+ }, ...(ngDevMode ? [{ debugName: "currentPageValid" }] : /* istanbul ignore next */ []));
7340
7308
  /**
7341
7309
  * Extended field signal context that includes currentPageValid.
7342
7310
  *
@@ -7347,16 +7315,37 @@ class PageOrchestratorComponent {
7347
7315
  extendedFieldSignalContext = computed(() => ({
7348
7316
  ...this.fieldSignalContext(),
7349
7317
  currentPageValid: this.currentPageValid,
7350
- }), ...(ngDevMode ? [{ debugName: "extendedFieldSignalContext" }] : []));
7318
+ }), ...(ngDevMode ? [{ debugName: "extendedFieldSignalContext" }] : /* istanbul ignore next */ []));
7351
7319
  constructor() {
7352
7320
  // Setup event listeners for navigation
7353
7321
  this.setupEventListeners();
7322
+ // B15: Auto-navigate away when current page becomes hidden
7323
+ explicitEffect([this.state, this.visiblePageIndices], ([state, visibleIndices]) => {
7324
+ const currentVisiblePosition = visibleIndices.indexOf(state.currentPageIndex);
7325
+ if (currentVisiblePosition === -1 && visibleIndices.length > 0) {
7326
+ // Current page is hidden — navigate to the nearest visible page
7327
+ const nearest = this.findNearestVisiblePage(state.currentPageIndex, visibleIndices);
7328
+ if (nearest !== -1) {
7329
+ this.navigateToPage(nearest);
7330
+ }
7331
+ }
7332
+ });
7354
7333
  }
7355
7334
  /**
7356
7335
  * Navigate to the next visible page, skipping hidden pages.
7357
7336
  * @returns Navigation result
7358
7337
  */
7359
7338
  navigateToNextPage() {
7339
+ // Guard: do not advance if current page has invalid fields.
7340
+ // Respects disableWhenPageInvalid option (defaults to true).
7341
+ const disableWhenPageInvalid = this.formOptions?.()?.nextButton?.disableWhenPageInvalid ?? true;
7342
+ if (disableWhenPageInvalid && !this.currentPageValid()) {
7343
+ return {
7344
+ success: false,
7345
+ newPageIndex: this.state().currentPageIndex,
7346
+ error: 'Current page has invalid fields',
7347
+ };
7348
+ }
7360
7349
  const currentState = this.state();
7361
7350
  const visibleIndices = this.visiblePageIndices();
7362
7351
  if (currentState.isLastPage) {
@@ -7426,7 +7415,7 @@ class PageOrchestratorComponent {
7426
7415
  navigateToPage(pageIndex) {
7427
7416
  const currentState = this.state();
7428
7417
  const totalPages = currentState.totalPages;
7429
- // Validate page index
7418
+ // Validate page index bounds
7430
7419
  if (pageIndex < 0 || pageIndex >= totalPages) {
7431
7420
  return {
7432
7421
  success: false,
@@ -7434,6 +7423,15 @@ class PageOrchestratorComponent {
7434
7423
  error: `Invalid page index: ${pageIndex}. Valid range is 0 to ${totalPages - 1}`,
7435
7424
  };
7436
7425
  }
7426
+ // Validate target page is visible
7427
+ const visibleIndices = this.visiblePageIndices();
7428
+ if (!visibleIndices.includes(pageIndex)) {
7429
+ return {
7430
+ success: false,
7431
+ newPageIndex: currentState.currentPageIndex,
7432
+ error: `Cannot navigate to hidden page at index ${pageIndex}. Visible pages: [${visibleIndices.join(', ')}]`,
7433
+ };
7434
+ }
7437
7435
  // Check if already on target page
7438
7436
  if (pageIndex === currentState.currentPageIndex) {
7439
7437
  return {
@@ -7451,6 +7449,23 @@ class PageOrchestratorComponent {
7451
7449
  newPageIndex: pageIndex,
7452
7450
  };
7453
7451
  }
7452
+ /**
7453
+ * Finds the nearest visible page index to the given index.
7454
+ * Prefers the forward (higher index) page when equidistant.
7455
+ */
7456
+ findNearestVisiblePage(currentIndex, visibleIndices) {
7457
+ let nearest = -1;
7458
+ let minDistance = Infinity;
7459
+ for (const idx of visibleIndices) {
7460
+ const distance = Math.abs(idx - currentIndex);
7461
+ // Prefer forward (higher index) when tied
7462
+ if (distance < minDistance || (distance === minDistance && idx > nearest)) {
7463
+ minDistance = distance;
7464
+ nearest = idx;
7465
+ }
7466
+ }
7467
+ return nearest;
7468
+ }
7454
7469
  /**
7455
7470
  * Set up event listeners for navigation events
7456
7471
  */
@@ -7507,8 +7522,8 @@ class PageOrchestratorComponent {
7507
7522
  }
7508
7523
  return false;
7509
7524
  }
7510
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7511
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
7525
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7526
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
7512
7527
  {
7513
7528
  provide: FIELD_SIGNAL_CONTEXT,
7514
7529
  useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
@@ -7538,9 +7553,9 @@ class PageOrchestratorComponent {
7538
7553
  }
7539
7554
  }
7540
7555
  }
7541
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs').then(m => m.default)]] });
7556
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-Cen4ArED.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-Cen4ArED.mjs').then(m => m.default)]] });
7542
7557
  }
7543
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.4", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
7558
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.6", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-Cen4ArED.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
7544
7559
  type: Component,
7545
7560
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
7546
7561
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -7578,6 +7593,33 @@ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.4", ng
7578
7593
  },
7579
7594
  ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;width:100%}\n"] }]
7580
7595
  }], ctorParameters: () => [], propDecorators: { pageFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageFields", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], fieldSignalContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldSignalContext", required: true }] }] } }) });
7596
+ /**
7597
+ * Recursively collects form-tree keys for all value-bearing nodes on a page.
7598
+ *
7599
+ * Row fields are layout-only containers whose children are flattened to the
7600
+ * parent level in the form tree, so we recurse through them transparently.
7601
+ *
7602
+ * Group fields create a nested sub-tree in the form (form['address']['street']).
7603
+ * Their group-level FieldTree node aggregates child validity, so we use the
7604
+ * group's own key — not the individual child keys — to check validity.
7605
+ *
7606
+ * Array fields are also accessed by their own key; the ArrayFieldTree node
7607
+ * covers minLength/maxLength and any item-level validators.
7608
+ */
7609
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any field shape for recursive traversal
7610
+ function collectLeafFieldKeys(fields) {
7611
+ const keys = [];
7612
+ for (const field of fields) {
7613
+ if (isRowField(field)) {
7614
+ // Row children are at the same form-tree level as the row itself
7615
+ keys.push(...collectLeafFieldKeys(field.fields));
7616
+ }
7617
+ else if (field.key) {
7618
+ keys.push(field.key);
7619
+ }
7620
+ }
7621
+ return keys;
7622
+ }
7581
7623
 
7582
7624
  /**
7583
7625
  * Symbol key for storing normalization metadata on array field objects.
@@ -7925,8 +7967,8 @@ function isFunctionHttpValidator(config) {
7925
7967
  /**
7926
7968
  * Evaluates an HTTP response against a `HttpValidationResponseMapping` to produce a validation result.
7927
7969
  *
7928
- * - `validWhen` truthy → `null` (valid, no error)
7929
- * - `validWhen` falsy → `{ kind: errorKind, ...evaluatedErrorParams }`
7970
+ * - `validWhen` === `true` → `null` (valid, no error)
7971
+ * - `validWhen` !== `true` → `{ kind: errorKind, ...evaluatedErrorParams }`
7930
7972
  * - Expression error → logs warning, returns `{ kind: errorKind }` (fail-closed)
7931
7973
  *
7932
7974
  * Expressions are evaluated with scope `{ response }` only.
@@ -7934,8 +7976,11 @@ function isFunctionHttpValidator(config) {
7934
7976
  function evaluateHttpValidationResponse(response, mapping, logger) {
7935
7977
  const scope = { response };
7936
7978
  try {
7937
- const isValid = ExpressionParser.evaluate(mapping.validWhen, scope);
7938
- if (isValid) {
7979
+ const result = ExpressionParser.evaluate(mapping.validWhen, scope);
7980
+ if (typeof result !== 'boolean') {
7981
+ logger.warn(`validWhen expression "${mapping.validWhen}" returned non-boolean value:`, result, 'Expected true or false.');
7982
+ }
7983
+ if (result === true) {
7939
7984
  return null;
7940
7985
  }
7941
7986
  const error = { kind: mapping.errorKind };
@@ -8037,35 +8082,119 @@ class HttpConditionFunctionCacheService {
8037
8082
  }
8038
8083
 
8039
8084
  /**
8040
- * Extracts a boolean from an HTTP response using an optional expression.
8041
- * When `responseExpression` is provided, evaluates it with `{ response }` scope.
8042
- * Otherwise, coerces the response to boolean.
8043
- */
8044
- function extractBoolean(response, responseExpression, pendingValue, logger) {
8045
- if (responseExpression) {
8046
- try {
8047
- const result = ExpressionParser.evaluate(responseExpression, { response });
8048
- return !!result;
8049
- }
8050
- catch (error) {
8051
- logger.warn(`Failed to evaluate responseExpression '${responseExpression}':`, error);
8052
- return pendingValue;
8053
- }
8054
- }
8055
- return !!response;
8056
- }
8057
- /**
8058
- * Creates a logic function for an HTTP condition.
8085
+ * Wraps a `Resource` so that the previous resolved value is preserved while the resource
8086
+ * is loading or reloading. This prevents UI flicker when params change and a new request
8087
+ * is in-flight — the consumer sees the stale value with a `loading`/`reloading` status
8088
+ * instead of `undefined`.
8059
8089
  *
8060
- * Uses a signal-based async resolution pattern: the LogicFn updates a `resolvedRequest` signal,
8061
- * a reactive pipeline fetches the response and updates a `resultValue` signal.
8062
- * The LogicFn returns the current `resultValue()`, which starts as `pendingValue` and
8063
- * transitions to the actual HTTP result.
8090
+ * Uses Angular's `resource.snapshot` + `resourceFromSnapshots` composition pattern.
8064
8091
  *
8065
- * Must be called in injection context (same as `createLogicFunction`).
8066
- */
8067
- function createHttpConditionLogicFunction(condition) {
8068
- const httpClient = inject(HttpClient);
8092
+ * @example
8093
+ * ```typescript
8094
+ * const user = withPreviousValue(
8095
+ * resource({ params: () => userId(), loader: ({ params }) => fetchUser(params) })
8096
+ * );
8097
+ * // user.value() keeps the old user data during loading transitions
8098
+ * ```
8099
+ *
8100
+ * @experimental Uses Angular's experimental resource composition APIs.
8101
+ */
8102
+ function withPreviousValue(input) {
8103
+ const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
8104
+ computation: (snapshot, previous) => {
8105
+ // During loading/reloading, keep the previous resolved value if available.
8106
+ // Don't preserve values from error states — those aren't useful to show.
8107
+ if ((snapshot.status === 'loading' || snapshot.status === 'reloading') &&
8108
+ previous !== undefined &&
8109
+ previous.value.status !== 'error') {
8110
+ return { status: snapshot.status, value: previous.value.value };
8111
+ }
8112
+ return snapshot;
8113
+ } });
8114
+ return resourceFromSnapshots(derived);
8115
+ }
8116
+
8117
+ /**
8118
+ * Type guard to check if a value is an array of Signals.
8119
+ * @internal
8120
+ */
8121
+ function isSignalArray(value) {
8122
+ return Array.isArray(value) && value.every((item) => isSignal(item));
8123
+ }
8124
+ /**
8125
+ * Type guard to check if a value is a record (object) of Signals.
8126
+ * @internal
8127
+ */
8128
+ function isSignalRecord(value) {
8129
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
8130
+ }
8131
+ // Implementation
8132
+ function derivedFromDeferred(source, operator, options) {
8133
+ const deferred$ = defer(() => {
8134
+ // Handle single signal
8135
+ if (isSignal(source)) {
8136
+ return toObservable(source, { injector: options.injector });
8137
+ }
8138
+ // Handle array of signals
8139
+ if (isSignalArray(source)) {
8140
+ const observables = source.map((s) => toObservable(s, { injector: options.injector }));
8141
+ return combineLatest(observables);
8142
+ }
8143
+ // Handle object of signals
8144
+ if (isSignalRecord(source)) {
8145
+ const keys = Object.keys(source);
8146
+ const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
8147
+ return combineLatest(observables, (...values) => {
8148
+ const result = {};
8149
+ keys.forEach((key, index) => {
8150
+ result[key] = values[index];
8151
+ });
8152
+ return result;
8153
+ });
8154
+ }
8155
+ // Fallback - should not happen with proper typing
8156
+ throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
8157
+ }).pipe(operator);
8158
+ return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
8159
+ }
8160
+
8161
+ /**
8162
+ * Extracts a boolean from an HTTP response using an optional expression.
8163
+ * When `responseExpression` is provided, evaluates it with `{ response }` scope.
8164
+ * Truthy values are treated as `true` — non-boolean results trigger a warning to encourage explicit boolean expressions.
8165
+ * Otherwise, coerces the response to boolean.
8166
+ */
8167
+ function extractBoolean(response, responseExpression, pendingValue, logger) {
8168
+ if (responseExpression) {
8169
+ try {
8170
+ const result = ExpressionParser.evaluate(responseExpression, { response });
8171
+ if (typeof result !== 'boolean') {
8172
+ logger.warn(`responseExpression "${responseExpression}" returned non-boolean value:`, result, 'Consider returning a boolean explicitly.');
8173
+ }
8174
+ return !!result;
8175
+ }
8176
+ catch (error) {
8177
+ logger.warn(`Failed to evaluate responseExpression '${responseExpression}':`, error);
8178
+ return pendingValue;
8179
+ }
8180
+ }
8181
+ return !!response;
8182
+ }
8183
+ /**
8184
+ * Creates a logic function for an HTTP condition.
8185
+ *
8186
+ * Uses Angular's `rxResource()` API with snapshot composition for async resolution:
8187
+ * the LogicFn updates a `resolvedRequest` signal, a debounced version feeds into
8188
+ * an `rxResource()` that manages the HTTP lifecycle, and `withPreviousValue()` preserves
8189
+ * the last resolved boolean during re-fetching to prevent UI flicker.
8190
+ *
8191
+ * Must be called in injection context (same as `createLogicFunction`).
8192
+ */
8193
+ function createHttpConditionLogicFunction(condition) {
8194
+ const httpClient = inject(HttpClient, { optional: true });
8195
+ if (!httpClient) {
8196
+ throw new DynamicFormError('HttpClient is required for HTTP conditions. Add provideHttpClient() to your providers.');
8197
+ }
8069
8198
  const fieldContextRegistry = inject(FieldContextRegistryService);
8070
8199
  const functionRegistry = inject(FunctionRegistryService);
8071
8200
  const injector = inject(Injector);
@@ -8088,51 +8217,69 @@ function createHttpConditionLogicFunction(condition) {
8088
8217
  const contextKey = safeReadPathKeys(ctx).join('.');
8089
8218
  let signalPair = perFunctionSignalStore.get(contextKey);
8090
8219
  if (!signalPair) {
8091
- const resolvedRequest = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedRequest" }] : []));
8092
- // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),
8220
+ const resolvedRequest = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedRequest" }] : /* istanbul ignore next */ []));
8221
+ // Wrap in untracked() to avoid NG0602: resource() internally creates effects,
8093
8222
  // which cannot be created inside a reactive context (computed). The LogicFn runs
8094
8223
  // inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
8095
- // clear the active reactive consumer before creating the observable pipeline.
8096
- const resultValue = untracked(() => {
8097
- const pipeline = toObservable(resolvedRequest, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged((prev, curr) => stableStringify(prev) === stableStringify(curr)), filter((request) => request !== undefined), switchMap((request) => {
8098
- const method = request.method ?? 'GET';
8099
- const options = {};
8100
- if (request.body)
8101
- options['body'] = request.body;
8102
- if (request.headers)
8103
- options['headers'] = request.headers;
8104
- return httpClient.request(method, request.url, options).pipe(map$1((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)), tap((value) => {
8105
- const requestKey = stableStringify(request);
8106
- cache.set(requestKey, value, cacheDurationMs);
8107
- }), catchError$1((error) => {
8108
- logger.warn('HTTP condition request failed:', error);
8109
- return of(pendingValue);
8110
- }));
8111
- }), startWith(pendingValue));
8112
- return toSignal(pipeline, { injector, initialValue: pendingValue });
8224
+ // clear the active reactive consumer before creating the resource pipeline.
8225
+ const resultResource = untracked(() => {
8226
+ // Create a debounced, stabilized version of the request signal.
8227
+ // This prevents rapid re-fetches when form values change quickly.
8228
+ const debouncedRequest = derivedFromDeferred(resolvedRequest, pipe(debounceTime(debounceMs), distinctUntilChanged((prev, curr) => stableStringify(prev) === stableStringify(curr))), { initialValue: undefined, injector });
8229
+ // rxResource() manages the HTTP lifecycle natively with Observables:
8230
+ // auto-cancellation via unsubscription, loading/resolved/error status tracking,
8231
+ // and signal-based reactivity.
8232
+ const httpResource = rxResource({
8233
+ params: () => debouncedRequest() ?? undefined,
8234
+ // When params() returns undefined, rxResource() enters idle state and skips the stream.
8235
+ stream: ({ params: request }) => {
8236
+ const method = request.method ?? 'GET';
8237
+ const options = {};
8238
+ if (request.body)
8239
+ options['body'] = request.body;
8240
+ if (request.headers)
8241
+ options['headers'] = request.headers;
8242
+ return httpClient.request(method, request.url, options).pipe(map$1((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)), tap((value) => {
8243
+ const requestKey = stableStringify(request);
8244
+ cache.set(requestKey, value, cacheDurationMs);
8245
+ }), catchError((error) => {
8246
+ logger.warn('HTTP condition request failed:', error);
8247
+ return of(pendingValue);
8248
+ }));
8249
+ },
8250
+ defaultValue: pendingValue,
8251
+ injector,
8252
+ });
8253
+ // Snapshot composition: preserve the previous boolean result during re-fetching.
8254
+ // Without this, the condition would briefly flash to pendingValue when params change,
8255
+ // causing visible UI flicker (e.g., a conditionally visible field briefly disappearing).
8256
+ return withPreviousValue(httpResource);
8113
8257
  });
8114
- signalPair = { resolvedRequest, resultValue };
8258
+ signalPair = { resolvedRequest, resultResource };
8115
8259
  perFunctionSignalStore.set(contextKey, signalPair);
8116
8260
  }
8117
8261
  // Build reactive evaluation context (creates signal dependencies on form values)
8118
8262
  const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
8119
- // Resolve the HTTP request config into a concrete request
8263
+ // Returns null when a path param is undefined — skip the request and return pending value
8120
8264
  const resolved = resolveHttpRequest(condition.http, evaluationContext);
8265
+ if (!resolved) {
8266
+ return signalPair.resultResource.value();
8267
+ }
8121
8268
  const requestKey = stableStringify(resolved);
8122
8269
  // Check response cache first
8123
8270
  const cachedResult = cache.get(requestKey);
8124
8271
  if (cachedResult !== undefined) {
8125
8272
  return cachedResult;
8126
8273
  }
8127
- // Update the resolved request signal (triggers pipeline if request changed)
8128
- const { resolvedRequest, resultValue } = signalPair;
8274
+ // Update the resolved request signal (triggers resource if request changed)
8275
+ const { resolvedRequest, resultResource } = signalPair;
8129
8276
  untracked(() => {
8130
8277
  const current = resolvedRequest();
8131
8278
  if (stableStringify(current) !== requestKey) {
8132
8279
  resolvedRequest.set(resolved);
8133
8280
  }
8134
8281
  });
8135
- return resultValue();
8282
+ return resultResource.value();
8136
8283
  };
8137
8284
  cacheService.httpConditionFunctionCache.set(cacheKey, fn);
8138
8285
  return fn;
@@ -8156,10 +8303,11 @@ class AsyncConditionFunctionCacheService {
8156
8303
  /**
8157
8304
  * Creates a logic function for an async condition.
8158
8305
  *
8159
- * Uses a signal-based async resolution pattern: the LogicFn updates a `trigger` signal,
8160
- * a reactive pipeline calls the async function and updates a `resultValue` signal.
8161
- * The LogicFn returns the current `resultValue()`, which starts as `pendingValue` and
8162
- * transitions to the actual async result.
8306
+ * Uses Angular's `rxResource()` API with snapshot composition for async resolution:
8307
+ * the LogicFn updates a `trigger` signal (serialized evaluation context), a debounced
8308
+ * version feeds into an `rxResource()` that manages the async function lifecycle, and
8309
+ * `withPreviousValue()` preserves the last resolved boolean during re-evaluation
8310
+ * to prevent UI flicker.
8163
8311
  *
8164
8312
  * Must be called in injection context (same as `createLogicFunction`).
8165
8313
  */
@@ -8183,33 +8331,53 @@ function createAsyncConditionLogicFunction(condition) {
8183
8331
  const contextKey = safeReadPathKeys(ctx).join('.');
8184
8332
  let signalPair = perFunctionSignalStore.get(contextKey);
8185
8333
  if (!signalPair) {
8186
- const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
8187
- // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),
8334
+ const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
8335
+ const ctxByTrigger = new Map();
8336
+ // Wrap in untracked() to avoid NG0602: resource() internally creates effects,
8188
8337
  // which cannot be created inside a reactive context (computed). The LogicFn runs
8189
8338
  // inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
8190
- // clear the active reactive consumer before creating the observable pipeline.
8191
- const resultValue = untracked(() => {
8192
- const pipeline = toObservable(trigger, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged(), filter((value) => value !== undefined), switchMap(() => {
8193
- // Look up the async function at call time (fresh reference)
8194
- const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
8195
- if (!asyncFn) {
8196
- logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
8197
- `Register it in customFnConfig.asyncConditions.`);
8198
- return of(pendingValue);
8199
- }
8200
- // Build evaluation context from current registry state
8201
- const evaluationContext = untracked(() => fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions()));
8202
- // concat ensures pendingValue is emitted immediately when a new trigger
8203
- // arrives, before the async call resolves. This prevents the signal from
8204
- // showing a stale resolved value during re-evaluation.
8205
- return concat(of(pendingValue), from(asyncFn(evaluationContext)).pipe(map$1((result) => !!result), catchError$1((error) => {
8206
- logger.warn('Async Condition - function failed:', error);
8207
- return of(pendingValue);
8208
- })));
8209
- }));
8210
- return toSignal(pipeline, { injector, initialValue: pendingValue });
8339
+ // clear the active reactive consumer before creating the resource pipeline.
8340
+ const resultResource = untracked(() => {
8341
+ // Debounce the trigger to batch rapid form value changes.
8342
+ const debouncedTrigger = derivedFromDeferred(trigger, pipe(debounceTime(debounceMs), distinctUntilChanged()), {
8343
+ initialValue: undefined,
8344
+ injector,
8345
+ });
8346
+ // rxResource() manages the async function lifecycle natively with Observables:
8347
+ // auto-cancellation via unsubscription and signal-based reactivity.
8348
+ const asyncResource = rxResource({
8349
+ params: () => debouncedTrigger() ?? undefined,
8350
+ stream: ({ params: triggerKey }) => {
8351
+ // Look up the async function at call time (fresh reference)
8352
+ const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
8353
+ if (!asyncFn) {
8354
+ logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
8355
+ `Register it in customFnConfig.asyncConditions.`);
8356
+ return of(pendingValue);
8357
+ }
8358
+ // Retrieve the FieldContext that was captured when this trigger key was set.
8359
+ // This avoids a race condition where a mutable ref could be overwritten by
8360
+ // a later LogicFn call before the loader finishes executing.
8361
+ const capturedCtx = ctxByTrigger.get(triggerKey);
8362
+ if (!capturedCtx)
8363
+ return of(pendingValue);
8364
+ const evaluationContext = untracked(() => fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions()));
8365
+ // asyncFn may return Promise<boolean> or Observable<boolean>.
8366
+ // from() normalizes both to Observable.
8367
+ return from(asyncFn(evaluationContext)).pipe(map$1((result) => !!result), catchError((error) => {
8368
+ logger.warn('Async Condition - function failed:', error);
8369
+ return of(pendingValue);
8370
+ }));
8371
+ },
8372
+ defaultValue: pendingValue,
8373
+ injector,
8374
+ });
8375
+ // Snapshot composition: preserve the previous boolean result during re-evaluation.
8376
+ // Without this, the condition would briefly flash to pendingValue when the async
8377
+ // function is re-invoked, causing visible UI flicker.
8378
+ return withPreviousValue(asyncResource);
8211
8379
  });
8212
- signalPair = { trigger, resultValue };
8380
+ signalPair = { trigger, resultResource, ctxByTrigger };
8213
8381
  perFunctionSignalStore.set(contextKey, signalPair);
8214
8382
  }
8215
8383
  // Build reactive evaluation context (creates signal dependencies on form values)
@@ -8220,15 +8388,18 @@ function createAsyncConditionLogicFunction(condition) {
8220
8388
  fieldValue: evaluationContext.fieldValue,
8221
8389
  externalData: evaluationContext.externalData,
8222
8390
  });
8223
- // Update the trigger signal (triggers pipeline if context changed)
8224
- const { trigger: triggerSignal, resultValue } = signalPair;
8391
+ // Capture the FieldContext for this trigger key so the loader can retrieve it.
8392
+ // Only keep the latest — previous entries are stale once the trigger changes.
8393
+ const { trigger: triggerSignal, resultResource, ctxByTrigger } = signalPair;
8394
+ ctxByTrigger.clear();
8395
+ ctxByTrigger.set(contextSnapshot, ctx);
8225
8396
  untracked(() => {
8226
8397
  const current = triggerSignal();
8227
8398
  if (current !== contextSnapshot) {
8228
8399
  triggerSignal.set(contextSnapshot);
8229
8400
  }
8230
8401
  });
8231
- return resultValue();
8402
+ return resultResource.value();
8232
8403
  };
8233
8404
  cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
8234
8405
  return fn;
@@ -8356,7 +8527,7 @@ function createDebouncedLogicFunction(expression, debounceMs) {
8356
8527
  let signalPair = cacheService.debouncedSignalStore.get(contextKey);
8357
8528
  if (!signalPair) {
8358
8529
  // Create a signal to hold the immediate evaluation result
8359
- const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : []));
8530
+ const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : /* istanbul ignore next */ []));
8360
8531
  // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),
8361
8532
  // which cannot be created inside a reactive context (computed). The LogicFn runs
8362
8533
  // inside Angular Signal Forms' BooleanOrLogic.compute (a computed).
@@ -8519,7 +8690,18 @@ function applyValidator(config, fieldPath) {
8519
8690
  break;
8520
8691
  case 'pattern':
8521
8692
  if (config.value instanceof RegExp || typeof config.value === 'string') {
8522
- const regexPattern = typeof config.value === 'string' ? new RegExp(config.value) : config.value;
8693
+ let regexPattern;
8694
+ if (typeof config.value === 'string') {
8695
+ try {
8696
+ regexPattern = new RegExp(config.value);
8697
+ }
8698
+ catch (e) {
8699
+ throw new DynamicFormError(`Invalid regex pattern in validator: '${config.value}' — ${e instanceof Error ? e.message : String(e)}`);
8700
+ }
8701
+ }
8702
+ else {
8703
+ regexPattern = config.value;
8704
+ }
8523
8705
  applyPatternValidator(fieldPath, regexPattern, config.expression);
8524
8706
  }
8525
8707
  break;
@@ -8529,22 +8711,6 @@ function applyValidator(config, fieldPath) {
8529
8711
  case 'async':
8530
8712
  applyAsyncValidator(config, path);
8531
8713
  break;
8532
- case 'customAsync': {
8533
- // TODO(@ng-forge): remove deprecated code in next minor
8534
- const logger = inject(DynamicFormLogger);
8535
- const tracker = inject(DEPRECATION_WARNING_TRACKER);
8536
- warnDeprecated(logger, tracker, 'type:customAsync', "Validator type 'customAsync' is deprecated. Use type: 'async' instead.");
8537
- applyAsyncValidator(config, path);
8538
- break;
8539
- }
8540
- case 'customHttp': {
8541
- // TODO(@ng-forge): remove deprecated code in next minor
8542
- const logger = inject(DynamicFormLogger);
8543
- const tracker = inject(DEPRECATION_WARNING_TRACKER);
8544
- warnDeprecated(logger, tracker, 'type:customHttp', "Validator type 'customHttp' is deprecated. Use type: 'http' with functionName instead.");
8545
- applyFunctionHttpValidator(config, path);
8546
- break;
8547
- }
8548
8714
  case 'http':
8549
8715
  applyUnifiedHttpValidator(config, path);
8550
8716
  break;
@@ -8710,7 +8876,8 @@ function applyDeclarativeHttpValidator(config, fieldPath) {
8710
8876
  // validateHttp's request runs inside Angular's resource API — it NEEDS reactive
8711
8877
  // dependencies to re-trigger when the field value changes.
8712
8878
  const evalCtx = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
8713
- return resolveHttpRequest(config.http, evalCtx);
8879
+ // resolveHttpRequest returns null when a path param is undefined — convert to undefined to skip validation
8880
+ return resolveHttpRequest(config.http, evalCtx) ?? undefined;
8714
8881
  },
8715
8882
  onSuccess: (response) => {
8716
8883
  return evaluateHttpValidationResponse(response, config.responseMapping, logger);
@@ -8749,8 +8916,8 @@ function getConfigDebounceMs(config) {
8749
8916
  return undefined;
8750
8917
  }
8751
8918
  function applyLogic(config, fieldPath) {
8752
- // Value derivations and property derivations are handled by their respective orchestrators — skip them.
8753
- if (isDerivationLogicConfig(config) || isPropertyDerivationLogicConfig(config))
8919
+ // Value derivations (including property derivations via targetProperty) are handled by their orchestrators — skip them.
8920
+ if (isDerivationLogicConfig(config))
8754
8921
  return;
8755
8922
  // Guard against unrecognized logic types that may be added in the future.
8756
8923
  if (!isStateLogicConfig(config)) {
@@ -8787,6 +8954,10 @@ function applyLogicFn(type, path, logicFn) {
8787
8954
  case 'required':
8788
8955
  required(path, { when: logicFn });
8789
8956
  break;
8957
+ default: {
8958
+ const _exhaustive = type;
8959
+ throw new DynamicFormError(`Unhandled state logic type: ${_exhaustive}`);
8960
+ }
8790
8961
  }
8791
8962
  }
8792
8963
  function applyMultipleLogic(configs, fieldPath) {
@@ -8797,7 +8968,7 @@ function applyMultipleLogic(configs, fieldPath) {
8797
8968
  * Create a type predicate function from a predicate string.
8798
8969
  *
8799
8970
  * Uses the secure ExpressionParser with whitelist-based evaluation instead of
8800
- * new Function() / eval. This provides security through:
8971
+ * dynamic code execution. This provides security through:
8801
8972
  * - AST-based parsing (no code execution)
8802
8973
  * - Whitelisted operators only
8803
8974
  * - Whitelisted methods only (Array.isArray, typeof, instanceof, etc.)
@@ -8909,7 +9080,18 @@ function applyStringValidators(path, config) {
8909
9080
  maxLength(path, config.maxLength);
8910
9081
  }
8911
9082
  if (config.pattern) {
8912
- const regex = typeof config.pattern === 'string' ? new RegExp(config.pattern) : config.pattern;
9083
+ let regex;
9084
+ if (typeof config.pattern === 'string') {
9085
+ try {
9086
+ regex = new RegExp(config.pattern);
9087
+ }
9088
+ catch (e) {
9089
+ throw new DynamicFormError(`Invalid regex pattern: '${config.pattern}' — ${e instanceof Error ? e.message : String(e)}`);
9090
+ }
9091
+ }
9092
+ else {
9093
+ regex = config.pattern;
9094
+ }
8913
9095
  pattern(path, regex);
8914
9096
  }
8915
9097
  }
@@ -9916,7 +10098,7 @@ function tryCreateSchemaEntry(fieldKey, config) {
9916
10098
  function convertBuiltInToCustomValidator(config) {
9917
10099
  const expression = config.expression;
9918
10100
  if (!expression) {
9919
- throw new Error(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
10101
+ throw new DynamicFormError(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
9920
10102
  }
9921
10103
  let validationExpression;
9922
10104
  switch (config.type) {
@@ -9937,13 +10119,13 @@ function convertBuiltInToCustomValidator(config) {
9937
10119
  try {
9938
10120
  new RegExp(expression);
9939
10121
  }
9940
- catch {
9941
- throw new Error(`Invalid regex pattern in cross-field validator: ${expression}`);
10122
+ catch (e) {
10123
+ throw new DynamicFormError(`Invalid regex pattern in cross-field validator: '${expression}' — ${e instanceof Error ? e.message : String(e)}`);
9942
10124
  }
9943
10125
  validationExpression = `fieldValue == null || new RegExp(${JSON.stringify(expression)}).test(fieldValue)`;
9944
10126
  break;
9945
10127
  default:
9946
- throw new Error(`Cannot convert ${config.type} validator to custom validator`);
10128
+ throw new DynamicFormError(`Cannot convert ${config.type} validator to custom validator`);
9947
10129
  }
9948
10130
  return {
9949
10131
  type: 'custom',
@@ -10112,17 +10294,25 @@ function flattenFields(fields, registry, options = {}) {
10112
10294
  * @returns Object containing memoized processing functions
10113
10295
  */
10114
10296
  function createContainerFieldProcessors() {
10297
+ const createFieldsResolver = (preserveRows = false) => (fields, registry) => {
10298
+ let key = '';
10299
+ for (const f of fields) {
10300
+ key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
10301
+ }
10302
+ return key + registry.size + '|' + preserveRows;
10303
+ };
10115
10304
  const memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
10116
10305
  // registry.size is a valid cache key proxy because the field registry is populated
10117
10306
  // once at bootstrap and never mutated at runtime. If the registry were mutable,
10118
10307
  // we would need a content-based hash instead.
10119
- resolver: (fields, registry) => {
10120
- let key = '';
10121
- for (const f of fields) {
10122
- key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
10123
- }
10124
- return key + registry.size;
10125
- },
10308
+ resolver: createFieldsResolver(),
10309
+ maxSize: 10,
10310
+ });
10311
+ const memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields([...fields], registry, { preserveRows: true }), {
10312
+ // registry.size is a valid cache key proxy because the field registry is populated
10313
+ // once at bootstrap and never mutated at runtime. If the registry were mutable,
10314
+ // we would need a content-based hash instead.
10315
+ resolver: createFieldsResolver(true),
10126
10316
  maxSize: 10,
10127
10317
  });
10128
10318
  const memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
@@ -10144,7 +10334,7 @@ function createContainerFieldProcessors() {
10144
10334
  },
10145
10335
  maxSize: 10,
10146
10336
  });
10147
- return { memoizedFlattenFields, memoizedKeyBy, memoizedDefaultValues };
10337
+ return { memoizedFlattenFields, memoizedFlattenFieldsForRendering, memoizedKeyBy, memoizedDefaultValues };
10148
10338
  }
10149
10339
  /**
10150
10340
  * Shared container field processors injection token.
@@ -10166,50 +10356,6 @@ const CONTAINER_FIELD_PROCESSORS = new InjectionToken('CONTAINER_FIELD_PROCESSOR
10166
10356
  factory: () => createContainerFieldProcessors(),
10167
10357
  });
10168
10358
 
10169
- /**
10170
- * Type guard to check if a value is an array of Signals.
10171
- * @internal
10172
- */
10173
- function isSignalArray(value) {
10174
- return Array.isArray(value) && value.every((item) => isSignal(item));
10175
- }
10176
- /**
10177
- * Type guard to check if a value is a record (object) of Signals.
10178
- * @internal
10179
- */
10180
- function isSignalRecord(value) {
10181
- return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
10182
- }
10183
- // Implementation
10184
- function derivedFromDeferred(source, operator, options) {
10185
- const deferred$ = defer(() => {
10186
- // Handle single signal
10187
- if (isSignal(source)) {
10188
- return toObservable(source, { injector: options.injector });
10189
- }
10190
- // Handle array of signals
10191
- if (isSignalArray(source)) {
10192
- const observables = source.map((s) => toObservable(s, { injector: options.injector }));
10193
- return combineLatest(observables);
10194
- }
10195
- // Handle object of signals
10196
- if (isSignalRecord(source)) {
10197
- const keys = Object.keys(source);
10198
- const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
10199
- return combineLatest(observables, (...values) => {
10200
- const result = {};
10201
- keys.forEach((key, index) => {
10202
- result[key] = values[index];
10203
- });
10204
- return result;
10205
- });
10206
- }
10207
- // Fallback - should not happen with proper typing
10208
- throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
10209
- }).pipe(operator);
10210
- return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
10211
- }
10212
-
10213
10359
  /**
10214
10360
  * Generates CSS class string for responsive grid layout based on field column configuration.
10215
10361
  *
@@ -10409,6 +10555,38 @@ function createPropertyOverrideStore() {
10409
10555
  */
10410
10556
  const PROPERTY_OVERRIDE_STORE = new InjectionToken('PROPERTY_OVERRIDE_STORE');
10411
10557
 
10558
+ /**
10559
+ * Known standard field properties where a missing key likely indicates a typo.
10560
+ * Override keys not in this set are assumed to be intentional dynamic properties
10561
+ * and will not trigger dev-mode warnings.
10562
+ */
10563
+ const KNOWN_FIELD_PROPERTY_NAMES = [
10564
+ 'label',
10565
+ 'placeholder',
10566
+ 'disabled',
10567
+ 'readonly',
10568
+ 'required',
10569
+ 'className',
10570
+ 'tabIndex',
10571
+ 'validationMessages',
10572
+ 'options',
10573
+ 'props',
10574
+ 'hint',
10575
+ 'tooltip',
10576
+ 'prefix',
10577
+ 'suffix',
10578
+ 'min',
10579
+ 'max',
10580
+ 'minLength',
10581
+ 'maxLength',
10582
+ 'step',
10583
+ 'pattern',
10584
+ 'rows',
10585
+ 'cols',
10586
+ 'multiple',
10587
+ 'appearance',
10588
+ ];
10589
+ const KNOWN_FIELD_PROPERTIES = new Set(KNOWN_FIELD_PROPERTY_NAMES);
10412
10590
  /**
10413
10591
  * Applies property overrides to a field's input record.
10414
10592
  *
@@ -10425,8 +10603,9 @@ const PROPERTY_OVERRIDE_STORE = new InjectionToken('PROPERTY_OVERRIDE_STORE');
10425
10603
  * Array-valued overrides replace wholesale (no merging).
10426
10604
  * Returns `inputs` unchanged if overrides is empty (no clone).
10427
10605
  *
10428
- * In dev mode, warns when an override key doesn't match any existing input property,
10429
- * which may indicate a typo in the `targetProperty` configuration.
10606
+ * In dev mode, warns when a known standard field property override key doesn't match
10607
+ * any existing input property, which may indicate a typo in the `targetProperty`
10608
+ * configuration. Dynamic/custom property keys are silently accepted.
10430
10609
  *
10431
10610
  * @param inputs - The current field inputs record
10432
10611
  * @param overrides - Record of property names to override values
@@ -10443,12 +10622,12 @@ function applyPropertyOverrides(inputs, overrides) {
10443
10622
  for (const key of keys) {
10444
10623
  const value = overrides[key];
10445
10624
  const dotIndex = key.indexOf('.');
10446
- // Dev-mode check: warn if the override key doesn't match any existing input property.
10447
- // This catches typos like 'mindata' instead of 'minDate'. False positives are possible
10448
- // for properties intentionally added by derivations that aren't in the initial config.
10625
+ // Dev-mode check: warn only when a known standard field property is missing from inputs,
10626
+ // which likely indicates a typo. Dynamic/custom keys are intentionally skipped to avoid
10627
+ // false positives for properties added by derivations that aren't in the initial config.
10449
10628
  if (isDevMode()) {
10450
10629
  const topLevelKey = dotIndex === -1 ? key : key.substring(0, dotIndex);
10451
- if (!(topLevelKey in inputs)) {
10630
+ if (KNOWN_FIELD_PROPERTIES.has(topLevelKey) && !(topLevelKey in inputs)) {
10452
10631
  console.warn(`[Dynamic Forms] Property override '${key}' does not match any existing input property. ` +
10453
10632
  'This may indicate a typo in the targetProperty configuration. ' +
10454
10633
  `Available properties: ${Object.keys(inputs).join(', ')}`);
@@ -10647,7 +10826,7 @@ function resolveField(fieldDef, context) {
10647
10826
  injector: context.injector,
10648
10827
  inputs,
10649
10828
  };
10650
- }), catchError$1((error) => {
10829
+ }), catchError((error) => {
10651
10830
  // Only call onError if component is not destroyed to avoid accessing cleaned-up state
10652
10831
  if (!context.destroyRef.destroyed) {
10653
10832
  context.onError?.(fieldDef, error);
@@ -10706,7 +10885,7 @@ function reconcileFields(prev, curr) {
10706
10885
  * Used by container components (page, group) to resolve their child fields.
10707
10886
  */
10708
10887
  function createFieldResolutionPipe(getContext) {
10709
- return pipe(switchMap((fields) => {
10888
+ return pipe(switchMap$1((fields) => {
10710
10889
  if (!fields || fields.length === 0) {
10711
10890
  return of([]);
10712
10891
  }
@@ -11065,6 +11244,131 @@ function injectFieldRegistry() {
11065
11244
  };
11066
11245
  }
11067
11246
 
11247
+ /**
11248
+ * Collects all field keys, types, and validates regex patterns from a field tree
11249
+ * by recursively traversing containers (page, row, group, array).
11250
+ *
11251
+ * @param collectKeys - Whether to add field keys to data.keys for duplicate detection.
11252
+ * Set to false when inside an array container: array item fields share template keys
11253
+ * across items by design (e.g. every item has 'name', 'email'), so they must not
11254
+ * participate in global duplicate-key checking.
11255
+ */
11256
+ function collectFieldData(fields, data, collectKeys = true) {
11257
+ for (const field of fields) {
11258
+ if (collectKeys && field.key) {
11259
+ data.keys.push(field.key);
11260
+ }
11261
+ if (field.type) {
11262
+ data.types.add(field.type);
11263
+ }
11264
+ const fieldWithValidation = field;
11265
+ if ('pattern' in field && typeof fieldWithValidation.pattern === 'string') {
11266
+ validateRegexPattern(fieldWithValidation.pattern, field.key || '<unknown>', data.regexErrors);
11267
+ }
11268
+ if ('validators' in field && fieldWithValidation.validators) {
11269
+ for (const validator of fieldWithValidation.validators) {
11270
+ if (validator.type === 'pattern' && 'value' in validator && typeof validator.value === 'string') {
11271
+ validateRegexPattern(validator.value, field.key || '<unknown>', data.regexErrors);
11272
+ }
11273
+ }
11274
+ }
11275
+ if (hasChildFields(field)) {
11276
+ // Array item fields share template keys across items by design — stop collecting keys
11277
+ // once inside an array. Types and regex patterns are still validated.
11278
+ const childCollectKeys = collectKeys && field.type !== 'array';
11279
+ const children = normalizeFieldsArray(field.fields);
11280
+ for (const child of children) {
11281
+ if (Array.isArray(child)) {
11282
+ collectFieldData(child, data, childCollectKeys);
11283
+ }
11284
+ else {
11285
+ collectFieldData([child], data, childCollectKeys);
11286
+ }
11287
+ }
11288
+ }
11289
+ }
11290
+ }
11291
+ /**
11292
+ * Validates a single regex pattern string, collecting errors rather than throwing.
11293
+ */
11294
+ function validateRegexPattern(pattern, fieldKey, errors) {
11295
+ try {
11296
+ new RegExp(pattern);
11297
+ }
11298
+ catch (e) {
11299
+ errors.push(`Invalid regex pattern in validator for field '${fieldKey}': '${pattern}' — ${e instanceof Error ? e.message : String(e)}`);
11300
+ }
11301
+ }
11302
+ /**
11303
+ * Validates that no duplicate field keys exist in the config.
11304
+ * Throws a DynamicFormError listing all duplicates if any are found.
11305
+ *
11306
+ * @throws {DynamicFormError} When duplicate keys are detected
11307
+ */
11308
+ function validateNoDuplicateKeys(allKeys) {
11309
+ const seen = new Set();
11310
+ const duplicates = new Set();
11311
+ for (const key of allKeys) {
11312
+ if (seen.has(key)) {
11313
+ duplicates.add(key);
11314
+ }
11315
+ seen.add(key);
11316
+ }
11317
+ if (duplicates.size > 0) {
11318
+ const duplicateList = Array.from(duplicates)
11319
+ .map((k) => `'${k}'`)
11320
+ .join(', ');
11321
+ throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Each field key must be unique within a form config.`);
11322
+ }
11323
+ }
11324
+ /**
11325
+ * Validates that every field type referenced in the config exists in the registry.
11326
+ * Logs a warning for unregistered types — unknown fields are skipped during rendering
11327
+ * rather than blocking the whole form (graceful degradation).
11328
+ *
11329
+ * Skips validation when the registry is empty (no UI adapter has been registered),
11330
+ * since the core library tests operate without a field registry.
11331
+ */
11332
+ function validateFieldTypesRegistered(allTypes, registry, logger) {
11333
+ if (registry.size === 0)
11334
+ return;
11335
+ const unregistered = [];
11336
+ for (const type of allTypes) {
11337
+ if (!registry.has(type)) {
11338
+ unregistered.push(type);
11339
+ }
11340
+ }
11341
+ if (unregistered.length > 0) {
11342
+ const typeList = unregistered.map((t) => `'${t}'`).join(', ');
11343
+ logger.warn(`Unknown field type(s): ${typeList}. Register them via provideDynamicForm(...withXxxFields()) or a custom registry entry. These fields will be skipped during rendering.`);
11344
+ }
11345
+ }
11346
+ /**
11347
+ * Validates a form config at bootstrap time, checking for:
11348
+ * - Duplicate field keys (throws — this is always a developer error)
11349
+ * - Unregistered field types (warns — form degrades gracefully, skipping unknown fields)
11350
+ * - Invalid regex patterns in validators (throws — invalid regex will cause runtime errors)
11351
+ *
11352
+ * Should be called once during form setup, before fields are processed.
11353
+ *
11354
+ * @throws {DynamicFormError} When duplicate keys or invalid regex patterns are detected
11355
+ */
11356
+ function validateFormConfig(fields, registry, logger) {
11357
+ const data = {
11358
+ keys: [],
11359
+ types: new Set(),
11360
+ regexErrors: [],
11361
+ };
11362
+ collectFieldData(fields, data);
11363
+ validateNoDuplicateKeys(data.keys);
11364
+ validateFieldTypesRegistered(data.types, registry, logger);
11365
+ if (data.regexErrors.length > 0) {
11366
+ throw new DynamicFormError(data.regexErrors.length === 1
11367
+ ? data.regexErrors[0]
11368
+ : `Invalid regex pattern(s) found:\n${data.regexErrors.map((e) => ` - ${e}`).join('\n')}`);
11369
+ }
11370
+ }
11371
+
11068
11372
  /**
11069
11373
  * Injection token for global value exclusion defaults.
11070
11374
  *
@@ -11403,7 +11707,7 @@ class FormStateMachine {
11403
11707
  }
11404
11708
  constructor(config) {
11405
11709
  this.config = config;
11406
- this._state = signal(createUninitializedState(), ...(ngDevMode ? [{ debugName: "_state" }] : []));
11710
+ this._state = signal(createUninitializedState(), ...(ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []));
11407
11711
  this.state = this._state.asReadonly();
11408
11712
  this.state$ = toObservable(this._state, { injector: config.injector });
11409
11713
  this.setupActionProcessing();
@@ -11415,7 +11719,7 @@ class FormStateMachine {
11415
11719
  /** Sets up sequential action processing via `concatMap`. */
11416
11720
  setupActionProcessing() {
11417
11721
  this.actions$
11418
- .pipe(concatMap((action) => this.processAction(action).pipe(catchError$1((error) => {
11722
+ .pipe(concatMap((action) => this.processAction(action).pipe(catchError((error) => {
11419
11723
  this.config.logger.error(`Action '${action.type}' failed:`, error);
11420
11724
  this.recoverFromError(error, action);
11421
11725
  return EMPTY;
@@ -11498,6 +11802,12 @@ class FormStateMachine {
11498
11802
  sideEffects: [{ type: Effect.CreateForm }],
11499
11803
  };
11500
11804
  }
11805
+ // Guard: reject config changes during active submission to prevent races
11806
+ // between the in-flight action Promise and form teardown/rebuild.
11807
+ if (this.config.isSubmitting?.()) {
11808
+ this.config.logger.warn('Config changed during active submission; ignoring. Apply config changes after submission completes.');
11809
+ return { state, sideEffects: [] };
11810
+ }
11501
11811
  if (isReadyState(state)) {
11502
11812
  return {
11503
11813
  state: createTransitioningState(Phase.Teardown, state.config, config, state.formSetup),
@@ -11671,7 +11981,6 @@ class FormStateManager {
11671
11981
  injector = inject(Injector);
11672
11982
  destroyRef = inject(DestroyRef);
11673
11983
  logger = inject(DynamicFormLogger);
11674
- deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });
11675
11984
  eventBus = inject(EventBus);
11676
11985
  functionRegistry = inject(FunctionRegistryService);
11677
11986
  schemaRegistry = inject(SchemaRegistryService);
@@ -11698,26 +12007,13 @@ class FormStateManager {
11698
12007
  // Memoized Functions
11699
12008
  // ─────────────────────────────────────────────────────────────────────────────
11700
12009
  fieldProcessors = inject(CONTAINER_FIELD_PROCESSORS);
11701
- memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields(fields, registry, { preserveRows: true }), {
11702
- // registry.size is a valid cache key proxy because the field registry is populated
11703
- // once at bootstrap and never mutated at runtime. If the registry were mutable,
11704
- // we would need a content-based hash instead.
11705
- resolver: (fields, registry) => {
11706
- let key = '';
11707
- for (const f of fields) {
11708
- key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
11709
- }
11710
- return key + registry.size;
11711
- },
11712
- maxSize: 10,
11713
- });
11714
12010
  // ─────────────────────────────────────────────────────────────────────────────
11715
12011
  // Internal State Signals
11716
12012
  // ─────────────────────────────────────────────────────────────────────────────
11717
12013
  /**
11718
12014
  * Field loading errors accumulated during resolution.
11719
12015
  */
11720
- fieldLoadingErrors = signal([], ...(ngDevMode ? [{ debugName: "fieldLoadingErrors" }] : []));
12016
+ fieldLoadingErrors = signal([], ...(ngDevMode ? [{ debugName: "fieldLoadingErrors" }] : /* istanbul ignore next */ []));
11721
12017
  /**
11722
12018
  * Class-level form cache for the "hold until settled" pattern.
11723
12019
  * MUST be class-level — a local variable in the computed would reset on re-evaluation.
@@ -11748,7 +12044,7 @@ class FormStateManager {
11748
12044
  if (state.type === LifecycleState.Initializing)
11749
12045
  return state.config;
11750
12046
  return undefined;
11751
- }, ...(ngDevMode ? [{ debugName: "activeConfig" }] : []));
12047
+ }, ...(ngDevMode ? [{ debugName: "activeConfig" }] : /* istanbul ignore next */ []));
11752
12048
  /**
11753
12049
  * Current render phase: 'render' = showing form, 'teardown' = hiding old components.
11754
12050
  */
@@ -11757,9 +12053,9 @@ class FormStateManager {
11757
12053
  if (isTransitioningState(state) && state.phase === Phase.Teardown)
11758
12054
  return Phase.Teardown;
11759
12055
  return 'render';
11760
- }, ...(ngDevMode ? [{ debugName: "renderPhase" }] : []));
12056
+ }, ...(ngDevMode ? [{ debugName: "renderPhase" }] : /* istanbul ignore next */ []));
11761
12057
  /** Whether to render the form template. */
11762
- shouldRender = computed(() => this.activeConfig() !== undefined, ...(ngDevMode ? [{ debugName: "shouldRender" }] : []));
12058
+ shouldRender = computed(() => this.activeConfig() !== undefined, ...(ngDevMode ? [{ debugName: "shouldRender" }] : /* istanbul ignore next */ []));
11763
12059
  /**
11764
12060
  * Computed form mode detection with validation.
11765
12061
  */
@@ -11769,14 +12065,14 @@ class FormStateManager {
11769
12065
  return { mode: 'non-paged', isValid: true, errors: [] };
11770
12066
  }
11771
12067
  return detectFormMode(config.fields || []);
11772
- }, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : []));
12068
+ }, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : /* istanbul ignore next */ []));
11773
12069
  /** Validation result for the current form configuration. */
11774
12070
  formConfigValidation = computed(() => {
11775
12071
  const config = this.activeConfig();
11776
12072
  if (!config)
11777
12073
  return { isValid: true, errors: [], warnings: [] };
11778
12074
  return FormModeValidator.validateFormConfiguration(config.fields || []);
11779
- }, ...(ngDevMode ? [{ debugName: "formConfigValidation" }] : []));
12075
+ }, ...(ngDevMode ? [{ debugName: "formConfigValidation" }] : /* istanbul ignore next */ []));
11780
12076
  /**
11781
12077
  * Effective form options (merged from config and input).
11782
12078
  */
@@ -11785,7 +12081,7 @@ class FormStateManager {
11785
12081
  const configOptions = config?.options || {};
11786
12082
  const inputOptions = this.deps.formOptions() ?? undefined;
11787
12083
  return { ...configOptions, ...inputOptions };
11788
- }, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : []));
12084
+ }, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : /* istanbul ignore next */ []));
11789
12085
  /**
11790
12086
  * Page field definitions (for paged forms).
11791
12087
  */
@@ -11796,11 +12092,11 @@ class FormStateManager {
11796
12092
  return config.fields.filter(isPageField);
11797
12093
  }
11798
12094
  return [];
11799
- }, ...(ngDevMode ? [{ debugName: "pageFieldDefinitions" }] : []));
12095
+ }, ...(ngDevMode ? [{ debugName: "pageFieldDefinitions" }] : /* istanbul ignore next */ []));
11800
12096
  // ─────────────────────────────────────────────────────────────────────────────
11801
12097
  // Computed Signals - Form Setup
11802
12098
  // ─────────────────────────────────────────────────────────────────────────────
11803
- rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
12099
+ rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
11804
12100
  /** Computed form setup — reads from the state machine, computed from config on bootstrap. */
11805
12101
  formSetup = computed(() => {
11806
12102
  const registry = this.rawFieldRegistry();
@@ -11831,18 +12127,18 @@ class FormStateManager {
11831
12127
  return this.createFormSetupFromConfig(config.fields, modeDetection.mode, registry);
11832
12128
  }
11833
12129
  return this.createEmptyFormSetup(registry);
11834
- }, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
12130
+ }, ...(ngDevMode ? [{ debugName: "formSetup" }] : /* istanbul ignore next */ []));
11835
12131
  /**
11836
12132
  * Default values computed from field definitions.
11837
12133
  */
11838
- defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
12134
+ defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
11839
12135
  /** Valid field keys — memoized separately so the Set isn't rebuilt on every keystroke. */
11840
12136
  validKeys = computed(() => {
11841
12137
  const schemaFields = this.formSetup().schemaFields;
11842
12138
  if (!schemaFields || schemaFields.length === 0)
11843
12139
  return undefined;
11844
12140
  return new Set(schemaFields.map((f) => f.key).filter((key) => key !== undefined));
11845
- }, ...(ngDevMode ? [{ debugName: "validKeys" }] : []));
12141
+ }, ...(ngDevMode ? [{ debugName: "validKeys" }] : /* istanbul ignore next */ []));
11846
12142
  // ─────────────────────────────────────────────────────────────────────────────
11847
12143
  // Computed Signals - Entity & Form
11848
12144
  // ─────────────────────────────────────────────────────────────────────────────
@@ -11881,39 +12177,36 @@ class FormStateManager {
11881
12177
  debugName: 'FormStateManager.entity',
11882
12178
  equal: isEqual,
11883
12179
  });
12180
+ /**
12181
+ * Schema derived from the current form config and field setup.
12182
+ * Separated from `form` so schema construction is memoized independently.
12183
+ * Requires `runInInjectionContext` because `createSchemaFromFields` calls `inject()` internally.
12184
+ */
12185
+ formSchema = computed(() => runInInjectionContext(this.injector, () => {
12186
+ const setup = this.formSetup();
12187
+ const config = this.activeConfig();
12188
+ if (!config)
12189
+ return undefined;
12190
+ if (setup.schemaFields?.length) {
12191
+ const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields);
12192
+ return createSchemaFromFields(setup.schemaFields, setup.registry, {
12193
+ crossFieldValidators: crossFieldCollection.validators,
12194
+ formLevelSchema: config.schema,
12195
+ });
12196
+ }
12197
+ if (config.schema) {
12198
+ return createFormLevelSchema(config.schema);
12199
+ }
12200
+ return undefined;
12201
+ }), ...(ngDevMode ? [{ debugName: "formSchema" }] : /* istanbul ignore next */ []));
11884
12202
  /**
11885
12203
  * The Angular Signal Form instance.
11886
12204
  */
11887
12205
  form = computed(() => {
11888
- return runInInjectionContext(this.injector, () => {
11889
- const setup = this.formSetup();
11890
- const config = this.activeConfig();
11891
- let formInstance;
11892
- if (!config) {
11893
- formInstance = untracked(() => form(this.entity));
11894
- }
11895
- else {
11896
- const hasFields = setup.schemaFields && setup.schemaFields.length > 0;
11897
- const hasFormSchema = config.schema !== undefined;
11898
- if (hasFields) {
11899
- const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields);
11900
- const combinedSchema = createSchemaFromFields(setup.schemaFields, setup.registry, {
11901
- crossFieldValidators: crossFieldCollection.validators,
11902
- formLevelSchema: config.schema,
11903
- });
11904
- formInstance = untracked(() => form(this.entity, combinedSchema));
11905
- }
11906
- else if (hasFormSchema) {
11907
- const formSchema = createFormLevelSchema(config.schema);
11908
- formInstance = untracked(() => form(this.entity, formSchema));
11909
- }
11910
- else {
11911
- formInstance = untracked(() => form(this.entity));
11912
- }
11913
- }
11914
- return formInstance;
11915
- });
11916
- }, ...(ngDevMode ? [{ debugName: "form" }] : []));
12206
+ const schema = this.formSchema();
12207
+ const injector = this.injector;
12208
+ return untracked(() => (schema ? form(this.entity, schema, { injector }) : form(this.entity, { injector })));
12209
+ }, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
11917
12210
  /** Whether resolvedFields has caught up with fieldsSource (set in constructor). */
11918
12211
  isFieldPipelineSettled;
11919
12212
  /**
@@ -11940,14 +12233,14 @@ class FormStateManager {
11940
12233
  return currentForm;
11941
12234
  },
11942
12235
  };
11943
- }, ...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
12236
+ }, ...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : /* istanbul ignore next */ []));
11944
12237
  // ─────────────────────────────────────────────────────────────────────────────
11945
12238
  // Computed Signals - Form State
11946
12239
  // ─────────────────────────────────────────────────────────────────────────────
11947
12240
  /** Intermediate computed that unwraps the double-signal (form()()) once. */
11948
- formInstance = computed(() => this.form()(), ...(ngDevMode ? [{ debugName: "formInstance" }] : []));
12241
+ formInstance = computed(() => this.form()(), ...(ngDevMode ? [{ debugName: "formInstance" }] : /* istanbul ignore next */ []));
11949
12242
  /** Current form values (reactive). */
11950
- formValue = computed(() => this.formInstance().value(), ...(ngDevMode ? [{ debugName: "formValue" }] : []));
12243
+ formValue = computed(() => this.formInstance().value(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
11951
12244
  /**
11952
12245
  * Form values filtered by value exclusion rules.
11953
12246
  *
@@ -11977,25 +12270,25 @@ class FormStateManager {
11977
12270
  // FieldTree doesn't expose this index signature, so we use a helper cast.
11978
12271
  const fieldTreeRecord = asFieldTreeRecord(formTree);
11979
12272
  return filterFormValue(rawValue, setup.schemaFields, fieldTreeRecord, setup.registry, this.valueExclusionDefaults, formOptions);
11980
- }, ...(ngDevMode ? [{ debugName: "filteredFormValue" }] : []));
12273
+ }, ...(ngDevMode ? [{ debugName: "filteredFormValue" }] : /* istanbul ignore next */ []));
11981
12274
  /** Whether the form is currently valid. */
11982
- valid = computed(() => this.formInstance().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
12275
+ valid = computed(() => this.formInstance().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
11983
12276
  /** Whether the form is currently invalid. */
11984
- invalid = computed(() => this.formInstance().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
12277
+ invalid = computed(() => this.formInstance().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
11985
12278
  /** Whether any form field has been modified. */
11986
- dirty = computed(() => this.formInstance().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
12279
+ dirty = computed(() => this.formInstance().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
11987
12280
  /** Whether any form field has been touched (blurred). */
11988
- touched = computed(() => this.formInstance().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
12281
+ touched = computed(() => this.formInstance().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
11989
12282
  /** Current validation errors from all fields. */
11990
- errors = computed(() => this.formInstance().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
12283
+ errors = computed(() => this.formInstance().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
11991
12284
  /** Whether the form is disabled (from options or form state). */
11992
12285
  disabled = computed(() => {
11993
12286
  const optionsDisabled = this.effectiveFormOptions().disabled;
11994
12287
  const formDisabled = this.formInstance().disabled();
11995
12288
  return optionsDisabled ?? formDisabled;
11996
- }, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
12289
+ }, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
11997
12290
  /** Whether the form is currently submitting. */
11998
- submitting = computed(() => this.formInstance().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
12291
+ submitting = computed(() => this.formInstance().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
11999
12292
  // ─────────────────────────────────────────────────────────────────────────────
12000
12293
  // Field Resolution
12001
12294
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12018,7 +12311,7 @@ class FormStateManager {
12018
12311
  }
12019
12312
  }
12020
12313
  return this.formSetup().fields;
12021
- }, { ...(ngDevMode ? { debugName: "fieldsSource" } : {}), equal: (a, b) => {
12314
+ }, { ...(ngDevMode ? { debugName: "fieldsSource" } : /* istanbul ignore next */ {}), equal: (a, b) => {
12022
12315
  if (a === b)
12023
12316
  return true;
12024
12317
  if (a.length !== b.length)
@@ -12037,7 +12330,7 @@ class FormStateManager {
12037
12330
  fieldInjector = computed(() => Injector.create({
12038
12331
  parent: this.injector,
12039
12332
  providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: this.fieldSignalContext() }],
12040
- }), ...(ngDevMode ? [{ debugName: "fieldInjector" }] : []));
12333
+ }), ...(ngDevMode ? [{ debugName: "fieldInjector" }] : /* istanbul ignore next */ []));
12041
12334
  /** Resolved fields ready for rendering. */
12042
12335
  resolvedFields;
12043
12336
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12057,7 +12350,7 @@ class FormStateManager {
12057
12350
  // Constructor
12058
12351
  // ─────────────────────────────────────────────────────────────────────────────
12059
12352
  constructor() {
12060
- this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
12353
+ this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap$1((fields) => {
12061
12354
  if (!fields || fields.length === 0) {
12062
12355
  return of([]);
12063
12356
  }
@@ -12117,14 +12410,17 @@ class FormStateManager {
12117
12410
  // fieldsSource changes atomically during transitions (lengths won't match until settled).
12118
12411
  this.isFieldPipelineSettled = computed(() => {
12119
12412
  return this.fieldsSource().length === this.resolvedFields().length;
12120
- }, ...(ngDevMode ? [{ debugName: "isFieldPipelineSettled" }] : []));
12413
+ }, ...(ngDevMode ? [{ debugName: "isFieldPipelineSettled" }] : /* istanbul ignore next */ []));
12121
12414
  this.submitted$ = this.eventBus.on('submit').pipe(filter(() => {
12122
12415
  if (!this.valid()) {
12123
12416
  this.logger.debug('Form submitted while invalid, not emitting to (submitted) output');
12124
12417
  return false;
12125
12418
  }
12126
12419
  return true;
12127
- }), switchMap(() => {
12420
+ }),
12421
+ // switchMap is intentional here: mapping is synchronous (reads signals, no async Promise).
12422
+ // The exhaustMap concern in createSubmissionHandler does not apply to this path.
12423
+ switchMap$1(() => {
12128
12424
  const config = this.activeConfig();
12129
12425
  const submissionConfig = config?.submission;
12130
12426
  if (submissionConfig?.action) {
@@ -12148,6 +12444,7 @@ class FormStateManager {
12148
12444
  return this.createFormSetupFromConfig(config.fields ?? [], detectFormMode(config.fields ?? []).mode, this.rawFieldRegistry());
12149
12445
  },
12150
12446
  captureValue: () => this.formValue(),
12447
+ isSubmitting: this.submitting,
12151
12448
  isFieldPipelineSettled: () => this.isFieldPipelineSettled(),
12152
12449
  restoreValue: (values, validKeys) => {
12153
12450
  const filtered = {};
@@ -12261,13 +12558,6 @@ class FormStateManager {
12261
12558
  if (customFnConfig.derivations) {
12262
12559
  this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);
12263
12560
  }
12264
- // TODO(@ng-forge): remove deprecated code in next minor
12265
- if (customFnConfig.propertyDerivations) {
12266
- if (this.deprecationTracker) {
12267
- warnDeprecated(this.logger, this.deprecationTracker, 'customFnConfig:propertyDerivations', "customFnConfig.propertyDerivations is deprecated. Register functions under 'derivations' instead and use type: 'derivation' with targetProperty.");
12268
- }
12269
- this.functionRegistry.setPropertyDerivationFunctions(customFnConfig.propertyDerivations);
12270
- }
12271
12561
  if (customFnConfig.asyncDerivations) {
12272
12562
  this.functionRegistry.setAsyncDerivationFunctions(customFnConfig.asyncDerivations);
12273
12563
  }
@@ -12276,16 +12566,15 @@ class FormStateManager {
12276
12566
  }
12277
12567
  this.functionRegistry.setValidators(customFnConfig.validators);
12278
12568
  this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);
12279
- // TODO(@ng-forge): remove deprecated code in next minor
12280
- if (customFnConfig.httpValidators && this.deprecationTracker) {
12281
- warnDeprecated(this.logger, this.deprecationTracker, 'customFnConfig:httpValidators', "customFnConfig.httpValidators registration path is deprecated. Use type: 'http' with functionName (for function-based) or http + responseMapping (for declarative).");
12282
- }
12283
12569
  this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);
12284
12570
  }
12285
12571
  createFormSetupFromConfig(fields, mode, registry) {
12286
12572
  const normalizedFields = normalizeSimplifiedArrays(fields);
12573
+ // Validate after normalization so simplified array templates are already expanded
12574
+ // into full ArrayField.fields and are reachable during traversal.
12575
+ validateFormConfig(normalizedFields, registry, this.logger);
12287
12576
  const flattenedFields = this.fieldProcessors.memoizedFlattenFields(normalizedFields, registry);
12288
- const flattenedFieldsForRendering = this.memoizedFlattenFieldsForRendering(normalizedFields, registry);
12577
+ const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(normalizedFields, registry);
12289
12578
  const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
12290
12579
  const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
12291
12580
  const fieldsToRender = mode === 'paged' ? [] : flattenedFieldsForRendering;
@@ -12313,10 +12602,10 @@ class FormStateManager {
12313
12602
  ngOnDestroy() {
12314
12603
  this.machine.dispatch({ type: Action.Destroy });
12315
12604
  }
12316
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
12317
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FormStateManager });
12605
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
12606
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FormStateManager });
12318
12607
  }
12319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FormStateManager, decorators: [{
12608
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FormStateManager, decorators: [{
12320
12609
  type: Injectable
12321
12610
  }], ctorParameters: () => [] });
12322
12611
 
@@ -12496,25 +12785,6 @@ class ConsoleLogger {
12496
12785
  }
12497
12786
  }
12498
12787
 
12499
- /**
12500
- * No-operation logger implementation.
12501
- * All methods are no-ops - used in production by default.
12502
- */
12503
- class NoopLogger {
12504
- debug() {
12505
- // Intentionally empty
12506
- }
12507
- info() {
12508
- // Intentionally empty
12509
- }
12510
- warn() {
12511
- // Intentionally empty
12512
- }
12513
- error() {
12514
- // Intentionally empty
12515
- }
12516
- }
12517
-
12518
12788
  /**
12519
12789
  * Injection token for derivation logging configuration.
12520
12790
  *
@@ -12662,6 +12932,87 @@ function provideDynamicFormDI() {
12662
12932
  ];
12663
12933
  }
12664
12934
 
12935
+ /**
12936
+ * Injectable service for dispatching events into a DynamicForm from outside the form.
12937
+ *
12938
+ * ## When to use
12939
+ *
12940
+ * Use `EventDispatcher` when you need to drive form behaviour **from the host component** —
12941
+ * for example, appending array items in response to a field value change, triggering a form
12942
+ * reset from a toolbar button, or reacting to external application state.
12943
+ *
12944
+ * This is the recommended alternative to accessing the form's internals via `viewChild`.
12945
+ *
12946
+ * ## Setup
12947
+ *
12948
+ * Provide `EventDispatcher` at the **host component** level (not root). DynamicForm
12949
+ * automatically detects it and connects its internal event bus to the dispatcher.
12950
+ *
12951
+ * ```typescript
12952
+ * @Component({
12953
+ * providers: [EventDispatcher],
12954
+ * template: `<form [dynamic-form]="config" [(value)]="formValue"></form>`
12955
+ * })
12956
+ * export class MyComponent {
12957
+ * readonly dispatcher = inject(EventDispatcher);
12958
+ * readonly formValue = signal<Record<string, unknown>>({});
12959
+ *
12960
+ * constructor() {
12961
+ * effect(() => {
12962
+ * const category = this.formValue()?.['category'] as string | undefined;
12963
+ * if (category) {
12964
+ * this.dispatcher.dispatch(
12965
+ * arrayEvent('tasks').append([{ key: 'name', type: 'input', label: 'Task', value: category }])
12966
+ * );
12967
+ * }
12968
+ * });
12969
+ * }
12970
+ * }
12971
+ * ```
12972
+ *
12973
+ * ## Multi-form note
12974
+ *
12975
+ * If multiple `DynamicForm` instances exist under the same provider scope, **all forms**
12976
+ * will receive dispatched events. To target a specific form, scope the provider to a
12977
+ * wrapper component that contains only that form.
12978
+ *
12979
+ * ## What events can be dispatched
12980
+ *
12981
+ * Any `FormEvent` instance is accepted — array events, reset/clear events, custom events, etc.
12982
+ * The `arrayEvent()` builder is the recommended way to construct array manipulation events:
12983
+ *
12984
+ * ```typescript
12985
+ * dispatcher.dispatch(arrayEvent('tasks').append(template));
12986
+ * dispatcher.dispatch(arrayEvent('tasks').removeAt(0));
12987
+ * dispatcher.dispatch(new FormResetEvent());
12988
+ * ```
12989
+ */
12990
+ class EventDispatcher {
12991
+ bus;
12992
+ /**
12993
+ * Dispatches a form event into the connected DynamicForm's event bus.
12994
+ * No-op if no form is currently connected.
12995
+ *
12996
+ * @param event - A FormEvent instance. Use the `arrayEvent()` builder for array operations.
12997
+ */
12998
+ dispatch(event) {
12999
+ this.bus?.emitInstance(event);
13000
+ }
13001
+ /** @internal - Called by DynamicForm on init */
13002
+ connect(bus) {
13003
+ this.bus = bus;
13004
+ }
13005
+ /** @internal - Called by DynamicForm on destroy */
13006
+ disconnect() {
13007
+ this.bus = undefined;
13008
+ }
13009
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
13010
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventDispatcher });
13011
+ }
13012
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: EventDispatcher, decorators: [{
13013
+ type: Injectable
13014
+ }] });
13015
+
12665
13016
  /**
12666
13017
  * Dynamic form component — renders a form based on configuration.
12667
13018
  * Delegates state management to `FormStateManager`.
@@ -12676,32 +13027,23 @@ class DynamicForm {
12676
13027
  // Inputs (must be declared BEFORE deps connection and stateManager injection)
12677
13028
  // ─────────────────────────────────────────────────────────────────────────────
12678
13029
  /** Form configuration defining the structure, validation, and behavior. */
12679
- config = input.required({ ...(ngDevMode ? { debugName: "config" } : {}), alias: 'dynamic-form' });
13030
+ config = input.required({ ...(ngDevMode ? { debugName: "config" } : /* istanbul ignore next */ {}), alias: 'dynamic-form' });
12680
13031
  /** Runtime form options that override config options when provided. */
12681
- formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : []));
13032
+ formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : /* istanbul ignore next */ []));
12682
13033
  /** Form values for two-way data binding. */
12683
- value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : []));
13034
+ value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
12684
13035
  // ─────────────────────────────────────────────────────────────────────────────
12685
13036
  // Dependencies
12686
13037
  // ─────────────────────────────────────────────────────────────────────────────
12687
13038
  destroyRef = inject(DestroyRef);
12688
13039
  injector = inject(Injector);
13040
+ environmentInjector = inject(EnvironmentInjector);
12689
13041
  eventBus = inject(EventBus);
12690
13042
  logger = inject(DynamicFormLogger);
12691
- /**
12692
- * Connect input signals to the shared deps holder BEFORE injecting FormStateManager.
12693
- * Field initializers run in declaration order, so this runs after inputs are created
12694
- * but before stateManager is injected. FormStateManager reads from these signals
12695
- * in computeds/effects, which are all lazy and evaluated after construction.
12696
- */
12697
- _connectStateDeps = (() => {
12698
- const deps = inject(FORM_STATE_DEPS);
12699
- deps.config = this.config;
12700
- deps.formOptions = this.formOptions;
12701
- deps.value = this.value;
12702
- })();
12703
- /** State manager that owns all form state. */
12704
- stateManager = inject((FormStateManager));
13043
+ dispatcher = inject(EventDispatcher, { optional: true });
13044
+ /** State manager that owns all form state. Initialized via connectDeps() to guarantee
13045
+ * FORM_STATE_DEPS is populated before FormStateManager is injected. */
13046
+ stateManager = this.connectDeps();
12705
13047
  // ─────────────────────────────────────────────────────────────────────────────
12706
13048
  // Private State
12707
13049
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12750,10 +13092,17 @@ class DynamicForm {
12750
13092
  // ─────────────────────────────────────────────────────────────────────────────
12751
13093
  // Computed Signals - Internal
12752
13094
  // ─────────────────────────────────────────────────────────────────────────────
13095
+ /**
13096
+ * Recursively counts container components that will emit ComponentInitializedEvent.
13097
+ * Includes the dynamic-form component itself (+1).
13098
+ *
13099
+ * Recurses into all container children, including those nested inside array
13100
+ * item templates, to avoid a premature (initialized) emission.
13101
+ */
12753
13102
  totalComponentsCount = computed(() => {
12754
13103
  const fields = this.stateManager.formSetup()?.fields ?? [];
12755
- return fields.filter(isContainerField).length + 1;
12756
- }, ...(ngDevMode ? [{ debugName: "totalComponentsCount" }] : []));
13104
+ return countContainersRecursive(fields) + 1;
13105
+ }, ...(ngDevMode ? [{ debugName: "totalComponentsCount" }] : /* istanbul ignore next */ []));
12757
13106
  // ─────────────────────────────────────────────────────────────────────────────
12758
13107
  // Initialization
12759
13108
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12799,14 +13148,16 @@ class DynamicForm {
12799
13148
  // Constructor
12800
13149
  // ─────────────────────────────────────────────────────────────────────────────
12801
13150
  constructor() {
13151
+ this.dispatcher?.connect(this.eventBus);
13152
+ this.destroyRef.onDestroy(() => this.dispatcher?.disconnect());
13153
+ // Inject orchestrators eagerly so derivation streams are set up before the first
13154
+ // render cycle. Previously afterNextRender() deferred injection, causing
13155
+ // (initialized) to fire before derivations ran. The circular dependency that
13156
+ // originally motivated the deferral is resolved via dynamic-form-di.ts.
13157
+ this.injector.get(DERIVATION_ORCHESTRATOR);
13158
+ this.injector.get(PROPERTY_DERIVATION_ORCHESTRATOR);
12802
13159
  this.setupEffects();
12803
13160
  this.setupEventHandlers();
12804
- // Lazily inject orchestrators to avoid circular dependency.
12805
- // The orchestrators' constructors set up RxJS subscriptions that process derivations.
12806
- afterNextRender(() => {
12807
- this.injector.get(DERIVATION_ORCHESTRATOR);
12808
- this.injector.get(PROPERTY_DERIVATION_ORCHESTRATOR);
12809
- });
12810
13161
  }
12811
13162
  /**
12812
13163
  * Handles native form submission triggered by:
@@ -12842,39 +13193,80 @@ class DynamicForm {
12842
13193
  }
12843
13194
  });
12844
13195
  }
13196
+ /**
13197
+ * Populates FORM_STATE_DEPS with this component's input signals, then injects
13198
+ * FormStateManager. Must be called as a field initializer (after the input signals
13199
+ * are declared) so that FormStateManager reads populated deps when it is constructed.
13200
+ * inject() is valid here because field initializers run inside the injection context.
13201
+ */
13202
+ connectDeps() {
13203
+ const deps = inject(FORM_STATE_DEPS);
13204
+ deps.config = this.config;
13205
+ deps.formOptions = this.formOptions;
13206
+ deps.value = this.value;
13207
+ return inject((FormStateManager));
13208
+ }
12845
13209
  setupEventHandlers() {
12846
13210
  createSubmissionHandler({
12847
13211
  eventBus: this.eventBus,
12848
13212
  configSignal: this.config,
12849
13213
  formSignal: this.form,
13214
+ validSignal: this.valid,
13215
+ logger: this.logger,
12850
13216
  })
12851
13217
  .pipe(takeUntilDestroyed(this.destroyRef))
12852
13218
  .subscribe({
12853
13219
  error: (err) => this.logger.error('Submission handler error', err),
12854
13220
  });
12855
13221
  }
12856
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
12857
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { attributes: { "novalidate": "" }, listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [provideDynamicFormDI()], ngImport: i0, template: `
13222
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
13223
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { attributes: { "novalidate": "" }, listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [provideDynamicFormDI()], ngImport: i0, template: `
12858
13224
  @if (shouldRender()) {
12859
- @if (formModeDetection().mode === 'paged') {
12860
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
12861
- } @else {
12862
- @for (field of resolvedFields(); track field.key) {
12863
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
13225
+ @switch (formModeDetection().mode) {
13226
+ @case ('paged') {
13227
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
13228
+ }
13229
+ @case ('non-paged') {
13230
+ @for (field of resolvedFields(); track field.key) {
13231
+ <ng-container
13232
+ *ngComponentOutlet="
13233
+ field.component;
13234
+ injector: field.injector;
13235
+ environmentInjector: environmentInjector;
13236
+ inputs: field.inputs()
13237
+ "
13238
+ />
13239
+ }
13240
+ }
13241
+ @default {
13242
+ never;
12864
13243
  }
12865
13244
  }
12866
13245
  }
12867
13246
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{--df-grid-row-gap: 0}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: PageOrchestratorComponent, selector: "div[page-orchestrator]", inputs: ["pageFields", "form", "fieldSignalContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12868
13247
  }
12869
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicForm, decorators: [{
13248
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicForm, decorators: [{
12870
13249
  type: Component,
12871
13250
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
12872
13251
  @if (shouldRender()) {
12873
- @if (formModeDetection().mode === 'paged') {
12874
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
12875
- } @else {
12876
- @for (field of resolvedFields(); track field.key) {
12877
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
13252
+ @switch (formModeDetection().mode) {
13253
+ @case ('paged') {
13254
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
13255
+ }
13256
+ @case ('non-paged') {
13257
+ @for (field of resolvedFields(); track field.key) {
13258
+ <ng-container
13259
+ *ngComponentOutlet="
13260
+ field.component;
13261
+ injector: field.injector;
13262
+ environmentInjector: environmentInjector;
13263
+ inputs: field.inputs()
13264
+ "
13265
+ />
13266
+ }
13267
+ }
13268
+ @default {
13269
+ never;
12878
13270
  }
12879
13271
  }
12880
13272
  }
@@ -12888,6 +13280,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
12888
13280
  '(submit)': 'onNativeSubmit($event)',
12889
13281
  }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{--df-grid-row-gap: 0}\n"] }]
12890
13282
  }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamic-form", required: true }] }], formOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formOptions", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], reset: [{ type: i0.Output, args: ["reset"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], events: [{ type: i0.Output, args: ["events"] }], initialized: [{ type: i0.Output, args: ["initialized"] }], onPageChange: [{ type: i0.Output, args: ["onPageChange"] }], onPageNavigationStateChange: [{ type: i0.Output, args: ["onPageNavigationStateChange"] }] } });
13283
+ /**
13284
+ * Recursively counts container fields (page, row, group, array) in a field tree.
13285
+ * Descends into container children including array item templates to ensure
13286
+ * nested containers are counted for accurate initialization tracking.
13287
+ */
13288
+ function countContainersRecursive(fields) {
13289
+ let count = 0;
13290
+ for (const field of fields) {
13291
+ if (isContainerField(field)) {
13292
+ count += 1;
13293
+ if (hasChildFields(field)) {
13294
+ const children = field.fields;
13295
+ if (Array.isArray(children)) {
13296
+ for (const child of children) {
13297
+ if (Array.isArray(child)) {
13298
+ // Array item template: FieldDef[] (object items)
13299
+ count += countContainersRecursive(child);
13300
+ }
13301
+ else if (child != null && isContainerField(child)) {
13302
+ count += countContainersRecursive([child]);
13303
+ }
13304
+ }
13305
+ }
13306
+ }
13307
+ }
13308
+ }
13309
+ return count;
13310
+ }
12891
13311
 
12892
13312
  /**
12893
13313
  * No-op logger for when no logger is provided.
@@ -13413,13 +13833,13 @@ const BUILT_IN_FIELDS = [
13413
13833
  },
13414
13834
  {
13415
13835
  name: 'page',
13416
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs'),
13836
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-Cen4ArED.mjs'),
13417
13837
  mapper: pageFieldMapper,
13418
13838
  valueHandling: 'flatten',
13419
13839
  },
13420
13840
  {
13421
13841
  name: 'text',
13422
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CmkBZk4p.mjs'),
13842
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-cE1EUmYR.mjs'),
13423
13843
  mapper: textFieldMapper,
13424
13844
  valueHandling: 'exclude',
13425
13845
  },
@@ -14185,14 +14605,13 @@ class DynamicTextPipe {
14185
14605
  }
14186
14606
  return of(value || '');
14187
14607
  }
14188
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
14189
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.4", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
14608
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
14609
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.6", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
14190
14610
  }
14191
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicTextPipe, decorators: [{
14611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicTextPipe, decorators: [{
14192
14612
  type: Pipe,
14193
14613
  args: [{
14194
14614
  name: 'dynamicText',
14195
- standalone: true,
14196
14615
  }]
14197
14616
  }] });
14198
14617
 
@@ -14209,12 +14628,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
14209
14628
  * @param injector - The injector to use for afterNextRender scheduling
14210
14629
  */
14211
14630
  function emitComponentInitialized(eventBus, componentType, componentKey, injector) {
14631
+ const logger = injector.get(DynamicFormLogger);
14212
14632
  afterNextRender(() => {
14213
14633
  try {
14214
14634
  eventBus.dispatch(ComponentInitializedEvent, componentType, componentKey);
14215
14635
  }
14216
- catch {
14217
- // Input not available - component may have been destroyed
14636
+ catch (error) {
14637
+ logger.error(`Failed to emit initialization event for ${componentType} '${componentKey}'`, error);
14218
14638
  }
14219
14639
  }, { injector });
14220
14640
  }
@@ -14266,25 +14686,26 @@ class RowFieldComponent {
14266
14686
  destroyRef = inject(DestroyRef);
14267
14687
  fieldRegistry = injectFieldRegistry();
14268
14688
  injector = inject(Injector);
14689
+ environmentInjector = inject(EnvironmentInjector);
14269
14690
  eventBus = inject(EventBus);
14270
14691
  logger = inject(DynamicFormLogger);
14271
14692
  // ─────────────────────────────────────────────────────────────────────────────
14272
14693
  // Inputs
14273
14694
  // ─────────────────────────────────────────────────────────────────────────────
14274
- field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
14275
- key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
14276
- className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
14277
- hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
14695
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
14696
+ key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
14697
+ className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
14698
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
14278
14699
  // ─────────────────────────────────────────────────────────────────────────────
14279
14700
  // Computed Signals
14280
14701
  // ─────────────────────────────────────────────────────────────────────────────
14281
- hostClasses = computed(() => computeContainerHostClasses('row', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
14282
- disabled = computed(() => this.field().disabled || false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
14283
- rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
14702
+ hostClasses = computed(() => computeContainerHostClasses('row', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
14703
+ disabled = computed(() => this.field().disabled || false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
14704
+ rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
14284
14705
  // ─────────────────────────────────────────────────────────────────────────────
14285
14706
  // Field Resolution
14286
14707
  // ─────────────────────────────────────────────────────────────────────────────
14287
- fieldsSource = computed(() => (this.field().fields || []), ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
14708
+ fieldsSource = computed(() => (this.field().fields || []), ...(ngDevMode ? [{ debugName: "fieldsSource" }] : /* istanbul ignore next */ []));
14288
14709
  resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
14289
14710
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
14290
14711
  registry: this.rawFieldRegistry(),
@@ -14309,18 +14730,22 @@ class RowFieldComponent {
14309
14730
  setupEffects() {
14310
14731
  setupContainerInitEffect(this.resolvedFields, this.eventBus, 'row', () => this.field().key, this.injector);
14311
14732
  }
14312
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14313
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
14733
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14734
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
14314
14735
  @for (field of resolvedFields(); track field.key) {
14315
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14736
+ <ng-container
14737
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
14738
+ />
14316
14739
  }
14317
- `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14740
+ `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14318
14741
  }
14319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RowFieldComponent, decorators: [{
14742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: RowFieldComponent, decorators: [{
14320
14743
  type: Component,
14321
14744
  args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
14322
14745
  @for (field of resolvedFields(); track field.key) {
14323
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14746
+ <ng-container
14747
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
14748
+ />
14324
14749
  }
14325
14750
  `, host: {
14326
14751
  '[class]': 'hostClasses()',
@@ -14329,7 +14754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
14329
14754
  '[attr.aria-hidden]': 'hidden() || null',
14330
14755
  '[id]': '`${key()}`',
14331
14756
  '[attr.data-testid]': 'key()',
14332
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"] }]
14757
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"] }]
14333
14758
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
14334
14759
 
14335
14760
  var rowField_component = /*#__PURE__*/Object.freeze({
@@ -14353,6 +14778,7 @@ class GroupFieldComponent {
14353
14778
  fieldRegistry = injectFieldRegistry();
14354
14779
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
14355
14780
  injector = inject(Injector);
14781
+ environmentInjector = inject(EnvironmentInjector);
14356
14782
  eventBus = inject(EventBus);
14357
14783
  logger = inject(DynamicFormLogger);
14358
14784
  // ─────────────────────────────────────────────────────────────────────────────
@@ -14362,24 +14788,26 @@ class GroupFieldComponent {
14362
14788
  // ─────────────────────────────────────────────────────────────────────────────
14363
14789
  // Inputs
14364
14790
  // ─────────────────────────────────────────────────────────────────────────────
14365
- field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
14366
- key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
14367
- className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
14368
- hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
14791
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
14792
+ key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
14793
+ className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
14794
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
14369
14795
  // ─────────────────────────────────────────────────────────────────────────────
14370
14796
  // Computed Signals
14371
14797
  // ─────────────────────────────────────────────────────────────────────────────
14372
- hostClasses = computed(() => computeContainerHostClasses('group', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
14373
- rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
14798
+ hostClasses = computed(() => computeContainerHostClasses('group', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
14799
+ rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
14374
14800
  formSetup = computed(() => {
14375
14801
  const groupField = this.field();
14376
14802
  const registry = this.rawFieldRegistry();
14377
14803
  if (groupField.fields && groupField.fields.length > 0) {
14378
14804
  const flattenedFields = this.fieldProcessors.memoizedFlattenFields(groupField.fields, registry);
14805
+ const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(groupField.fields, registry);
14379
14806
  const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
14380
14807
  const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
14381
14808
  return {
14382
- fields: flattenedFields,
14809
+ fields: flattenedFieldsForRendering,
14810
+ schemaFields: flattenedFields,
14383
14811
  originalFields: groupField.fields,
14384
14812
  defaultValues,
14385
14813
  registry,
@@ -14387,12 +14815,13 @@ class GroupFieldComponent {
14387
14815
  }
14388
14816
  return {
14389
14817
  fields: [],
14818
+ schemaFields: [],
14390
14819
  originalFields: [],
14391
14820
  defaultValues: {},
14392
14821
  registry,
14393
14822
  };
14394
- }, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
14395
- defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
14823
+ }, ...(ngDevMode ? [{ debugName: "formSetup" }] : /* istanbul ignore next */ []));
14824
+ defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
14396
14825
  /**
14397
14826
  * Entity computed from parent value, group key, and defaults.
14398
14827
  * Uses deep equality check to prevent unnecessary updates when
@@ -14411,28 +14840,28 @@ class GroupFieldComponent {
14411
14840
  form = computed(() => {
14412
14841
  return runInInjectionContext(this.injector, () => {
14413
14842
  const setup = this.formSetup();
14414
- if (setup.fields.length > 0) {
14415
- const schema = createSchemaFromFields(setup.fields, setup.registry);
14843
+ if (setup.schemaFields.length > 0) {
14844
+ const schema = createSchemaFromFields(setup.schemaFields, setup.registry);
14416
14845
  return untracked(() => form(this.entity, schema));
14417
14846
  }
14418
14847
  return untracked(() => form(this.entity));
14419
14848
  });
14420
- }, ...(ngDevMode ? [{ debugName: "form" }] : []));
14849
+ }, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
14421
14850
  // ─────────────────────────────────────────────────────────────────────────────
14422
14851
  // Public State Signals
14423
14852
  // ─────────────────────────────────────────────────────────────────────────────
14424
- formValue = computed(() => this.entity(), ...(ngDevMode ? [{ debugName: "formValue" }] : []));
14425
- valid = computed(() => this.form()().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
14426
- invalid = computed(() => this.form()().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
14427
- dirty = computed(() => this.form()().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
14428
- touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
14429
- errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
14430
- disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
14853
+ formValue = computed(() => this.entity(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
14854
+ valid = computed(() => this.form()().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
14855
+ invalid = computed(() => this.form()().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
14856
+ dirty = computed(() => this.form()().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
14857
+ touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
14858
+ errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
14859
+ disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
14431
14860
  nestedFieldTree = computed(() => {
14432
14861
  const parentForm = this.parentFieldSignalContext.form;
14433
14862
  const groupKey = this.field().key;
14434
14863
  return parentForm[groupKey];
14435
- }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
14864
+ }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : /* istanbul ignore next */ []));
14436
14865
  groupFieldSignalContext = (() => {
14437
14866
  const nestedFieldTree = this.nestedFieldTree;
14438
14867
  return {
@@ -14457,7 +14886,7 @@ class GroupFieldComponent {
14457
14886
  // ─────────────────────────────────────────────────────────────────────────────
14458
14887
  // Field Resolution
14459
14888
  // ─────────────────────────────────────────────────────────────────────────────
14460
- fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
14889
+ fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : /* istanbul ignore next */ []));
14461
14890
  resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
14462
14891
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
14463
14892
  registry: this.rawFieldRegistry(),
@@ -14480,25 +14909,28 @@ class GroupFieldComponent {
14480
14909
  // ─────────────────────────────────────────────────────────────────────────────
14481
14910
  setupEffects() {
14482
14911
  setupContainerInitEffect(this.resolvedFields, this.eventBus, 'group', () => this.field().key, this.injector);
14483
- explicitEffect([this.nestedFieldTree], ([tree]) => {
14912
+ explicitEffect([this.nestedFieldTree, this.field], ([tree, field]) => {
14484
14913
  if (!tree) {
14485
- const groupKey = this.field().key;
14486
- this.logger.warn(`Group field "${groupKey}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
14914
+ this.logger.warn(`Group field "${field.key}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
14487
14915
  }
14488
- });
14916
+ }, { defer: true });
14489
14917
  }
14490
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14491
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
14918
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14919
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
14492
14920
  @for (field of resolvedFields(); track field.key) {
14493
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14921
+ <ng-container
14922
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
14923
+ />
14494
14924
  }
14495
14925
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{border:none;margin:0;padding:0;min-width:0;display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14496
14926
  }
14497
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: GroupFieldComponent, decorators: [{
14927
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: GroupFieldComponent, decorators: [{
14498
14928
  type: Component,
14499
14929
  args: [{ selector: 'fieldset[group-field]', imports: [NgComponentOutlet], template: `
14500
14930
  @for (field of resolvedFields(); track field.key) {
14501
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14931
+ <ng-container
14932
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
14933
+ />
14502
14934
  }
14503
14935
  `, host: {
14504
14936
  '[class]': 'hostClasses()',
@@ -14599,7 +15031,7 @@ function createArrayItemInjectorAndInputs(options) {
14599
15031
  return undefined;
14600
15032
  }
14601
15033
  return arrayFieldTree[index];
14602
- }, { ...(ngDevMode ? { debugName: "itemFormAccessor" } : {}), equal: isEqual });
15034
+ }, { ...(ngDevMode ? { debugName: "itemFormAccessor" } : /* istanbul ignore next */ {}), equal: isEqual });
14603
15035
  const injector = createItemInjector({
14604
15036
  itemFormAccessor,
14605
15037
  indexSignal,
@@ -14706,7 +15138,7 @@ function resolveArrayItem(options) {
14706
15138
  const indexSignal = linkedSignal(() => {
14707
15139
  const positionMap = itemPositionMap();
14708
15140
  return positionMap.get(itemId) ?? index;
14709
- }, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
15141
+ }, ...(ngDevMode ? [{ debugName: "indexSignal" }] : /* istanbul ignore next */ []));
14710
15142
  // Resolve all templates in parallel
14711
15143
  const fieldObservables = templates.map((template) => from(loadTypeComponent(template.type)).pipe(map$1((component) => {
14712
15144
  if (destroyRef.destroyed) {
@@ -14734,7 +15166,7 @@ function resolveArrayItem(options) {
14734
15166
  injector,
14735
15167
  inputs,
14736
15168
  };
14737
- }), catchError$1((error) => {
15169
+ }), catchError((error) => {
14738
15170
  if (!destroyRef.destroyed) {
14739
15171
  const logger = parentInjector.get(DynamicFormLogger);
14740
15172
  logger.error(`Failed to load component for field type '${template.type}' at index ${index} ` +
@@ -14826,6 +15258,7 @@ class ArrayFieldComponent {
14826
15258
  fieldRegistry = injectFieldRegistry();
14827
15259
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
14828
15260
  parentInjector = inject(Injector);
15261
+ environmentInjector = inject(EnvironmentInjector);
14829
15262
  eventBus = inject(EventBus);
14830
15263
  logger = inject(DynamicFormLogger);
14831
15264
  templateRegistry = inject(ARRAY_TEMPLATE_REGISTRY);
@@ -14833,10 +15266,10 @@ class ArrayFieldComponent {
14833
15266
  // ─────────────────────────────────────────────────────────────────────────────
14834
15267
  // Inputs
14835
15268
  // ─────────────────────────────────────────────────────────────────────────────
14836
- field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
14837
- key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
14838
- className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
14839
- hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
15269
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
15270
+ key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
15271
+ className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
15272
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
14840
15273
  // ─────────────────────────────────────────────────────────────────────────────
14841
15274
  // Computed Signals
14842
15275
  // ─────────────────────────────────────────────────────────────────────────────
@@ -14844,8 +15277,8 @@ class ArrayFieldComponent {
14844
15277
  const base = 'df-field df-array';
14845
15278
  const custom = this.className();
14846
15279
  return custom ? `${base} ${custom}` : base;
14847
- }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
14848
- rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
15280
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
15281
+ rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
14849
15282
  /**
14850
15283
  * Gets the auto-remove button FieldDef from normalization metadata.
14851
15284
  * Set by simplified array normalization for primitive arrays with remove buttons.
@@ -14855,7 +15288,7 @@ class ArrayFieldComponent {
14855
15288
  autoRemoveButton = computed(() => {
14856
15289
  const metadata = getNormalizedArrayMetadata(this.field());
14857
15290
  return metadata?.autoRemoveButton;
14858
- }, ...(ngDevMode ? [{ debugName: "autoRemoveButton" }] : []));
15291
+ }, ...(ngDevMode ? [{ debugName: "autoRemoveButton" }] : /* istanbul ignore next */ []));
14859
15292
  /**
14860
15293
  * For primitive array items, the key of the value field template (e.g., 'value').
14861
15294
  * Used to wrap the FormControl in the item context so getFieldTree(key) works.
@@ -14881,7 +15314,7 @@ class ArrayFieldComponent {
14881
15314
  return definitions[0].key;
14882
15315
  }
14883
15316
  return undefined;
14884
- }, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : []));
15317
+ }, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
14885
15318
  /**
14886
15319
  * Gets the item templates (field definitions) for the array.
14887
15320
  * Each element can be either:
@@ -14907,7 +15340,7 @@ class ArrayFieldComponent {
14907
15340
  }
14908
15341
  return normalized;
14909
15342
  });
14910
- }, ...(ngDevMode ? [{ debugName: "itemTemplates" }] : []));
15343
+ }, ...(ngDevMode ? [{ debugName: "itemTemplates" }] : /* istanbul ignore next */ []));
14911
15344
  arrayFieldTrees = computed(() => {
14912
15345
  const arrayKey = this.field().key;
14913
15346
  const parentForm = this.parentFieldSignalContext.form;
@@ -14925,12 +15358,12 @@ class ArrayFieldComponent {
14925
15358
  items.push(itemFieldTree ?? null);
14926
15359
  }
14927
15360
  return items;
14928
- }, ...(ngDevMode ? [{ debugName: "arrayFieldTrees" }] : []));
15361
+ }, ...(ngDevMode ? [{ debugName: "arrayFieldTrees" }] : /* istanbul ignore next */ []));
14929
15362
  // ─────────────────────────────────────────────────────────────────────────────
14930
15363
  // State Signals
14931
15364
  // ─────────────────────────────────────────────────────────────────────────────
14932
- resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : []));
14933
- updateVersion = signal(0, ...(ngDevMode ? [{ debugName: "updateVersion" }] : []));
15365
+ resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : /* istanbul ignore next */ []));
15366
+ updateVersion = signal(0, ...(ngDevMode ? [{ debugName: "updateVersion" }] : /* istanbul ignore next */ []));
14934
15367
  /**
14935
15368
  * Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().
14936
15369
  * Used by child linkedSignals to reactively track their position in the array.
@@ -14938,9 +15371,21 @@ class ArrayFieldComponent {
14938
15371
  itemPositionMap = computed(() => {
14939
15372
  const items = this.resolvedItemsSignal();
14940
15373
  return new Map(items.map((item, idx) => [item.id, idx]));
14941
- }, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : []));
15374
+ }, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : /* istanbul ignore next */ []));
14942
15375
  /** Read-only view of resolved items for template consumption. */
14943
- resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : []));
15376
+ resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
15377
+ /**
15378
+ * Whether the array has reached its configured maxLength.
15379
+ * Exposed as a public signal so add-button components can bind [disabled]="atMaxLength()".
15380
+ */
15381
+ atMaxLength = computed(() => {
15382
+ const maxLength = this.field().maxLength;
15383
+ if (maxLength === undefined)
15384
+ return false;
15385
+ const arrayKey = this.field().key;
15386
+ const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
15387
+ return currentArray.length >= maxLength;
15388
+ }, ...(ngDevMode ? [{ debugName: "atMaxLength" }] : /* istanbul ignore next */ []));
14944
15389
  // ─────────────────────────────────────────────────────────────────────────────
14945
15390
  // Auto-remove Cache
14946
15391
  // ─────────────────────────────────────────────────────────────────────────────
@@ -15061,7 +15506,7 @@ class ArrayFieldComponent {
15061
15506
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
15062
15507
  generateItemId: this.generateItemId,
15063
15508
  primitiveFieldKey: isPrimitiveItem ? templates[0].key : undefined,
15064
- }).pipe(catchError$1((error) => {
15509
+ }).pipe(catchError((error) => {
15065
15510
  this.logger.error(`Failed to resolve array item at index ${insertIndex}:`, error);
15066
15511
  return of(undefined);
15067
15512
  })));
@@ -15130,7 +15575,7 @@ class ArrayFieldComponent {
15130
15575
  return;
15131
15576
  }
15132
15577
  // Wrap each item observable to catch individual errors
15133
- const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i]).pipe(catchError$1((error) => {
15578
+ const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i]).pipe(catchError((error) => {
15134
15579
  this.logger.error(`Failed to resolve array item at index ${i}:`, error);
15135
15580
  return of(undefined);
15136
15581
  })));
@@ -15169,7 +15614,7 @@ class ArrayFieldComponent {
15169
15614
  // Wrap each item observable to catch individual errors
15170
15615
  const safeItemObservables = itemsToResolve.map((_, i) => {
15171
15616
  const index = startIndex + i;
15172
- return this.createResolveItemObservable(index).pipe(catchError$1((error) => {
15617
+ return this.createResolveItemObservable(index).pipe(catchError((error) => {
15173
15618
  this.logger.error(`Failed to resolve array item at index ${index}:`, error);
15174
15619
  return of(undefined);
15175
15620
  }));
@@ -15269,15 +15714,27 @@ class ArrayFieldComponent {
15269
15714
  const currentArray = getArrayValue(currentValue, arrayKey);
15270
15715
  if (currentArray.length === 0)
15271
15716
  return;
15272
- // When index is provided, validate it's within bounds. Out-of-bounds is a no-op.
15273
- // When index is undefined, remove the last item (pop behavior).
15274
- const removeIndex = index !== undefined ? index : currentArray.length - 1;
15275
- // Ignore out-of-bounds indices - they should be a no-op, not remove the last item
15276
- if (removeIndex < 0 || removeIndex >= currentArray.length)
15717
+ // When index is undefined or -1, remove the last item.
15718
+ let removeIndex;
15719
+ if (index === undefined || index === -1) {
15720
+ removeIndex = currentArray.length - 1;
15721
+ }
15722
+ else if (index < -1 || index >= currentArray.length) {
15723
+ this.logger.warn(`removeArrayItem index ${index} is out of bounds for array '${arrayKey}' with length ${currentArray.length}. Operation skipped.`);
15277
15724
  return;
15725
+ }
15726
+ else {
15727
+ removeIndex = index;
15728
+ }
15278
15729
  // Update resolvedItems FIRST - remove the item at the specified index.
15279
15730
  // This ensures differential update sees "none" (lengths already match).
15280
15731
  // Remaining items' linkedSignal indices auto-update via itemPositionMap.
15732
+ //
15733
+ // When the item is removed from resolvedItemsSignal, the @for loop removes the DOM element
15734
+ // and NgComponentOutlet destroys the component view, triggering DestroyRef callbacks.
15735
+ // Async validators use Angular's resource API tied to the form-level schema path — when the
15736
+ // array value is updated below, the resource's params re-evaluate. If the removed item's path
15737
+ // no longer exists, params returns undefined, cancelling the pending validation automatically.
15281
15738
  const removedItem = this.resolvedItemsSignal()[removeIndex];
15282
15739
  if (removedItem) {
15283
15740
  this.templateRegistry.delete(removedItem.id);
@@ -15294,8 +15751,8 @@ class ArrayFieldComponent {
15294
15751
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15295
15752
  parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
15296
15753
  }
15297
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15298
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, providers: [
15754
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15755
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, providers: [
15299
15756
  // Each array gets its own template registry to track templates used for dynamically added items
15300
15757
  { provide: ARRAY_TEMPLATE_REGISTRY, useFactory: () => new Map() },
15301
15758
  // Each array gets its own ID generator for SSR hydration compatibility
@@ -15310,13 +15767,15 @@ class ArrayFieldComponent {
15310
15767
  [attr.data-array-item-index]="i"
15311
15768
  >
15312
15769
  @for (field of item.fields; track $index) {
15313
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
15770
+ <ng-container
15771
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
15772
+ />
15314
15773
  }
15315
15774
  </div>
15316
15775
  }
15317
15776
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-array-item-gap, var(--df-grid-row-gap, .5rem));width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}.df-array-item{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-array-item>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15318
15777
  }
15319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ArrayFieldComponent, decorators: [{
15778
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: ArrayFieldComponent, decorators: [{
15320
15779
  type: Component,
15321
15780
  args: [{ selector: 'array-field', imports: [NgComponentOutlet], template: `
15322
15781
  @for (item of resolvedItems(); track item.id; let i = $index) {
@@ -15328,7 +15787,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
15328
15787
  [attr.data-array-item-index]="i"
15329
15788
  >
15330
15789
  @for (field of item.fields; track $index) {
15331
- <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
15790
+ <ng-container
15791
+ *ngComponentOutlet="field.component; injector: field.injector; environmentInjector: environmentInjector; inputs: field.inputs()"
15792
+ />
15332
15793
  }
15333
15794
  </div>
15334
15795
  }
@@ -15491,5 +15952,5 @@ function extractErrorParams(error) {
15491
15952
  * Generated bundle index. Do not edit.
15492
15953
  */
15493
15954
 
15494
- export { baseFieldMapper as $, isContainerField as A, isDisplayOnlyField as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, isGroupField as F, isLeafField as G, isPageField as H, isValueBearingField as I, AppendArrayItemEvent as J, FormClearEvent as K, FormResetEvent as L, InsertArrayItemEvent as M, NoopLogger as N, NextPageEvent as O, PageChangeEvent as P, PopArrayItemEvent as Q, PrependArrayItemEvent as R, PreviousPageEvent as S, RemoveAtIndexEvent as T, ShiftArrayItemEvent as U, SubmitEvent as V, arrayEvent as W, createField as X, field as Y, formConfig as Z, DynamicFormError as _, createFieldResolutionPipe as a, buildBaseInputs as a0, arrayFieldMapper as a1, groupFieldMapper as a2, pageFieldMapper as a3, rowFieldMapper as a4, textFieldMapper as a5, FIELD_REGISTRY as a6, ARRAY_CONTEXT as a7, DEFAULT_PROPS as a8, DEFAULT_VALIDATION_MESSAGES as a9, FIELD_SIGNAL_CONTEXT as aa, FORM_OPTIONS as ab, dynamicTextToObservable as ac, ArrayFieldComponent as ad, GroupFieldComponent as ae, RowFieldComponent as af, applyValidator as ag, applyValidators as ah, getArrayLength as ai, resolveTokens as aj, BUILT_IN_FIELDS as ak, isEqual as al, omit as am, interpolateParams as an, applyMetaToElement as ao, RootFormRegistryService as ap, DynamicTextPipe as b, computeContainerHostClasses as c, derivedFromDeferred as d, DynamicForm as e, withEventFormValue as f, withValueExclusionDefaults as g, hasFormValue as h, injectFieldRegistry as i, isFormStateCondition as j, resolveNextButtonDisabled as k, resolveNonFieldHidden as l, resolveNonFieldDisabled as m, evaluateNonFieldHidden as n, evaluateNonFieldDisabled as o, provideDynamicForm as p, isCheckedField as q, resolveSubmitButtonDisabled as r, setupContainerInitEffect as s, isValueField as t, isRowField as u, validatePageNesting as v, withLoggerConfig as w, isSimplifiedArrayField as x, isPropertyDerivationLogicConfig as y, isArrayField as z };
15495
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bf6Qnz0T.mjs.map
15955
+ export { hasFormValue as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FIELD_REGISTRY as F, GroupFieldComponent as G, applyMetaToElement as H, INITIALIZATION_TIMEOUT_MS as I, applyValidator as J, applyValidators as K, arrayEvent as L, arrayFieldMapper as M, NextPageEvent as N, baseFieldMapper as O, PageChangeEvent as P, buildBaseInputs as Q, RemoveAtIndexEvent as R, ShiftArrayItemEvent as S, createField as T, dynamicTextToObservable as U, evaluateNonFieldDisabled as V, evaluateNonFieldHidden as W, field as X, formConfig as Y, getArrayLength as Z, groupFieldMapper as _, createFieldResolutionPipe as a, interpolateParams as a0, isArrayField as a1, isCheckedField as a2, isContainerField as a3, isDisplayOnlyField as a4, isEqual as a5, isFormStateCondition as a6, isGroupField as a7, isLeafField as a8, isPageField as a9, isRowField as aa, isSimplifiedArrayField as ab, isValueBearingField as ac, isValueField as ad, omit as ae, pageFieldMapper as af, provideDynamicForm as ag, resolveNextButtonDisabled as ah, resolveNonFieldDisabled as ai, resolveNonFieldHidden as aj, resolveSubmitButtonDisabled as ak, resolveTokens as al, rowFieldMapper as am, textFieldMapper as an, withEventFormValue as ao, withLoggerConfig as ap, withPreviousValue as aq, withValueExclusionDefaults as ar, DynamicTextPipe as b, computeContainerHostClasses as c, derivedFromDeferred as d, AppendArrayItemEvent as e, ArrayFieldComponent as f, DEFAULT_PROPS as g, DEFAULT_VALIDATION_MESSAGES as h, injectFieldRegistry as i, DynamicForm as j, DynamicFormError as k, EventDispatcher as l, FIELD_SIGNAL_CONTEXT as m, FORM_OPTIONS as n, FormClearEvent as o, FormResetEvent as p, InsertArrayItemEvent as q, NoopLogger as r, setupContainerInitEffect as s, PopArrayItemEvent as t, PrependArrayItemEvent as u, validatePageNesting as v, PreviousPageEvent as w, RootFormRegistryService as x, RowFieldComponent as y, SubmitEvent as z };
15956
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Sad9wa1d.mjs.map