@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.
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bf6Qnz0T.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-DXXp5M0q.mjs} +778 -241
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DXXp5M0q.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs → ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-_mRHaw7x.mjs.map → ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CmkBZk4p.mjs → ng-forge-dynamic-forms-text-field.component-Ael-STTb.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CmkBZk4p.mjs.map → ng-forge-dynamic-forms-text-field.component-Ael-STTb.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +4 -4
- package/types/ng-forge-dynamic-forms.d.ts +230 -120
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bf6Qnz0T.mjs.map +0 -1
|
@@ -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,
|
|
7
|
-
import { scan, map, filter as filter$1, take, switchMap as
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
408
|
+
* Event bus for form-wide event communication between field components.
|
|
368
409
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
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
|
-
*
|
|
376
|
-
*
|
|
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
|
-
*
|
|
392
|
-
*
|
|
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
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
*
|
|
414
|
-
*
|
|
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
|
-
|
|
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.
|
|
468
|
+
this.safeEmit(attachFormValue(event, formValue));
|
|
434
469
|
return;
|
|
435
470
|
}
|
|
436
471
|
}
|
|
437
|
-
this.
|
|
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.
|
|
489
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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.
|
|
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
|
-
*
|
|
572
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
656
|
+
tracking$ = eventBus.on('component-initialized').pipe(filter$1((event) => event.componentType === 'dynamic-form' && event.componentId === componentId), map(() => true), take(1));
|
|
596
657
|
}
|
|
597
|
-
|
|
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
|
|
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.
|
|
1693
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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.
|
|
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.
|
|
1735
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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.
|
|
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.
|
|
2316
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5779
|
+
// Returns null when a path param is undefined — suppress the request
|
|
5672
5780
|
const resolvedRequest = resolveHttpRequest(entry.http, evalContext);
|
|
5673
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
7511
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
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-
|
|
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.
|
|
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`
|
|
7929
|
-
* - `validWhen`
|
|
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
|
|
7938
|
-
if (
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10429
|
-
* which may indicate a typo in the `targetProperty`
|
|
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
|
|
10447
|
-
//
|
|
10448
|
-
// for properties
|
|
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
|
|
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
|
|
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
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
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
|
-
}),
|
|
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.
|
|
12317
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
12693
|
-
*
|
|
12694
|
-
|
|
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
|
|
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.
|
|
12857
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
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.
|
|
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-
|
|
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-
|
|
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.
|
|
14189
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
14313
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
14491
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
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.
|
|
15298
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.
|
|
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.
|
|
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 {
|
|
15495
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
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
|