@ng-forge/dynamic-forms 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,8 @@ import { InjectionToken, inject, Injectable, isSignal, untracked, isDevMode, isW
3
3
  import { NgComponentOutlet } from '@angular/common';
4
4
  import { toObservable, toSignal, takeUntilDestroyed, 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, concat, 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.0", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
536
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", 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.0", 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
  */
@@ -1361,10 +1428,18 @@ class FunctionRegistryService {
1361
1428
  return this.propertyDerivationFunctions.get(name) ?? this.derivationFunctions.get(name);
1362
1429
  }
1363
1430
  /**
1364
- * Get all property derivation functions as an object
1431
+ * Get all property derivation functions as an object.
1432
+ *
1433
+ * Merges both `derivationFunctions` and `propertyDerivationFunctions` maps,
1434
+ * with `propertyDerivationFunctions` taking precedence. This allows users
1435
+ * to migrate functions from `propertyDerivations` to `derivations` without
1436
+ * breaking property derivation lookups.
1365
1437
  */
1366
1438
  getPropertyDerivationFunctions() {
1367
- return Object.fromEntries(this.propertyDerivationFunctions);
1439
+ return {
1440
+ ...Object.fromEntries(this.derivationFunctions),
1441
+ ...Object.fromEntries(this.propertyDerivationFunctions),
1442
+ };
1368
1443
  }
1369
1444
  /**
1370
1445
  * Set property derivation functions from a config object.
@@ -1689,10 +1764,10 @@ class FunctionRegistryService {
1689
1764
  this.clearAsyncValidators();
1690
1765
  this.clearHttpValidators();
1691
1766
  }
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 });
1767
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1768
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FunctionRegistryService });
1694
1769
  }
1695
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FunctionRegistryService, decorators: [{
1770
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FunctionRegistryService, decorators: [{
1696
1771
  type: Injectable
1697
1772
  }] });
1698
1773
 
@@ -1731,10 +1806,10 @@ class SchemaRegistryService {
1731
1806
  }
1732
1807
  return schema;
1733
1808
  }
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 });
1809
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1810
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SchemaRegistryService });
1736
1811
  }
1737
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SchemaRegistryService, decorators: [{
1812
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SchemaRegistryService, decorators: [{
1738
1813
  type: Injectable
1739
1814
  }] });
1740
1815
 
@@ -2234,6 +2309,9 @@ class FieldContextRegistryService {
2234
2309
  }
2235
2310
  const resolved = {};
2236
2311
  for (const [key, value] of Object.entries(externalDataRecord)) {
2312
+ if (!isSignal(value)) {
2313
+ throw new DynamicFormError(`externalData["${key}"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);
2314
+ }
2237
2315
  resolved[key] = reactive ? value() : untracked(() => value());
2238
2316
  }
2239
2317
  return resolved;
@@ -2312,10 +2390,10 @@ class FieldContextRegistryService {
2312
2390
  },
2313
2391
  };
2314
2392
  }
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 });
2393
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2394
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FieldContextRegistryService });
2317
2395
  }
2318
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FieldContextRegistryService, decorators: [{
2396
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FieldContextRegistryService, decorators: [{
2319
2397
  type: Injectable
2320
2398
  }] });
2321
2399
 
@@ -2812,6 +2890,9 @@ class Tokenizer {
2812
2890
  value += this.expression[this.position];
2813
2891
  this.position++;
2814
2892
  }
2893
+ if (!/^\d+(\.\d+)?$/.test(value)) {
2894
+ throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);
2895
+ }
2815
2896
  return { type: TokenType.NUMBER, value, position: start };
2816
2897
  }
2817
2898
  readIdentifierOrKeyword() {
@@ -2934,7 +3015,12 @@ class Parser {
2934
3015
  if (this.isAtEnd()) {
2935
3016
  throw new ExpressionParserError('Empty expression', 0, this.expression);
2936
3017
  }
2937
- return this.parseExpression();
3018
+ const result = this.parseExpression();
3019
+ if (!this.isAtEnd()) {
3020
+ const token = this.peek();
3021
+ throw new ExpressionParserError(`Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`, token.position, this.expression);
3022
+ }
3023
+ return result;
2938
3024
  }
2939
3025
  parseExpression() {
2940
3026
  return this.parseLogicalOr();
@@ -5504,6 +5590,9 @@ function createDerivationWarningTracker() {
5504
5590
  * Resolves an `HttpRequestConfig` into an `HttpResourceRequest` by evaluating
5505
5591
  * expression-based query params and (optionally) body values.
5506
5592
  *
5593
+ * Returns `null` if any path parameter resolves to `undefined` or `null`,
5594
+ * signaling that the request should be suppressed (the URL would be malformed).
5595
+ *
5507
5596
  * `debounceMs` is intentionally ignored — it's used by HTTP derivations/conditions (PRs 3-4),
5508
5597
  * not by validators.
5509
5598
  */
@@ -5512,7 +5601,12 @@ function resolveHttpRequest(config, context) {
5512
5601
  if (config.params) {
5513
5602
  for (const [key, expression] of Object.entries(config.params)) {
5514
5603
  const value = ExpressionParser.evaluate(expression, context);
5515
- url = url.replace(`:${key}`, encodeURIComponent(value != null ? String(value) : ''));
5604
+ if (value == null) {
5605
+ context.logger.debug(`HTTP request suppressed: path param '${key}' resolved to ${String(value)} ` +
5606
+ `(expression: '${expression}'). The request will not be sent.`);
5607
+ return null;
5608
+ }
5609
+ url = url.replace(`:${key}`, encodeURIComponent(String(value)));
5516
5610
  }
5517
5611
  }
5518
5612
  if (config.queryParams) {
@@ -5553,6 +5647,20 @@ function resolveHttpRequest(config, context) {
5553
5647
  return request;
5554
5648
  }
5555
5649
 
5650
+ /**
5651
+ * Creates a staleness guard for HTTP/async derivation streams.
5652
+ *
5653
+ * - Call `invalidate()` when tearing down the current generation of streams.
5654
+ * - Pipe HTTP requests through `takeUntil(guard$)` to automatically discard
5655
+ * in-flight responses that arrive after the generation has been invalidated.
5656
+ */
5657
+ function createStreamGuard() {
5658
+ const subject = new Subject();
5659
+ return {
5660
+ invalidate: () => subject.next(),
5661
+ guard$: subject.asObservable().pipe(take$1(1)),
5662
+ };
5663
+ }
5556
5664
  const LOG_PREFIX$1 = 'HTTP Derivation -';
5557
5665
  const DEFAULT_HTTP_DEBOUNCE_MS = 300;
5558
5666
  /**
@@ -5597,7 +5705,7 @@ function createHttpDerivationStream(entry, formValue$, context) {
5597
5705
  if (changedFields.size === 0)
5598
5706
  return false;
5599
5707
  return entry.dependsOn.some((dep) => changedFields.has(dep));
5600
- }), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
5708
+ }), debounceTime(debounceMs), switchMap$1(({ current, changedFields }) => {
5601
5709
  return new Observable((subscriber) => {
5602
5710
  const formAccessor = untracked(() => context.form());
5603
5711
  // Check stopOnUserOverride — skip if the user has manually edited the target field
@@ -5668,15 +5776,28 @@ function createHttpDerivationStream(entry, formValue$, context) {
5668
5776
  return;
5669
5777
  }
5670
5778
  }
5671
- // Resolve the HTTP request (evaluates expressions in queryParams/body)
5779
+ // Returns null when a path param is undefined — suppress the request
5672
5780
  const resolvedRequest = resolveHttpRequest(entry.http, evalContext);
5673
- // Make the HTTP request
5781
+ if (!resolvedRequest) {
5782
+ const derivationLogger = untracked(() => context.derivationLogger());
5783
+ derivationLogger.evaluation({
5784
+ debugName: entry.debugName,
5785
+ fieldKey: entry.fieldKey,
5786
+ result: 'skipped',
5787
+ skipReason: 'condition-false',
5788
+ });
5789
+ subscriber.complete();
5790
+ return;
5791
+ }
5792
+ // Make the HTTP request. takeUntil(guard$) automatically discards responses
5793
+ // that arrive after the config has changed and the guard has been invalidated.
5674
5794
  const method = (resolvedRequest.method ?? 'GET').toUpperCase();
5675
5795
  const httpSub = context.httpClient
5676
5796
  .request(method, resolvedRequest.url, {
5677
5797
  body: resolvedRequest.body,
5678
5798
  headers: resolvedRequest.headers,
5679
5799
  })
5800
+ .pipe(takeUntil(context.guard$))
5680
5801
  .subscribe({
5681
5802
  next: (response) => {
5682
5803
  try {
@@ -5721,12 +5842,14 @@ function createHttpDerivationStream(entry, formValue$, context) {
5721
5842
  subscriber.complete();
5722
5843
  },
5723
5844
  });
5724
- // Cleanup subscription on unsubscribe (switchMap cancellation)
5845
+ // Cleanup: RxJS unsubscription cancels the in-flight HTTP request.
5846
+ // Angular's HttpClient uses an internal AbortController when operating with
5847
+ // the fetch backend, so unsubscribing is sufficient for network cancellation.
5725
5848
  return () => {
5726
5849
  httpSub.unsubscribe();
5727
5850
  };
5728
5851
  });
5729
- }), catchError((error) => {
5852
+ }), catchError$1((error) => {
5730
5853
  // Safety net — should not normally be reached since inner errors are caught
5731
5854
  const message = error instanceof Error ? error.message : String(error);
5732
5855
  context.logger.warn(`${LOG_PREFIX$1} Unexpected stream error for '${entry.fieldKey}': ${message}`);
@@ -5776,7 +5899,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
5776
5899
  if (changedFields.size === 0)
5777
5900
  return false;
5778
5901
  return entry.dependsOn.some((dep) => changedFields.has(dep));
5779
- }), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
5902
+ }), debounceTime(debounceMs), switchMap$1(({ current, changedFields }) => {
5780
5903
  return new Observable((subscriber) => {
5781
5904
  const formAccessor = untracked(() => context.form());
5782
5905
  // Check stopOnUserOverride — skip if the user has manually edited the target field
@@ -5897,7 +6020,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
5897
6020
  asyncSub.unsubscribe();
5898
6021
  };
5899
6022
  });
5900
- }), catchError((error) => {
6023
+ }), catchError$1((error) => {
5901
6024
  // Safety net — should not normally be reached since inner errors are caught
5902
6025
  const message = error instanceof Error ? error.message : String(error);
5903
6026
  context.logger.warn(`${LOG_PREFIX} Unexpected stream error for '${entry.fieldKey}': ${message}`);
@@ -5934,6 +6057,12 @@ class DerivationOrchestrator {
5934
6057
  asyncFunctionSubscriptions = [];
5935
6058
  /** Identity keys of current async entries for smart teardown comparison */
5936
6059
  lastAsyncEntryKeys = null;
6060
+ /**
6061
+ * Guard for detecting stale HTTP responses. Call `invalidate()` on teardown to cancel
6062
+ * in-flight requests from the previous generation. Each HTTP stream subscribes via
6063
+ * `takeUntil(guard$)` — when `invalidate()` fires, all in-flight requests are discarded.
6064
+ */
6065
+ streamGuard = createStreamGuard();
5937
6066
  /**
5938
6067
  * Computed signal containing the collected and validated derivations.
5939
6068
  * Returns null if no derivations are defined.
@@ -6007,7 +6136,7 @@ class DerivationOrchestrator {
6007
6136
  // switchMap: For debounced derivations, it's OK to cancel pending work
6008
6137
  // if new changes come in - we want the latest debounced values.
6009
6138
  // (Unlike onChange which uses exhaustMap to prevent cancellation)
6010
- switchMap(({ changedFields }) => {
6139
+ switchMap$1(({ changedFields }) => {
6011
6140
  const collection = untracked(() => this.derivationCollection());
6012
6141
  const formAccessor = untracked(() => this.config.form());
6013
6142
  if (!collection || !formAccessor)
@@ -6126,6 +6255,7 @@ class DerivationOrchestrator {
6126
6255
  customFunctions: () => this.functionRegistry.getCustomFunctions(),
6127
6256
  externalData: () => this.resolveExternalData(),
6128
6257
  warningTracker: this.warningTracker,
6258
+ guard$: this.streamGuard.guard$,
6129
6259
  };
6130
6260
  const stream = createHttpDerivationStream(entry, formValue$, context).pipe(takeUntilDestroyed(this.destroyRef));
6131
6261
  this.httpSubscriptions.push(stream.subscribe({
@@ -6135,9 +6265,11 @@ class DerivationOrchestrator {
6135
6265
  });
6136
6266
  }
6137
6267
  /**
6138
- * Tears down all active HTTP derivation streams.
6268
+ * Tears down all active HTTP derivation streams. Invalidating the stream guard
6269
+ * cancels any in-flight HTTP responses from the old generation via `takeUntil`.
6139
6270
  */
6140
6271
  teardownHttpStreams() {
6272
+ this.streamGuard.invalidate();
6141
6273
  for (const sub of this.httpSubscriptions) {
6142
6274
  sub.unsubscribe();
6143
6275
  }
@@ -6528,7 +6660,7 @@ function computePropertyValue(entry, evalContext, applicatorContext) {
6528
6660
  if (entry.functionName) {
6529
6661
  const fn = applicatorContext.propertyDerivationFunctions?.[entry.functionName];
6530
6662
  if (!fn) {
6531
- throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.propertyDerivations`);
6663
+ throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.derivations or customFnConfig.propertyDerivations`);
6532
6664
  }
6533
6665
  return fn(evalContext);
6534
6666
  }
@@ -6800,7 +6932,7 @@ class PropertyDerivationOrchestrator {
6800
6932
  .pipe(debounceTime(DEFAULT_DEBOUNCE_MS), startWith(null), pairwise(), filter((pair) => pair[1] !== null), map$1(([previous, current]) => ({
6801
6933
  current,
6802
6934
  changedFields: getChangedKeys(previous, current),
6803
- })), filter(({ changedFields }) => changedFields.size > 0), switchMap(({ changedFields }) => {
6935
+ })), filter(({ changedFields }) => changedFields.size > 0), switchMap$1(({ changedFields }) => {
6804
6936
  const collection = untracked(() => this.propertyDerivationCollection());
6805
6937
  if (!collection)
6806
6938
  return of(null);
@@ -7233,6 +7365,7 @@ class PageOrchestratorComponent {
7233
7365
  eventBus = inject(EventBus);
7234
7366
  fieldContextRegistry = inject(FieldContextRegistryService);
7235
7367
  functionRegistry = inject(FunctionRegistryService);
7368
+ formOptions = inject(FORM_OPTIONS, { optional: true });
7236
7369
  /**
7237
7370
  * Array of page field definitions to render
7238
7371
  */
@@ -7320,13 +7453,11 @@ class PageOrchestratorComponent {
7320
7453
  }
7321
7454
  const currentPage = pages[currentIndex];
7322
7455
  const pageFields = currentPage.fields || [];
7323
- // Check validity of each field on the current page
7456
+ // Collect all leaf field keys, recursively traversing group/row containers
7457
+ const leafKeys = collectLeafFieldKeys(pageFields);
7458
+ // Check validity of each leaf field on the current page
7324
7459
  // 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
7460
+ for (const fieldKey of leafKeys) {
7330
7461
  const field = form[fieldKey];
7331
7462
  if (field && typeof field === 'function') {
7332
7463
  const fieldState = field();
@@ -7351,12 +7482,33 @@ class PageOrchestratorComponent {
7351
7482
  constructor() {
7352
7483
  // Setup event listeners for navigation
7353
7484
  this.setupEventListeners();
7485
+ // B15: Auto-navigate away when current page becomes hidden
7486
+ explicitEffect([this.state, this.visiblePageIndices], ([state, visibleIndices]) => {
7487
+ const currentVisiblePosition = visibleIndices.indexOf(state.currentPageIndex);
7488
+ if (currentVisiblePosition === -1 && visibleIndices.length > 0) {
7489
+ // Current page is hidden — navigate to the nearest visible page
7490
+ const nearest = this.findNearestVisiblePage(state.currentPageIndex, visibleIndices);
7491
+ if (nearest !== -1) {
7492
+ this.navigateToPage(nearest);
7493
+ }
7494
+ }
7495
+ });
7354
7496
  }
7355
7497
  /**
7356
7498
  * Navigate to the next visible page, skipping hidden pages.
7357
7499
  * @returns Navigation result
7358
7500
  */
7359
7501
  navigateToNextPage() {
7502
+ // Guard: do not advance if current page has invalid fields.
7503
+ // Respects disableWhenPageInvalid option (defaults to true).
7504
+ const disableWhenPageInvalid = this.formOptions?.()?.nextButton?.disableWhenPageInvalid ?? true;
7505
+ if (disableWhenPageInvalid && !this.currentPageValid()) {
7506
+ return {
7507
+ success: false,
7508
+ newPageIndex: this.state().currentPageIndex,
7509
+ error: 'Current page has invalid fields',
7510
+ };
7511
+ }
7360
7512
  const currentState = this.state();
7361
7513
  const visibleIndices = this.visiblePageIndices();
7362
7514
  if (currentState.isLastPage) {
@@ -7426,7 +7578,7 @@ class PageOrchestratorComponent {
7426
7578
  navigateToPage(pageIndex) {
7427
7579
  const currentState = this.state();
7428
7580
  const totalPages = currentState.totalPages;
7429
- // Validate page index
7581
+ // Validate page index bounds
7430
7582
  if (pageIndex < 0 || pageIndex >= totalPages) {
7431
7583
  return {
7432
7584
  success: false,
@@ -7434,6 +7586,15 @@ class PageOrchestratorComponent {
7434
7586
  error: `Invalid page index: ${pageIndex}. Valid range is 0 to ${totalPages - 1}`,
7435
7587
  };
7436
7588
  }
7589
+ // Validate target page is visible
7590
+ const visibleIndices = this.visiblePageIndices();
7591
+ if (!visibleIndices.includes(pageIndex)) {
7592
+ return {
7593
+ success: false,
7594
+ newPageIndex: currentState.currentPageIndex,
7595
+ error: `Cannot navigate to hidden page at index ${pageIndex}. Visible pages: [${visibleIndices.join(', ')}]`,
7596
+ };
7597
+ }
7437
7598
  // Check if already on target page
7438
7599
  if (pageIndex === currentState.currentPageIndex) {
7439
7600
  return {
@@ -7451,6 +7612,23 @@ class PageOrchestratorComponent {
7451
7612
  newPageIndex: pageIndex,
7452
7613
  };
7453
7614
  }
7615
+ /**
7616
+ * Finds the nearest visible page index to the given index.
7617
+ * Prefers the forward (higher index) page when equidistant.
7618
+ */
7619
+ findNearestVisiblePage(currentIndex, visibleIndices) {
7620
+ let nearest = -1;
7621
+ let minDistance = Infinity;
7622
+ for (const idx of visibleIndices) {
7623
+ const distance = Math.abs(idx - currentIndex);
7624
+ // Prefer forward (higher index) when tied
7625
+ if (distance < minDistance || (distance === minDistance && idx > nearest)) {
7626
+ minDistance = distance;
7627
+ nearest = idx;
7628
+ }
7629
+ }
7630
+ return nearest;
7631
+ }
7454
7632
  /**
7455
7633
  * Set up event listeners for navigation events
7456
7634
  */
@@ -7507,8 +7685,8 @@ class PageOrchestratorComponent {
7507
7685
  }
7508
7686
  return false;
7509
7687
  }
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: [
7688
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7689
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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
7690
  {
7513
7691
  provide: FIELD_SIGNAL_CONTEXT,
7514
7692
  useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
@@ -7538,9 +7716,9 @@ class PageOrchestratorComponent {
7538
7716
  }
7539
7717
  }
7540
7718
  }
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)]] });
7719
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs').then(m => m.default)]] });
7542
7720
  }
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: [{
7721
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
7544
7722
  type: Component,
7545
7723
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
7546
7724
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -7578,6 +7756,33 @@ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.4", ng
7578
7756
  },
7579
7757
  ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;width:100%}\n"] }]
7580
7758
  }], 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 }] }] } }) });
