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