7759
+ /**
7760
+ * Recursively collects form-tree keys for all value-bearing nodes on a page.
7761
+ *
7762
+ * Row fields are layout-only containers whose children are flattened to the
7763
+ * parent level in the form tree, so we recurse through them transparently.
7764
+ *
7765
+ * Group fields create a nested sub-tree in the form (form['address']['street']).
7766
+ * Their group-level FieldTree node aggregates child validity, so we use the
7767
+ * group's own key — not the individual child keys — to check validity.
7768
+ *
7769
+ * Array fields are also accessed by their own key; the ArrayFieldTree node
7770
+ * covers minLength/maxLength and any item-level validators.
7771
+ */
7772
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any field shape for recursive traversal
7773
+ function collectLeafFieldKeys(fields) {
7774
+ const keys = [];
7775
+ for (const field of fields) {
7776
+ if (isRowField(field)) {
7777
+ // Row children are at the same form-tree level as the row itself
7778
+ keys.push(...collectLeafFieldKeys(field.fields));
7779
+ }
7780
+ else if (field.key) {
7781
+ keys.push(field.key);
7782
+ }
7783
+ }
7784
+ return keys;
7785
+ }
7581
7786
 
7582
7787
  /**
7583
7788
  * Symbol key for storing normalization metadata on array field objects.
@@ -7925,8 +8130,8 @@ function isFunctionHttpValidator(config) {
7925
8130
  /**
7926
8131
  * Evaluates an HTTP response against a `HttpValidationResponseMapping` to produce a validation result.
7927
8132
  *
7928
- * - `validWhen` truthy → `null` (valid, no error)
7929
- * - `validWhen` falsy → `{ kind: errorKind, ...evaluatedErrorParams }`
8133
+ * - `validWhen` === `true` → `null` (valid, no error)
8134
+ * - `validWhen` !== `true` → `{ kind: errorKind, ...evaluatedErrorParams }`
7930
8135
  * - Expression error → logs warning, returns `{ kind: errorKind }` (fail-closed)
7931
8136
  *
7932
8137
  * Expressions are evaluated with scope `{ response }` only.
@@ -7934,8 +8139,11 @@ function isFunctionHttpValidator(config) {
7934
8139
  function evaluateHttpValidationResponse(response, mapping, logger) {
7935
8140
  const scope = { response };
7936
8141
  try {
7937
- const isValid = ExpressionParser.evaluate(mapping.validWhen, scope);
7938
- if (isValid) {
8142
+ const result = ExpressionParser.evaluate(mapping.validWhen, scope);
8143
+ if (typeof result !== 'boolean') {
8144
+ logger.warn(`validWhen expression "${mapping.validWhen}" returned non-boolean value:`, result, 'Expected true or false.');
8145
+ }
8146
+ if (result === true) {
7939
8147
  return null;
7940
8148
  }
7941
8149
  const error = { kind: mapping.errorKind };
@@ -8039,12 +8247,16 @@ class HttpConditionFunctionCacheService {
8039
8247
  /**
8040
8248
  * Extracts a boolean from an HTTP response using an optional expression.
8041
8249
  * When `responseExpression` is provided, evaluates it with `{ response }` scope.
8250
+ * Truthy values are treated as `true` — non-boolean results trigger a warning to encourage explicit boolean expressions.
8042
8251
  * Otherwise, coerces the response to boolean.
8043
8252
  */
8044
8253
  function extractBoolean(response, responseExpression, pendingValue, logger) {
8045
8254
  if (responseExpression) {
8046
8255
  try {
8047
8256
  const result = ExpressionParser.evaluate(responseExpression, { response });
8257
+ if (typeof result !== 'boolean') {
8258
+ logger.warn(`responseExpression "${responseExpression}" returned non-boolean value:`, result, 'Consider returning a boolean explicitly.');
8259
+ }
8048
8260
  return !!result;
8049
8261
  }
8050
8262
  catch (error) {
@@ -8065,7 +8277,10 @@ function extractBoolean(response, responseExpression, pendingValue, logger) {
8065
8277
  * Must be called in injection context (same as `createLogicFunction`).
8066
8278
  */
8067
8279
  function createHttpConditionLogicFunction(condition) {
8068
- const httpClient = inject(HttpClient);
8280
+ const httpClient = inject(HttpClient, { optional: true });
8281
+ if (!httpClient) {
8282
+ throw new DynamicFormError('HttpClient is required for HTTP conditions. Add provideHttpClient() to your providers.');
8283
+ }
8069
8284
  const fieldContextRegistry = inject(FieldContextRegistryService);
8070
8285
  const functionRegistry = inject(FunctionRegistryService);
8071
8286
  const injector = inject(Injector);
@@ -8094,7 +8309,7 @@ function createHttpConditionLogicFunction(condition) {
8094
8309
  // inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
8095
8310
  // clear the active reactive consumer before creating the observable pipeline.
8096
8311
  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) => {
8312
+ const pipeline = toObservable(resolvedRequest, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged((prev, curr) => stableStringify(prev) === stableStringify(curr)), filter((request) => request !== undefined), switchMap$1((request) => {
8098
8313
  const method = request.method ?? 'GET';
8099
8314
  const options = {};
8100
8315
  if (request.body)
@@ -8104,7 +8319,7 @@ function createHttpConditionLogicFunction(condition) {
8104
8319
  return httpClient.request(method, request.url, options).pipe(map$1((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)), tap((value) => {
8105
8320
  const requestKey = stableStringify(request);
8106
8321
  cache.set(requestKey, value, cacheDurationMs);
8107
- }), catchError$1((error) => {
8322
+ }), catchError((error) => {
8108
8323
  logger.warn('HTTP condition request failed:', error);
8109
8324
  return of(pendingValue);
8110
8325
  }));
@@ -8116,8 +8331,11 @@ function createHttpConditionLogicFunction(condition) {
8116
8331
  }
8117
8332
  // Build reactive evaluation context (creates signal dependencies on form values)
8118
8333
  const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
8119
- // Resolve the HTTP request config into a concrete request
8334
+ // Returns null when a path param is undefined — skip the request and return pending value
8120
8335
  const resolved = resolveHttpRequest(condition.http, evaluationContext);
8336
+ if (!resolved) {
8337
+ return signalPair.resultValue();
8338
+ }
8121
8339
  const requestKey = stableStringify(resolved);
8122
8340
  // Check response cache first
8123
8341
  const cachedResult = cache.get(requestKey);
@@ -8189,7 +8407,7 @@ function createAsyncConditionLogicFunction(condition) {
8189
8407
  // inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
8190
8408
  // clear the active reactive consumer before creating the observable pipeline.
8191
8409
  const resultValue = untracked(() => {
8192
- const pipeline = toObservable(trigger, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged(), filter((value) => value !== undefined), switchMap(() => {
8410
+ const pipeline = toObservable(trigger, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged(), filter((value) => value !== undefined), switchMap$1(() => {
8193
8411
  // Look up the async function at call time (fresh reference)
8194
8412
  const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
8195
8413
  if (!asyncFn) {
@@ -8202,7 +8420,7 @@ function createAsyncConditionLogicFunction(condition) {
8202
8420
  // concat ensures pendingValue is emitted immediately when a new trigger
8203
8421
  // arrives, before the async call resolves. This prevents the signal from
8204
8422
  // showing a stale resolved value during re-evaluation.
8205
- return concat(of(pendingValue), from(asyncFn(evaluationContext)).pipe(map$1((result) => !!result), catchError$1((error) => {
8423
+ return concat(of(pendingValue), from(asyncFn(evaluationContext)).pipe(map$1((result) => !!result), catchError((error) => {
8206
8424
  logger.warn('Async Condition - function failed:', error);
8207
8425
  return of(pendingValue);
8208
8426
  })));
@@ -8519,7 +8737,18 @@ function applyValidator(config, fieldPath) {
8519
8737
  break;
8520
8738
  case 'pattern':
8521
8739
  if (config.value instanceof RegExp || typeof config.value === 'string') {
8522
- const regexPattern = typeof config.value === 'string' ? new RegExp(config.value) : config.value;
8740
+ let regexPattern;
8741
+ if (typeof config.value === 'string') {
8742
+ try {
8743
+ regexPattern = new RegExp(config.value);
8744
+ }
8745
+ catch (e) {
8746
+ throw new DynamicFormError(`Invalid regex pattern in validator: '${config.value}' — ${e instanceof Error ? e.message : String(e)}`);
8747
+ }
8748
+ }
8749
+ else {
8750
+ regexPattern = config.value;
8751
+ }
8523
8752
  applyPatternValidator(fieldPath, regexPattern, config.expression);
8524
8753
  }
8525
8754
  break;
@@ -8710,7 +8939,8 @@ function applyDeclarativeHttpValidator(config, fieldPath) {
8710
8939
  // validateHttp's request runs inside Angular's resource API — it NEEDS reactive
8711
8940
  // dependencies to re-trigger when the field value changes.
8712
8941
  const evalCtx = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
8713
- return resolveHttpRequest(config.http, evalCtx);
8942
+ // resolveHttpRequest returns null when a path param is undefined — convert to undefined to skip validation
8943
+ return resolveHttpRequest(config.http, evalCtx) ?? undefined;
8714
8944
  },
8715
8945
  onSuccess: (response) => {
8716
8946
  return evaluateHttpValidationResponse(response, config.responseMapping, logger);
@@ -8909,7 +9139,18 @@ function applyStringValidators(path, config) {
8909
9139
  maxLength(path, config.maxLength);
8910
9140
  }
8911
9141
  if (config.pattern) {
8912
- const regex = typeof config.pattern === 'string' ? new RegExp(config.pattern) : config.pattern;
9142
+ let regex;
9143
+ if (typeof config.pattern === 'string') {
9144
+ try {
9145
+ regex = new RegExp(config.pattern);
9146
+ }
9147
+ catch (e) {
9148
+ throw new DynamicFormError(`Invalid regex pattern: '${config.pattern}' — ${e instanceof Error ? e.message : String(e)}`);
9149
+ }
9150
+ }
9151
+ else {
9152
+ regex = config.pattern;
9153
+ }
8913
9154
  pattern(path, regex);
8914
9155
  }
8915
9156
  }
@@ -9916,7 +10157,7 @@ function tryCreateSchemaEntry(fieldKey, config) {
9916
10157
  function convertBuiltInToCustomValidator(config) {
9917
10158
  const expression = config.expression;
9918
10159
  if (!expression) {
9919
- throw new Error(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
10160
+ throw new DynamicFormError(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
9920
10161
  }
9921
10162
  let validationExpression;
9922
10163
  switch (config.type) {
@@ -9937,13 +10178,13 @@ function convertBuiltInToCustomValidator(config) {
9937
10178
  try {
9938
10179
  new RegExp(expression);
9939
10180
  }
9940
- catch {
9941
- throw new Error(`Invalid regex pattern in cross-field validator: ${expression}`);
10181
+ catch (e) {
10182
+ throw new DynamicFormError(`Invalid regex pattern in cross-field validator: '${expression}' — ${e instanceof Error ? e.message : String(e)}`);
9942
10183
  }
9943
10184
  validationExpression = `fieldValue == null || new RegExp(${JSON.stringify(expression)}).test(fieldValue)`;
9944
10185
  break;
9945
10186
  default:
9946
- throw new Error(`Cannot convert ${config.type} validator to custom validator`);
10187
+ throw new DynamicFormError(`Cannot convert ${config.type} validator to custom validator`);
9947
10188
  }
9948
10189
  return {
9949
10190
  type: 'custom',
@@ -10409,6 +10650,38 @@ function createPropertyOverrideStore() {
10409
10650
  */
10410
10651
  const PROPERTY_OVERRIDE_STORE = new InjectionToken('PROPERTY_OVERRIDE_STORE');
10411
10652
 
10653
+ /**
10654
+ * Known standard field properties where a missing key likely indicates a typo.
10655
+ * Override keys not in this set are assumed to be intentional dynamic properties
10656
+ * and will not trigger dev-mode warnings.
10657
+ */
10658
+ const KNOWN_FIELD_PROPERTY_NAMES = [
10659
+ 'label',
10660
+ 'placeholder',
10661
+ 'disabled',
10662
+ 'readonly',
10663
+ 'required',
10664
+ 'className',
10665
+ 'tabIndex',
10666
+ 'validationMessages',
10667
+ 'options',
10668
+ 'props',
10669
+ 'hint',
10670
+ 'tooltip',
10671
+ 'prefix',
10672
+ 'suffix',
10673
+ 'min',
10674
+ 'max',
10675
+ 'minLength',
10676
+ 'maxLength',
10677
+ 'step',
10678
+ 'pattern',
10679
+ 'rows',
10680
+ 'cols',
10681
+ 'multiple',
10682
+ 'appearance',
10683
+ ];
10684
+ const KNOWN_FIELD_PROPERTIES = new Set(KNOWN_FIELD_PROPERTY_NAMES);
10412
10685
  /**
10413
10686
  * Applies property overrides to a field's input record.
10414
10687
  *
@@ -10425,8 +10698,9 @@ const PROPERTY_OVERRIDE_STORE = new InjectionToken('PROPERTY_OVERRIDE_STORE');
10425
10698
  * Array-valued overrides replace wholesale (no merging).
10426
10699
  * Returns `inputs` unchanged if overrides is empty (no clone).
10427
10700
  *
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.
10701
+ * In dev mode, warns when a known standard field property override key doesn't match
10702
+ * any existing input property, which may indicate a typo in the `targetProperty`
10703
+ * configuration. Dynamic/custom property keys are silently accepted.
10430
10704
  *
10431
10705
  * @param inputs - The current field inputs record
10432
10706
  * @param overrides - Record of property names to override values
@@ -10443,12 +10717,12 @@ function applyPropertyOverrides(inputs, overrides) {
10443
10717
  for (const key of keys) {
10444
10718
  const value = overrides[key];
10445
10719
  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.
10720
+ // Dev-mode check: warn only when a known standard field property is missing from inputs,
10721
+ // which likely indicates a typo. Dynamic/custom keys are intentionally skipped to avoid
10722
+ // false positives for properties added by derivations that aren't in the initial config.
10449
10723
  if (isDevMode()) {
10450
10724
  const topLevelKey = dotIndex === -1 ? key : key.substring(0, dotIndex);
10451
- if (!(topLevelKey in inputs)) {
10725
+ if (KNOWN_FIELD_PROPERTIES.has(topLevelKey) && !(topLevelKey in inputs)) {
10452
10726
  console.warn(`[Dynamic Forms] Property override '${key}' does not match any existing input property. ` +
10453
10727
  'This may indicate a typo in the targetProperty configuration. ' +
10454
10728
  `Available properties: ${Object.keys(inputs).join(', ')}`);
@@ -10647,7 +10921,7 @@ function resolveField(fieldDef, context) {
10647
10921
  injector: context.injector,
10648
10922
  inputs,
10649
10923
  };
10650
- }), catchError$1((error) => {
10924
+ }), catchError((error) => {
10651
10925
  // Only call onError if component is not destroyed to avoid accessing cleaned-up state
10652
10926
  if (!context.destroyRef.destroyed) {
10653
10927
  context.onError?.(fieldDef, error);
@@ -10706,7 +10980,7 @@ function reconcileFields(prev, curr) {
10706
10980
  * Used by container components (page, group) to resolve their child fields.
10707
10981
  */
10708
10982
  function createFieldResolutionPipe(getContext) {
10709
- return pipe(switchMap((fields) => {
10983
+ return pipe(switchMap$1((fields) => {
10710
10984
  if (!fields || fields.length === 0) {
10711
10985
  return of([]);
10712
10986
  }
@@ -11065,6 +11339,131 @@ function injectFieldRegistry() {
11065
11339
  };
11066
11340
  }
11067
11341
 
11342
+ /**
11343
+ * Collects all field keys, types, and validates regex patterns from a field tree
11344
+ * by recursively traversing containers (page, row, group, array).
11345
+ *
11346
+ * @param collectKeys - Whether to add field keys to data.keys for duplicate detection.
11347
+ * Set to false when inside an array container: array item fields share template keys
11348
+ * across items by design (e.g. every item has 'name', 'email'), so they must not
11349
+ * participate in global duplicate-key checking.
11350
+ */
11351
+ function collectFieldData(fields, data, collectKeys = true) {
11352
+ for (const field of fields) {
11353
+ if (collectKeys && field.key) {
11354
+ data.keys.push(field.key);
11355
+ }
11356
+ if (field.type) {
11357
+ data.types.add(field.type);
11358
+ }
11359
+ const fieldWithValidation = field;
11360
+ if ('pattern' in field && typeof fieldWithValidation.pattern === 'string') {
11361
+ validateRegexPattern(fieldWithValidation.pattern, field.key || '<unknown>', data.regexErrors);
11362
+ }
11363
+ if ('validators' in field && fieldWithValidation.validators) {
11364
+ for (const validator of fieldWithValidation.validators) {
11365
+ if (validator.type === 'pattern' && 'value' in validator && typeof validator.value === 'string') {
11366
+ validateRegexPattern(validator.value, field.key || '<unknown>', data.regexErrors);
11367
+ }
11368
+ }
11369
+ }
11370
+ if (hasChildFields(field)) {
11371
+ // Array item fields share template keys across items by design — stop collecting keys
11372
+ // once inside an array. Types and regex patterns are still validated.
11373
+ const childCollectKeys = collectKeys && field.type !== 'array';
11374
+ const children = normalizeFieldsArray(field.fields);
11375
+ for (const child of children) {
11376
+ if (Array.isArray(child)) {
11377
+ collectFieldData(child, data, childCollectKeys);
11378
+ }
11379
+ else {
11380
+ collectFieldData([child], data, childCollectKeys);
11381
+ }
11382
+ }
11383
+ }
11384
+ }
11385
+ }
11386
+ /**
11387
+ * Validates a single regex pattern string, collecting errors rather than throwing.
11388
+ */
11389
+ function validateRegexPattern(pattern, fieldKey, errors) {
11390
+ try {
11391
+ new RegExp(pattern);
11392
+ }
11393
+ catch (e) {
11394
+ errors.push(`Invalid regex pattern in validator for field '${fieldKey}': '${pattern}' — ${e instanceof Error ? e.message : String(e)}`);
11395
+ }
11396
+ }
11397
+ /**
11398
+ * Validates that no duplicate field keys exist in the config.
11399
+ * Throws a DynamicFormError listing all duplicates if any are found.
11400
+ *
11401
+ * @throws {DynamicFormError} When duplicate keys are detected
11402
+ */
11403
+ function validateNoDuplicateKeys(allKeys) {
11404
+ const seen = new Set();
11405
+ const duplicates = new Set();
11406
+ for (const key of allKeys) {
11407
+ if (seen.has(key)) {
11408
+ duplicates.add(key);
11409
+ }
11410
+ seen.add(key);
11411
+ }
11412
+ if (duplicates.size > 0) {
11413
+ const duplicateList = Array.from(duplicates)
11414
+ .map((k) => `'${k}'`)
11415
+ .join(', ');
11416
+ throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Each field key must be unique within a form config.`);
11417
+ }
11418
+ }
11419
+ /**
11420
+ * Validates that every field type referenced in the config exists in the registry.
11421
+ * Logs a warning for unregistered types — unknown fields are skipped during rendering
11422
+ * rather than blocking the whole form (graceful degradation).
11423
+ *
11424
+ * Skips validation when the registry is empty (no UI adapter has been registered),
11425
+ * since the core library tests operate without a field registry.
11426
+ */
11427
+ function validateFieldTypesRegistered(allTypes, registry, logger) {
11428
+ if (registry.size === 0)
11429
+ return;
11430
+ const unregistered = [];
11431
+ for (const type of allTypes) {
11432
+ if (!registry.has(type)) {
11433
+ unregistered.push(type);
11434
+ }
11435
+ }
11436
+ if (unregistered.length > 0) {
11437
+ const typeList = unregistered.map((t) => `'${t}'`).join(', ');
11438
+ logger.warn(`Unknown field type(s): ${typeList}. Register them via provideDynamicForm(...withXxxFields()) or a custom registry entry. These fields will be skipped during rendering.`);
11439
+ }
11440
+ }
11441
+ /**
11442
+ * Validates a form config at bootstrap time, checking for:
11443
+ * - Duplicate field keys (throws — this is always a developer error)
11444
+ * - Unregistered field types (warns — form degrades gracefully, skipping unknown fields)
11445
+ * - Invalid regex patterns in validators (throws — invalid regex will cause runtime errors)
11446
+ *
11447
+ * Should be called once during form setup, before fields are processed.
11448
+ *
11449
+ * @throws {DynamicFormError} When duplicate keys or invalid regex patterns are detected
11450
+ */
11451
+ function validateFormConfig(fields, registry, logger) {
11452
+ const data = {
11453
+ keys: [],
11454
+ types: new Set(),
11455
+ regexErrors: [],
11456
+ };
11457
+ collectFieldData(fields, data);
11458
+ validateNoDuplicateKeys(data.keys);
11459
+ validateFieldTypesRegistered(data.types, registry, logger);
11460
+ if (data.regexErrors.length > 0) {
11461
+ throw new DynamicFormError(data.regexErrors.length === 1
11462
+ ? data.regexErrors[0]
11463
+ : `Invalid regex pattern(s) found:\n${data.regexErrors.map((e) => ` - ${e}`).join('\n')}`);
11464
+ }
11465
+ }
11466
+
11068
11467
  /**
11069
11468
  * Injection token for global value exclusion defaults.
11070
11469
  *
@@ -11415,7 +11814,7 @@ class FormStateMachine {
11415
11814
  /** Sets up sequential action processing via `concatMap`. */
11416
11815
  setupActionProcessing() {
11417
11816
  this.actions$
11418
- .pipe(concatMap((action) => this.processAction(action).pipe(catchError$1((error) => {
11817
+ .pipe(concatMap((action) => this.processAction(action).pipe(catchError((error) => {
11419
11818
  this.config.logger.error(`Action '${action.type}' failed:`, error);
11420
11819
  this.recoverFromError(error, action);
11421
11820
  return EMPTY;
@@ -11498,6 +11897,12 @@ class FormStateMachine {
11498
11897
  sideEffects: [{ type: Effect.CreateForm }],
11499
11898
  };
11500
11899
  }
11900
+ // Guard: reject config changes during active submission to prevent races
11901
+ // between the in-flight action Promise and form teardown/rebuild.
11902
+ if (this.config.isSubmitting?.()) {
11903
+ this.config.logger.warn('Config changed during active submission; ignoring. Apply config changes after submission completes.');
11904
+ return { state, sideEffects: [] };
11905
+ }
11501
11906
  if (isReadyState(state)) {
11502
11907
  return {
11503
11908
  state: createTransitioningState(Phase.Teardown, state.config, config, state.formSetup),
@@ -11881,38 +12286,35 @@ class FormStateManager {
11881
12286
  debugName: 'FormStateManager.entity',
11882
12287
  equal: isEqual,
11883
12288
  });
12289
+ /**
12290
+ * Schema derived from the current form config and field setup.
12291
+ * Separated from `form` so schema construction is memoized independently.
12292
+ * Requires `runInInjectionContext` because `createSchemaFromFields` calls `inject()` internally.
12293
+ */
12294
+ formSchema = computed(() => runInInjectionContext(this.injector, () => {
12295
+ const setup = this.formSetup();
12296
+ const config = this.activeConfig();
12297
+ if (!config)
12298
+ return undefined;
12299
+ if (setup.schemaFields?.length) {
12300
+ const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields);
12301
+ return createSchemaFromFields(setup.schemaFields, setup.registry, {
12302
+ crossFieldValidators: crossFieldCollection.validators,
12303
+ formLevelSchema: config.schema,
12304
+ });
12305
+ }
12306
+ if (config.schema) {
12307
+ return createFormLevelSchema(config.schema);
12308
+ }
12309
+ return undefined;
12310
+ }), ...(ngDevMode ? [{ debugName: "formSchema" }] : []));
11884
12311
  /**
11885
12312
  * The Angular Signal Form instance.
11886
12313
  */
11887
12314
  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
- });
12315
+ const schema = this.formSchema();
12316
+ const injector = this.injector;
12317
+ return untracked(() => (schema ? form(this.entity, schema, { injector }) : form(this.entity, { injector })));
11916
12318
  }, ...(ngDevMode ? [{ debugName: "form" }] : []));
11917
12319
  /** Whether resolvedFields has caught up with fieldsSource (set in constructor). */
11918
12320
  isFieldPipelineSettled;
@@ -12057,7 +12459,7 @@ class FormStateManager {
12057
12459
  // Constructor
12058
12460
  // ─────────────────────────────────────────────────────────────────────────────
12059
12461
  constructor() {
12060
- this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
12462
+ this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap$1((fields) => {
12061
12463
  if (!fields || fields.length === 0) {
12062
12464
  return of([]);
12063
12465
  }
@@ -12124,7 +12526,10 @@ class FormStateManager {
12124
12526
  return false;
12125
12527
  }
12126
12528
  return true;
12127
- }), switchMap(() => {
12529
+ }),
12530
+ // switchMap is intentional here: mapping is synchronous (reads signals, no async Promise).
12531
+ // The exhaustMap concern in createSubmissionHandler does not apply to this path.
12532
+ switchMap$1(() => {
12128
12533
  const config = this.activeConfig();
12129
12534
  const submissionConfig = config?.submission;
12130
12535
  if (submissionConfig?.action) {
@@ -12148,6 +12553,7 @@ class FormStateManager {
12148
12553
  return this.createFormSetupFromConfig(config.fields ?? [], detectFormMode(config.fields ?? []).mode, this.rawFieldRegistry());
12149
12554
  },
12150
12555
  captureValue: () => this.formValue(),
12556
+ isSubmitting: this.submitting,
12151
12557
  isFieldPipelineSettled: () => this.isFieldPipelineSettled(),
12152
12558
  restoreValue: (values, validKeys) => {
12153
12559
  const filtered = {};
@@ -12284,6 +12690,9 @@ class FormStateManager {
12284
12690
  }
12285
12691
  createFormSetupFromConfig(fields, mode, registry) {
12286
12692
  const normalizedFields = normalizeSimplifiedArrays(fields);
12693
+ // Validate after normalization so simplified array templates are already expanded
12694
+ // into full ArrayField.fields and are reachable during traversal.
12695
+ validateFormConfig(normalizedFields, registry, this.logger);
12287
12696
  const flattenedFields = this.fieldProcessors.memoizedFlattenFields(normalizedFields, registry);
12288
12697
  const flattenedFieldsForRendering = this.memoizedFlattenFieldsForRendering(normalizedFields, registry);
12289
12698
  const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
@@ -12313,10 +12722,10 @@ class FormStateManager {
12313
12722
  ngOnDestroy() {
12314
12723
  this.machine.dispatch({ type: Action.Destroy });
12315
12724
  }
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 });
12725
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
12726
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FormStateManager });
12318
12727
  }
12319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FormStateManager, decorators: [{
12728
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FormStateManager, decorators: [{
12320
12729
  type: Injectable
12321
12730
  }], ctorParameters: () => [] });
12322
12731
 
@@ -12496,25 +12905,6 @@ class ConsoleLogger {
12496
12905
  }
12497
12906
  }
12498
12907
 
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
12908
  /**
12519
12909
  * Injection token for derivation logging configuration.
12520
12910
  *
@@ -12662,6 +13052,87 @@ function provideDynamicFormDI() {
12662
13052
  ];
12663
13053
  }
12664
13054
 
13055
+ /**
13056
+ * Injectable service for dispatching events into a DynamicForm from outside the form.
13057
+ *
13058
+ * ## When to use
13059
+ *
13060
+ * Use `EventDispatcher` when you need to drive form behaviour **from the host component** —
13061
+ * for example, appending array items in response to a field value change, triggering a form
13062
+ * reset from a toolbar button, or reacting to external application state.
13063
+ *
13064
+ * This is the recommended alternative to accessing the form's internals via `viewChild`.
13065
+ *
13066
+ * ## Setup
13067
+ *
13068
+ * Provide `EventDispatcher` at the **host component** level (not root). DynamicForm
13069
+ * automatically detects it and connects its internal event bus to the dispatcher.
13070
+ *
13071
+ * ```typescript
13072
+ * @Component({
13073
+ * providers: [EventDispatcher],
13074
+ * template: `<form [dynamic-form]="config" [(value)]="formValue"></form>`
13075
+ * })
13076
+ * export class MyComponent {
13077
+ * readonly dispatcher = inject(EventDispatcher);
13078
+ * readonly formValue = signal<Record<string, unknown>>({});
13079
+ *
13080
+ * constructor() {
13081
+ * effect(() => {
13082
+ * const category = this.formValue()?.['category'] as string | undefined;
13083
+ * if (category) {
13084
+ * this.dispatcher.dispatch(
13085
+ * arrayEvent('tasks').append([{ key: 'name', type: 'input', label: 'Task', value: category }])
13086
+ * );
13087
+ * }
13088
+ * });
13089
+ * }
13090
+ * }
13091
+ * ```
13092
+ *
13093
+ * ## Multi-form note
13094
+ *
13095
+ * If multiple `DynamicForm` instances exist under the same provider scope, **all forms**
13096
+ * will receive dispatched events. To target a specific form, scope the provider to a
13097
+ * wrapper component that contains only that form.
13098
+ *
13099
+ * ## What events can be dispatched
13100
+ *
13101
+ * Any `FormEvent` instance is accepted — array events, reset/clear events, custom events, etc.
13102
+ * The `arrayEvent()` builder is the recommended way to construct array manipulation events:
13103
+ *
13104
+ * ```typescript
13105
+ * dispatcher.dispatch(arrayEvent('tasks').append(template));
13106
+ * dispatcher.dispatch(arrayEvent('tasks').removeAt(0));
13107
+ * dispatcher.dispatch(new FormResetEvent());
13108
+ * ```
13109
+ */
13110
+ class EventDispatcher {
13111
+ bus;
13112
+ /**
13113
+ * Dispatches a form event into the connected DynamicForm's event bus.
13114
+ * No-op if no form is currently connected.
13115
+ *
13116
+ * @param event - A FormEvent instance. Use the `arrayEvent()` builder for array operations.
13117
+ */
13118
+ dispatch(event) {
13119
+ this.bus?.emitInstance(event);
13120
+ }
13121
+ /** @internal - Called by DynamicForm on init */
13122
+ connect(bus) {
13123
+ this.bus = bus;
13124
+ }
13125
+ /** @internal - Called by DynamicForm on destroy */
13126
+ disconnect() {
13127
+ this.bus = undefined;
13128
+ }
13129
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
13130
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EventDispatcher });
13131
+ }
13132
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EventDispatcher, decorators: [{
13133
+ type: Injectable
13134
+ }] });
13135
+
12665
13136
  /**
12666
13137
  * Dynamic form component — renders a form based on configuration.
12667
13138
  * Delegates state management to `FormStateManager`.
@@ -12688,20 +13159,10 @@ class DynamicForm {
12688
13159
  injector = inject(Injector);
12689
13160
  eventBus = inject(EventBus);
12690
13161
  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));
13162
+ dispatcher = inject(EventDispatcher, { optional: true });
13163
+ /** State manager that owns all form state. Initialized via connectDeps() to guarantee
13164
+ * FORM_STATE_DEPS is populated before FormStateManager is injected. */
13165
+ stateManager = this.connectDeps();
12705
13166
  // ─────────────────────────────────────────────────────────────────────────────
12706
13167
  // Private State
12707
13168
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12750,9 +13211,16 @@ class DynamicForm {
12750
13211
  // ─────────────────────────────────────────────────────────────────────────────
12751
13212
  // Computed Signals - Internal
12752
13213
  // ─────────────────────────────────────────────────────────────────────────────
13214
+ /**
13215
+ * Recursively counts container components that will emit ComponentInitializedEvent.
13216
+ * Includes the dynamic-form component itself (+1).
13217
+ *
13218
+ * Recurses into all container children, including those nested inside array
13219
+ * item templates, to avoid a premature (initialized) emission.
13220
+ */
12753
13221
  totalComponentsCount = computed(() => {
12754
13222
  const fields = this.stateManager.formSetup()?.fields ?? [];
12755
- return fields.filter(isContainerField).length + 1;
13223
+ return countContainersRecursive(fields) + 1;
12756
13224
  }, ...(ngDevMode ? [{ debugName: "totalComponentsCount" }] : []));
12757
13225
  // ─────────────────────────────────────────────────────────────────────────────
12758
13226
  // Initialization
@@ -12799,14 +13267,16 @@ class DynamicForm {
12799
13267
  // Constructor
12800
13268
  // ─────────────────────────────────────────────────────────────────────────────
12801
13269
  constructor() {
13270
+ this.dispatcher?.connect(this.eventBus);
13271
+ this.destroyRef.onDestroy(() => this.dispatcher?.disconnect());
13272
+ // Inject orchestrators eagerly so derivation streams are set up before the first
13273
+ // render cycle. Previously afterNextRender() deferred injection, causing
13274
+ // (initialized) to fire before derivations ran. The circular dependency that
13275
+ // originally motivated the deferral is resolved via dynamic-form-di.ts.
13276
+ this.injector.get(DERIVATION_ORCHESTRATOR);
13277
+ this.injector.get(PROPERTY_DERIVATION_ORCHESTRATOR);
12802
13278
  this.setupEffects();
12803
13279
  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
13280
  }
12811
13281
  /**
12812
13282
  * Handles native form submission triggered by:
@@ -12842,19 +13312,34 @@ class DynamicForm {
12842
13312
  }
12843
13313
  });
12844
13314
  }
13315
+ /**
13316
+ * Populates FORM_STATE_DEPS with this component's input signals, then injects
13317
+ * FormStateManager. Must be called as a field initializer (after the input signals
13318
+ * are declared) so that FormStateManager reads populated deps when it is constructed.
13319
+ * inject() is valid here because field initializers run inside the injection context.
13320
+ */
13321
+ connectDeps() {
13322
+ const deps = inject(FORM_STATE_DEPS);
13323
+ deps.config = this.config;
13324
+ deps.formOptions = this.formOptions;
13325
+ deps.value = this.value;
13326
+ return inject((FormStateManager));
13327
+ }
12845
13328
  setupEventHandlers() {
12846
13329
  createSubmissionHandler({
12847
13330
  eventBus: this.eventBus,
12848
13331
  configSignal: this.config,
12849
13332
  formSignal: this.form,
13333
+ validSignal: this.valid,
13334
+ logger: this.logger,
12850
13335
  })
12851
13336
  .pipe(takeUntilDestroyed(this.destroyRef))
12852
13337
  .subscribe({
12853
13338
  error: (err) => this.logger.error('Submission handler error', err),
12854
13339
  });
12855
13340
  }
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: `
13341
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
13342
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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
13343
  @if (shouldRender()) {
12859
13344
  @if (formModeDetection().mode === 'paged') {
12860
13345
  <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
@@ -12866,7 +13351,7 @@ class DynamicForm {
12866
13351
  }
12867
13352
  `, 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
13353
  }
12869
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicForm, decorators: [{
13354
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicForm, decorators: [{
12870
13355
  type: Component,
12871
13356
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
12872
13357
  @if (shouldRender()) {
@@ -12888,6 +13373,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
12888
13373
  '(submit)': 'onNativeSubmit($event)',
12889
13374
  }, 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
13375
  }], 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"] }] } });
13376
+ /**
13377
+ * Recursively counts container fields (page, row, group, array) in a field tree.
13378
+ * Descends into container children including array item templates to ensure
13379
+ * nested containers are counted for accurate initialization tracking.
13380
+ */
13381
+ function countContainersRecursive(fields) {
13382
+ let count = 0;
13383
+ for (const field of fields) {
13384
+ if (isContainerField(field)) {
13385
+ count += 1;
13386
+ if (hasChildFields(field)) {
13387
+ const children = field.fields;
13388
+ if (Array.isArray(children)) {
13389
+ for (const child of children) {
13390
+ if (Array.isArray(child)) {
13391
+ // Array item template: FieldDef[] (object items)
13392
+ count += countContainersRecursive(child);
13393
+ }
13394
+ else if (child != null && isContainerField(child)) {
13395
+ count += countContainersRecursive([child]);
13396
+ }
13397
+ }
13398
+ }
13399
+ }
13400
+ }
13401
+ }
13402
+ return count;
13403
+ }
12891
13404
 
12892
13405
  /**
12893
13406
  * No-op logger for when no logger is provided.
@@ -13413,13 +13926,13 @@ const BUILT_IN_FIELDS = [
13413
13926
  },
13414
13927
  {
13415
13928
  name: 'page',
13416
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs'),
13929
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs'),
13417
13930
  mapper: pageFieldMapper,
13418
13931
  valueHandling: 'flatten',
13419
13932
  },
13420
13933
  {
13421
13934
  name: 'text',
13422
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CmkBZk4p.mjs'),
13935
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-Ael-STTb.mjs'),
13423
13936
  mapper: textFieldMapper,
13424
13937
  valueHandling: 'exclude',
13425
13938
  },
@@ -14185,10 +14698,10 @@ class DynamicTextPipe {
14185
14698
  }
14186
14699
  return of(value || '');
14187
14700
  }
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" });
14701
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
14702
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
14190
14703
  }
14191
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: DynamicTextPipe, decorators: [{
14704
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicTextPipe, decorators: [{
14192
14705
  type: Pipe,
14193
14706
  args: [{
14194
14707
  name: 'dynamicText',
@@ -14209,12 +14722,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
14209
14722
  * @param injector - The injector to use for afterNextRender scheduling
14210
14723
  */
14211
14724
  function emitComponentInitialized(eventBus, componentType, componentKey, injector) {
14725
+ const logger = injector.get(DynamicFormLogger);
14212
14726
  afterNextRender(() => {
14213
14727
  try {
14214
14728
  eventBus.dispatch(ComponentInitializedEvent, componentType, componentKey);
14215
14729
  }
14216
- catch {
14217
- // Input not available - component may have been destroyed
14730
+ catch (error) {
14731
+ logger.error(`Failed to emit initialization event for ${componentType} '${componentKey}'`, error);
14218
14732
  }
14219
14733
  }, { injector });
14220
14734
  }
@@ -14309,14 +14823,14 @@ class RowFieldComponent {
14309
14823
  setupEffects() {
14310
14824
  setupContainerInitEffect(this.resolvedFields, this.eventBus, 'row', () => this.field().key, this.injector);
14311
14825
  }
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: `
14826
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14827
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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
14828
  @for (field of resolvedFields(); track field.key) {
14315
14829
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14316
14830
  }
14317
14831
  `, 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 });
14318
14832
  }
14319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RowFieldComponent, decorators: [{
14833
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RowFieldComponent, decorators: [{
14320
14834
  type: Component,
14321
14835
  args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
14322
14836
  @for (field of resolvedFields(); track field.key) {
@@ -14480,21 +14994,20 @@ class GroupFieldComponent {
14480
14994
  // ─────────────────────────────────────────────────────────────────────────────
14481
14995
  setupEffects() {
14482
14996
  setupContainerInitEffect(this.resolvedFields, this.eventBus, 'group', () => this.field().key, this.injector);
14483
- explicitEffect([this.nestedFieldTree], ([tree]) => {
14997
+ explicitEffect([this.nestedFieldTree, this.field], ([tree, field]) => {
14484
14998
  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.`);
14999
+ this.logger.warn(`Group field "${field.key}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
14487
15000
  }
14488
- });
15001
+ }, { defer: true });
14489
15002
  }
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: `
15003
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15004
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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
15005
  @for (field of resolvedFields(); track field.key) {
14493
15006
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
14494
15007
  }
14495
15008
  `, 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
15009
  }
14497
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: GroupFieldComponent, decorators: [{
15010
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GroupFieldComponent, decorators: [{
14498
15011
  type: Component,
14499
15012
  args: [{ selector: 'fieldset[group-field]', imports: [NgComponentOutlet], template: `
14500
15013
  @for (field of resolvedFields(); track field.key) {
@@ -14734,7 +15247,7 @@ function resolveArrayItem(options) {
14734
15247
  injector,
14735
15248
  inputs,
14736
15249
  };
14737
- }), catchError$1((error) => {
15250
+ }), catchError((error) => {
14738
15251
  if (!destroyRef.destroyed) {
14739
15252
  const logger = parentInjector.get(DynamicFormLogger);
14740
15253
  logger.error(`Failed to load component for field type '${template.type}' at index ${index} ` +
@@ -14941,6 +15454,18 @@ class ArrayFieldComponent {
14941
15454
  }, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : []));
14942
15455
  /** Read-only view of resolved items for template consumption. */
14943
15456
  resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : []));
15457
+ /**
15458
+ * Whether the array has reached its configured maxLength.
15459
+ * Exposed as a public signal so add-button components can bind [disabled]="atMaxLength()".
15460
+ */
15461
+ atMaxLength = computed(() => {
15462
+ const maxLength = this.field().maxLength;
15463
+ if (maxLength === undefined)
15464
+ return false;
15465
+ const arrayKey = this.field().key;
15466
+ const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
15467
+ return currentArray.length >= maxLength;
15468
+ }, ...(ngDevMode ? [{ debugName: "atMaxLength" }] : []));
14944
15469
  // ─────────────────────────────────────────────────────────────────────────────
14945
15470
  // Auto-remove Cache
14946
15471
  // ─────────────────────────────────────────────────────────────────────────────
@@ -15061,7 +15586,7 @@ class ArrayFieldComponent {
15061
15586
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
15062
15587
  generateItemId: this.generateItemId,
15063
15588
  primitiveFieldKey: isPrimitiveItem ? templates[0].key : undefined,
15064
- }).pipe(catchError$1((error) => {
15589
+ }).pipe(catchError((error) => {
15065
15590
  this.logger.error(`Failed to resolve array item at index ${insertIndex}:`, error);
15066
15591
  return of(undefined);
15067
15592
  })));
@@ -15130,7 +15655,7 @@ class ArrayFieldComponent {
15130
15655
  return;
15131
15656
  }
15132
15657
  // Wrap each item observable to catch individual errors
15133
- const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i]).pipe(catchError$1((error) => {
15658
+ const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i]).pipe(catchError((error) => {
15134
15659
  this.logger.error(`Failed to resolve array item at index ${i}:`, error);
15135
15660
  return of(undefined);
15136
15661
  })));
@@ -15169,7 +15694,7 @@ class ArrayFieldComponent {
15169
15694
  // Wrap each item observable to catch individual errors
15170
15695
  const safeItemObservables = itemsToResolve.map((_, i) => {
15171
15696
  const index = startIndex + i;
15172
- return this.createResolveItemObservable(index).pipe(catchError$1((error) => {
15697
+ return this.createResolveItemObservable(index).pipe(catchError((error) => {
15173
15698
  this.logger.error(`Failed to resolve array item at index ${index}:`, error);
15174
15699
  return of(undefined);
15175
15700
  }));
@@ -15269,15 +15794,27 @@ class ArrayFieldComponent {
15269
15794
  const currentArray = getArrayValue(currentValue, arrayKey);
15270
15795
  if (currentArray.length === 0)
15271
15796
  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)
15797
+ // When index is undefined or -1, remove the last item.
15798
+ let removeIndex;
15799
+ if (index === undefined || index === -1) {
15800
+ removeIndex = currentArray.length - 1;
15801
+ }
15802
+ else if (index < -1 || index >= currentArray.length) {
15803
+ this.logger.warn(`removeArrayItem index ${index} is out of bounds for array '${arrayKey}' with length ${currentArray.length}. Operation skipped.`);
15277
15804
  return;
15805
+ }
15806
+ else {
15807
+ removeIndex = index;
15808
+ }
15278
15809
  // Update resolvedItems FIRST - remove the item at the specified index.
15279
15810
  // This ensures differential update sees "none" (lengths already match).
15280
15811
  // Remaining items' linkedSignal indices auto-update via itemPositionMap.
15812
+ //
15813
+ // When the item is removed from resolvedItemsSignal, the @for loop removes the DOM element
15814
+ // and NgComponentOutlet destroys the component view, triggering DestroyRef callbacks.
15815
+ // Async validators use Angular's resource API tied to the form-level schema path — when the
15816
+ // array value is updated below, the resource's params re-evaluate. If the removed item's path
15817
+ // no longer exists, params returns undefined, cancelling the pending validation automatically.
15281
15818
  const removedItem = this.resolvedItemsSignal()[removeIndex];
15282
15819
  if (removedItem) {
15283
15820
  this.templateRegistry.delete(removedItem.id);
@@ -15294,8 +15831,8 @@ class ArrayFieldComponent {
15294
15831
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15295
15832
  parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
15296
15833
  }
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: [
15834
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15835
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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
15836
  // Each array gets its own template registry to track templates used for dynamically added items
15300
15837
  { provide: ARRAY_TEMPLATE_REGISTRY, useFactory: () => new Map() },
15301
15838
  // Each array gets its own ID generator for SSR hydration compatibility
@@ -15316,7 +15853,7 @@ class ArrayFieldComponent {
15316
15853
  }
15317
15854
  `, 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
15855
  }
15319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ArrayFieldComponent, decorators: [{
15856
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ArrayFieldComponent, decorators: [{
15320
15857
  type: Component,
15321
15858
  args: [{ selector: 'array-field', imports: [NgComponentOutlet], template: `
15322
15859
  @for (item of resolvedItems(); track item.id; let i = $index) {
@@ -15491,5 +16028,5 @@ function extractErrorParams(error) {
15491
16028
  * Generated bundle index. Do not edit.
15492
16029
  */
15493
16030
 
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
16031
+ 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, isPropertyDerivationLogicConfig as aa, isRowField as ab, isSimplifiedArrayField as ac, isValueBearingField as ad, isValueField as ae, omit as af, pageFieldMapper as ag, provideDynamicForm as ah, resolveNextButtonDisabled as ai, resolveNonFieldDisabled as aj, resolveNonFieldHidden as ak, resolveSubmitButtonDisabled as al, resolveTokens as am, rowFieldMapper as an, textFieldMapper as ao, withEventFormValue as ap, withLoggerConfig 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 };
16032
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DXXp5M0q.mjs.map