@ng-forge/dynamic-forms 0.9.0-next.11 → 0.9.0-next.12
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-array-field.component-DJ-LiLm_.mjs → ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs} +154 -69
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs → ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs} +9 -5
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs.map → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs.map → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs → ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs} +9 -5
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs} +396 -248
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs → ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs} +9 -5
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs.map → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms.d.ts +488 -468
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, reflectComponentType, Injector, inject, EnvironmentInjector, DestroyRef, computed, input, ViewContainerRef,
|
|
2
|
+
import { InjectionToken, reflectComponentType, Injector, inject, EnvironmentInjector, DestroyRef, computed, signal, input, ViewContainerRef, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, model, makeEnvironmentProviders, Pipe } from '@angular/core';
|
|
3
3
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
4
|
-
import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
|
|
5
4
|
import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
|
|
6
6
|
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';
|
|
7
7
|
import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
|
|
8
8
|
import { HttpClient } from '@angular/common/http';
|
|
@@ -252,65 +252,57 @@ const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
|
|
|
252
252
|
* and inherited by all children via Angular's hierarchical injector.
|
|
253
253
|
*/
|
|
254
254
|
const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
|
|
255
|
+
|
|
255
256
|
/**
|
|
256
|
-
* Injection token for
|
|
257
|
-
*
|
|
258
|
-
* This registry tracks which template was used to create each array item,
|
|
259
|
-
* keyed by the item's unique ID. This is essential for "recreate" operations
|
|
260
|
-
* (e.g., after removing items from the middle) where we need to re-resolve
|
|
261
|
-
* items using their original templates, not a fallback.
|
|
262
|
-
*
|
|
263
|
-
* The token is provided by ArrayFieldComponent at its level and shared
|
|
264
|
-
* across all items in that array. Each nested array gets its own registry.
|
|
265
|
-
*
|
|
266
|
-
* @example
|
|
267
|
-
* ```typescript
|
|
268
|
-
* // In ArrayFieldComponent
|
|
269
|
-
* providers: [
|
|
270
|
-
* { provide: ARRAY_TEMPLATE_REGISTRY, useValue: new Map() }
|
|
271
|
-
* ]
|
|
272
|
-
*
|
|
273
|
-
* // When adding items
|
|
274
|
-
* const registry = inject(ARRAY_TEMPLATE_REGISTRY);
|
|
275
|
-
* registry.set(itemId, templates);
|
|
257
|
+
* Injection token for the dynamic forms logger.
|
|
276
258
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* ```
|
|
259
|
+
* Provided by provideDynamicForm() with ConsoleLogger as default.
|
|
260
|
+
* Override with withLoggerConfig(false) to disable logging.
|
|
280
261
|
*/
|
|
281
|
-
const
|
|
262
|
+
const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
|
|
263
|
+
|
|
282
264
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* Provides a function that generates unique IDs for array items. Each array
|
|
286
|
-
* component instance gets its own generator (via useFactory), ensuring:
|
|
287
|
-
* - SSR hydration compatibility (server and client generate same IDs for same array)
|
|
288
|
-
* - No global state pollution between form instances
|
|
289
|
-
* - Deterministic IDs within each array's lifecycle
|
|
290
|
-
*
|
|
291
|
-
* The token is provided by ArrayFieldComponent at its level with a factory
|
|
292
|
-
* that creates a fresh counter for each array instance.
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* ```typescript
|
|
296
|
-
* // In ArrayFieldComponent
|
|
297
|
-
* providers: [
|
|
298
|
-
* { provide: ARRAY_ITEM_ID_GENERATOR, useFactory: createArrayItemIdGenerator }
|
|
299
|
-
* ]
|
|
300
|
-
*
|
|
301
|
-
* // Usage
|
|
302
|
-
* const generateId = inject(ARRAY_ITEM_ID_GENERATOR);
|
|
303
|
-
* const itemId = generateId(); // 'item-0', 'item-1', etc.
|
|
304
|
-
* ```
|
|
265
|
+
* Shared empty chain — returned when no wrappers apply so callers get a
|
|
266
|
+
* stable reference. Freezing makes accidental mutation observable.
|
|
305
267
|
*/
|
|
306
|
-
const
|
|
268
|
+
const EMPTY_WRAPPERS = Object.freeze([]);
|
|
307
269
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
270
|
+
* Element-wise identity (`===`) comparator for wrapper chains. Used as the
|
|
271
|
+
* `equal` option on signal memoisation so reconciled `FieldDef`s with the
|
|
272
|
+
* same chain don't churn downstream — relies on `WrapperConfig` objects
|
|
273
|
+
* being stable across ticks, which holds for configs declared in
|
|
274
|
+
* `FormConfig` / `createWrappers(...)`.
|
|
310
275
|
*/
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
276
|
+
function isSameWrapperChain(a, b) {
|
|
277
|
+
if (a === b)
|
|
278
|
+
return true;
|
|
279
|
+
if (a.length !== b.length)
|
|
280
|
+
return false;
|
|
281
|
+
for (let i = 0; i < a.length; i++) {
|
|
282
|
+
if (a[i] !== b[i])
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Resolves the wrapper chain for a field.
|
|
289
|
+
*
|
|
290
|
+
* Merge order (outermost → innermost): auto-associations, form defaults,
|
|
291
|
+
* field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
|
|
292
|
+
* `skipDefaultWrappers` on the field opt out of individual layers. Fresh
|
|
293
|
+
* array per call — downstream memoisation is on the consuming computed.
|
|
294
|
+
*/
|
|
295
|
+
function resolveWrappers(field, defaultWrappers, autoAssociations) {
|
|
296
|
+
if (field.wrappers === null) {
|
|
297
|
+
return EMPTY_WRAPPERS;
|
|
298
|
+
}
|
|
299
|
+
const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
|
|
300
|
+
const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
|
|
301
|
+
const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
|
|
302
|
+
if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
|
|
303
|
+
return EMPTY_WRAPPERS;
|
|
304
|
+
}
|
|
305
|
+
return [...autoWrappers, ...defaults, ...fieldLevel];
|
|
314
306
|
}
|
|
315
307
|
|
|
316
308
|
/**
|
|
@@ -507,58 +499,6 @@ class WrapperAwareInjector extends Injector {
|
|
|
507
499
|
}
|
|
508
500
|
}
|
|
509
501
|
|
|
510
|
-
/**
|
|
511
|
-
* Injection token for the dynamic forms logger.
|
|
512
|
-
*
|
|
513
|
-
* Provided by provideDynamicForm() with ConsoleLogger as default.
|
|
514
|
-
* Override with withLoggerConfig(false) to disable logging.
|
|
515
|
-
*/
|
|
516
|
-
const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Shared empty chain — returned when no wrappers apply so callers get a
|
|
520
|
-
* stable reference. Freezing makes accidental mutation observable.
|
|
521
|
-
*/
|
|
522
|
-
const EMPTY_WRAPPERS = Object.freeze([]);
|
|
523
|
-
/**
|
|
524
|
-
* Element-wise identity (`===`) comparator for wrapper chains. Used as the
|
|
525
|
-
* `equal` option on signal memoisation so reconciled `FieldDef`s with the
|
|
526
|
-
* same chain don't churn downstream — relies on `WrapperConfig` objects
|
|
527
|
-
* being stable across ticks, which holds for configs declared in
|
|
528
|
-
* `FormConfig` / `createWrappers(...)`.
|
|
529
|
-
*/
|
|
530
|
-
function isSameWrapperChain(a, b) {
|
|
531
|
-
if (a === b)
|
|
532
|
-
return true;
|
|
533
|
-
if (a.length !== b.length)
|
|
534
|
-
return false;
|
|
535
|
-
for (let i = 0; i < a.length; i++) {
|
|
536
|
-
if (a[i] !== b[i])
|
|
537
|
-
return false;
|
|
538
|
-
}
|
|
539
|
-
return true;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Resolves the wrapper chain for a field.
|
|
543
|
-
*
|
|
544
|
-
* Merge order (outermost → innermost): auto-associations, form defaults,
|
|
545
|
-
* field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
|
|
546
|
-
* `skipDefaultWrappers` on the field opt out of individual layers. Fresh
|
|
547
|
-
* array per call — downstream memoisation is on the consuming computed.
|
|
548
|
-
*/
|
|
549
|
-
function resolveWrappers(field, defaultWrappers, autoAssociations) {
|
|
550
|
-
if (field.wrappers === null) {
|
|
551
|
-
return EMPTY_WRAPPERS;
|
|
552
|
-
}
|
|
553
|
-
const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
|
|
554
|
-
const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
|
|
555
|
-
const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
|
|
556
|
-
if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
|
|
557
|
-
return EMPTY_WRAPPERS;
|
|
558
|
-
}
|
|
559
|
-
return [...autoWrappers, ...defaults, ...fieldLevel];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
502
|
/**
|
|
563
503
|
* Owns the wrapper-chain lifecycle for a field or container: async component
|
|
564
504
|
* loading, switchMap-based cancellation of stale loads, flicker-tolerant
|
|
@@ -749,6 +689,155 @@ function toReadonlyFieldTreeCached(cache, field) {
|
|
|
749
689
|
return view;
|
|
750
690
|
}
|
|
751
691
|
|
|
692
|
+
const EMPTY_STATE = Object.freeze({ phase: 'empty' });
|
|
693
|
+
/**
|
|
694
|
+
* Encapsulates the imperative `ViewContainerRef` / `ComponentRef` lifecycle
|
|
695
|
+
* for the innermost field component. Pulled out of `DfFieldOutlet` so the
|
|
696
|
+
* directive itself stays declarative — signals + constructor wiring — while
|
|
697
|
+
* the unavoidable mutable bookkeeping (Angular's view container API is
|
|
698
|
+
* imperative) lives in one place behind a small, focused surface.
|
|
699
|
+
*
|
|
700
|
+
* State is held in a single `signal<FieldSlotState>` discriminated union;
|
|
701
|
+
* every mutation is an explicit `state.set(...)` transition. No nullable
|
|
702
|
+
* instance fields, no `undefined`-initialized refs — each phase carries
|
|
703
|
+
* exactly the data it owns.
|
|
704
|
+
*
|
|
705
|
+
* Responsibilities:
|
|
706
|
+
* - Create the component (or reuse the same-class instance across a rebuild,
|
|
707
|
+
* preserving its DOM state, focus, and caret).
|
|
708
|
+
* - Push mapper inputs via `setInput`, dedup'd per-key so dev-mode NG0303
|
|
709
|
+
* warnings for undeclared mapper keys don't replay on every emission.
|
|
710
|
+
* - Detach cleanly before the outer VCR clears, so same-class reuse can
|
|
711
|
+
* re-insert the preserved `hostView`.
|
|
712
|
+
* - Destroy on directive teardown when the host view is still detached.
|
|
713
|
+
*/
|
|
714
|
+
class FieldComponentSlot {
|
|
715
|
+
state = signal(EMPTY_STATE, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
716
|
+
/**
|
|
717
|
+
* Per-key cache for `pushInputs`. Lives outside the lifecycle state machine on
|
|
718
|
+
* purpose — it's a `setInput`-call optimization that changes on every keystroke,
|
|
719
|
+
* not a phase transition. Keeping it here means the `state` signal only emits
|
|
720
|
+
* on real `empty ⇄ mounted ⇄ detached` transitions (≈ once per
|
|
721
|
+
* mount/destroy cycle), not once per input emission.
|
|
722
|
+
*/
|
|
723
|
+
lastInputs = null;
|
|
724
|
+
/** Read-only view of the lifecycle phase. Useful for tests and assertions. */
|
|
725
|
+
phase = () => this.state().phase;
|
|
726
|
+
/** Read-only view of the full state. Exposed so tests can assert ref stability. */
|
|
727
|
+
snapshot = () => this.state();
|
|
728
|
+
/**
|
|
729
|
+
* Mount the field component into `slot`. Reuses the existing `ComponentRef`
|
|
730
|
+
* when its component type matches (preserving DOM/focus/caret across a
|
|
731
|
+
* wrapper-chain rebuild); otherwise discards the old ref and creates a fresh
|
|
732
|
+
* one.
|
|
733
|
+
*/
|
|
734
|
+
mountOrReuse(slot, component, fieldInjector, environmentInjector, rawInputs) {
|
|
735
|
+
const current = this.state();
|
|
736
|
+
if (current.phase !== 'empty' && current.ref.componentType === component) {
|
|
737
|
+
slot.insert(current.ref.hostView);
|
|
738
|
+
this.lastInputs = null;
|
|
739
|
+
this.state.set(Object.freeze({ phase: 'mounted', ref: current.ref, slot }));
|
|
740
|
+
this.pushInputs(rawInputs);
|
|
741
|
+
if (current.phase === 'detached')
|
|
742
|
+
this.replayFocus(current.focusSnapshot);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Different component class — destroy the prior ref (if any) and create
|
|
746
|
+
// a fresh one. The `empty` transition before `createComponent` ensures a
|
|
747
|
+
// throw inside the factory doesn't leave a dangling ref the next mount
|
|
748
|
+
// would try to re-insert.
|
|
749
|
+
if (current.phase !== 'empty')
|
|
750
|
+
current.ref.destroy();
|
|
751
|
+
this.lastInputs = null;
|
|
752
|
+
this.state.set(EMPTY_STATE);
|
|
753
|
+
const ref = slot.createComponent(component, {
|
|
754
|
+
environmentInjector,
|
|
755
|
+
injector: createWrapperAwareInjector(fieldInjector, slot.injector),
|
|
756
|
+
});
|
|
757
|
+
this.state.set(Object.freeze({ phase: 'mounted', ref, slot }));
|
|
758
|
+
this.pushInputs(rawInputs);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Push mapper inputs onto the mounted component. Per-key reference compare
|
|
762
|
+
* skips `setInput` for unchanged values — necessary in dev mode where
|
|
763
|
+
* `setInput` on unknown keys logs NG0303 per call (Angular 21 is lenient
|
|
764
|
+
* at runtime but warns); without the cache, every emission would re-warn
|
|
765
|
+
* for every undeclared mapper key.
|
|
766
|
+
*
|
|
767
|
+
* Updates the `lastInputs` cache (a plain field, not part of `state`) so the
|
|
768
|
+
* `state` signal doesn't churn its reference on every keystroke.
|
|
769
|
+
*
|
|
770
|
+
* `reflectComponentType` does NOT include hostDirective-forwarded inputs
|
|
771
|
+
* (angular/angular#49734), so a declared-input filter here would skip every
|
|
772
|
+
* value flowing through `NgForgeFieldShell` / `NgForgeField` /
|
|
773
|
+
* `NgForgeAction`. Mapper-as-contract is enforced by the `*_INPUTS`
|
|
774
|
+
* lockstep type assertions, not at runtime here.
|
|
775
|
+
*/
|
|
776
|
+
pushInputs(rawInputs) {
|
|
777
|
+
const current = this.state();
|
|
778
|
+
if (current.phase !== 'mounted')
|
|
779
|
+
return;
|
|
780
|
+
const prev = this.lastInputs;
|
|
781
|
+
if (prev === rawInputs)
|
|
782
|
+
return;
|
|
783
|
+
for (const [key, value] of Object.entries(rawInputs)) {
|
|
784
|
+
if (prev && prev[key] === value)
|
|
785
|
+
continue;
|
|
786
|
+
current.ref.setInput(key, value);
|
|
787
|
+
}
|
|
788
|
+
this.lastInputs = rawInputs;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Detach the host view from its current slot, capturing focus + caret so the
|
|
792
|
+
* next `mountOrReuse()` can replay them after re-inserting into the new
|
|
793
|
+
* slot. A stranded `mounted` state with a bad slot index (re-entrancy during
|
|
794
|
+
* a partial rebuild) is treated like `empty` to avoid leaks.
|
|
795
|
+
*/
|
|
796
|
+
detach() {
|
|
797
|
+
const current = this.state();
|
|
798
|
+
if (current.phase !== 'mounted')
|
|
799
|
+
return;
|
|
800
|
+
const focusSnapshot = this.captureFocus(current.ref);
|
|
801
|
+
const idx = current.slot.indexOf(current.ref.hostView);
|
|
802
|
+
if (idx >= 0)
|
|
803
|
+
current.slot.detach(idx);
|
|
804
|
+
this.lastInputs = null;
|
|
805
|
+
this.state.set(Object.freeze({ phase: 'detached', ref: current.ref, focusSnapshot }));
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Called on directive teardown. Only destroys when the host view is still
|
|
809
|
+
* detached — otherwise the outer `vcr.clear()` cascade has already destroyed
|
|
810
|
+
* it (calling `.destroy()` again would throw).
|
|
811
|
+
*/
|
|
812
|
+
destroyOnTeardown() {
|
|
813
|
+
const current = this.state();
|
|
814
|
+
if (current.phase === 'detached')
|
|
815
|
+
current.ref.destroy();
|
|
816
|
+
this.lastInputs = null;
|
|
817
|
+
this.state.set(EMPTY_STATE);
|
|
818
|
+
}
|
|
819
|
+
captureFocus(ref) {
|
|
820
|
+
const hostEl = ref.location.nativeElement;
|
|
821
|
+
const active = document.activeElement;
|
|
822
|
+
if (!(active instanceof HTMLElement) || !hostEl.contains(active))
|
|
823
|
+
return null;
|
|
824
|
+
const sel = active;
|
|
825
|
+
return {
|
|
826
|
+
element: active,
|
|
827
|
+
selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
|
|
828
|
+
selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
replayFocus(snap) {
|
|
832
|
+
if (!snap || !snap.element.isConnected)
|
|
833
|
+
return;
|
|
834
|
+
snap.element.focus();
|
|
835
|
+
if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
|
|
836
|
+
snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
752
841
|
/**
|
|
753
842
|
* Structural directive that renders a `ResolvedField` with its effective
|
|
754
843
|
* wrapper chain.
|
|
@@ -769,12 +858,16 @@ function toReadonlyFieldTreeCached(cache, field) {
|
|
|
769
858
|
* includes the field's mapper outputs and a `ReadonlyFieldTree` view of its
|
|
770
859
|
* form state.
|
|
771
860
|
*
|
|
772
|
-
* Rendering is gated by `field.renderReady()` — the
|
|
773
|
-
* the mapper produces the required inputs
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* a rebuild.
|
|
861
|
+
* Rendering is gated by `field.renderReady() && !field.hidden()` — the
|
|
862
|
+
* directive waits until the mapper produces the required inputs AND the
|
|
863
|
+
* field isn't hidden before instantiating the component. Once rendered, a
|
|
864
|
+
* transient `renderReady → false` does *not* tear the chain down; the
|
|
865
|
+
* controller keeps the mounted components alive and ignores the flicker.
|
|
866
|
+
* Only a structural change (wrappers or component class) triggers a rebuild.
|
|
867
|
+
*
|
|
868
|
+
* Imperative `ComponentRef` / `ViewContainerRef` lifecycle is encapsulated
|
|
869
|
+
* in `FieldComponentSlot` — the directive itself is signal definitions
|
|
870
|
+
* plus three method calls (`mountOrReuse`, `detach`, `destroyOnTeardown`).
|
|
778
871
|
*
|
|
779
872
|
* @example
|
|
780
873
|
* ```html
|
|
@@ -794,25 +887,16 @@ class DfFieldOutlet {
|
|
|
794
887
|
wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);
|
|
795
888
|
defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });
|
|
796
889
|
readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
* VCR when there are no wrappers, or the innermost wrapper's `#fieldComponent`
|
|
801
|
-
* slot). Tracked so `beforeRebuild` can detach the view before the outer VCR
|
|
802
|
-
* cascade-destroys it.
|
|
803
|
-
*/
|
|
804
|
-
fieldSlot;
|
|
805
|
-
/** Focus + caret captured during detach, replayed after re-insert. */
|
|
806
|
-
focusSnapshot;
|
|
890
|
+
/** Encapsulates Angular's imperative ComponentRef / ViewContainerRef lifecycle. */
|
|
891
|
+
fieldComponent = new FieldComponentSlot();
|
|
892
|
+
componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
|
|
807
893
|
/**
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
894
|
+
* Gate for the wrapper chain controller: true only when required inputs are populated AND the
|
|
895
|
+
* field isn't hidden. Combining the two prevents an initial-mount race where the chain mounts
|
|
896
|
+
* during the brief window between `renderReady` flipping true and `hidden()` settling — that
|
|
897
|
+
* window is exactly where Angular Signal Forms' `[formField]` directive emits NG01916.
|
|
812
898
|
*/
|
|
813
|
-
|
|
814
|
-
componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
|
|
815
|
-
renderReady = computed(() => this.dfFieldOutlet().renderReady(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
|
|
899
|
+
renderReady = computed(() => this.dfFieldOutlet().renderReady() && !this.dfFieldOutlet().hidden(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
|
|
816
900
|
rawInputs = computed(() => this.dfFieldOutlet().inputs(), ...(ngDevMode ? [{ debugName: "rawInputs" }] : /* istanbul ignore next */ []));
|
|
817
901
|
/**
|
|
818
902
|
* Effective wrapper chain. Element-wise identity comparison keeps the signal
|
|
@@ -839,109 +923,17 @@ class DfFieldOutlet {
|
|
|
839
923
|
rebuildKey: this.componentIdentity,
|
|
840
924
|
fieldInputs: this.fieldInputs,
|
|
841
925
|
fieldInjector: this.fieldInjector,
|
|
842
|
-
beforeRebuild: () => this.
|
|
926
|
+
beforeRebuild: () => this.fieldComponent.detach(),
|
|
843
927
|
renderInnermost: (slot) => {
|
|
844
928
|
const resolved = this.dfFieldOutlet();
|
|
845
|
-
|
|
846
|
-
// Same component class — re-insert the preserved hostView. Browser
|
|
847
|
-
// loses focus on detach, so we replay the snapshot captured in
|
|
848
|
-
// detachFieldRef. Input value persists on the DOM node itself.
|
|
849
|
-
slot.insert(this.fieldRef.hostView);
|
|
850
|
-
this.fieldSlot = slot;
|
|
851
|
-
this.pushRawInputs(this.fieldRef, this.rawInputs());
|
|
852
|
-
this.restoreFocusSnapshot();
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
// Different component class — discard the old ref and create fresh.
|
|
856
|
-
// Merge field + element injectors so the field can inject ancestor
|
|
857
|
-
// wrappers AND form-context tokens (ARRAY_CONTEXT etc.).
|
|
858
|
-
// Clear state before createComponent — if it throws (bad provider,
|
|
859
|
-
// invalid class) we don't want to leak a reference to a destroyed ref
|
|
860
|
-
// that the same-component branch would later try to re-insert.
|
|
861
|
-
this.focusSnapshot = undefined;
|
|
862
|
-
this.lastPushedInputs = undefined;
|
|
863
|
-
const previousRef = this.fieldRef;
|
|
864
|
-
this.fieldRef = undefined;
|
|
865
|
-
this.fieldSlot = undefined;
|
|
866
|
-
previousRef?.destroy();
|
|
867
|
-
this.fieldRef = slot.createComponent(resolved.component, {
|
|
868
|
-
environmentInjector: this.fieldEnvInjector(),
|
|
869
|
-
injector: createWrapperAwareInjector(resolved.injector, slot.injector),
|
|
870
|
-
});
|
|
871
|
-
this.fieldSlot = slot;
|
|
872
|
-
// setInput dirties the signal synchronously; the next CD pass reads
|
|
873
|
-
// it. Safe outside CD — see wrapper-chain-controller scheduling.
|
|
874
|
-
this.pushRawInputs(this.fieldRef, this.rawInputs());
|
|
929
|
+
this.fieldComponent.mountOrReuse(slot, resolved.component, resolved.injector, this.fieldEnvInjector(), this.rawInputs());
|
|
875
930
|
},
|
|
876
931
|
});
|
|
877
|
-
// Push rawInputs to the innermost field.
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (this.lastPushedInputs === rawInputs)
|
|
883
|
-
return;
|
|
884
|
-
this.pushRawInputs(this.fieldRef, rawInputs);
|
|
885
|
-
});
|
|
886
|
-
this.destroyRef.onDestroy(() => {
|
|
887
|
-
// Only destroy when detached — vcr.clear() cascades otherwise.
|
|
888
|
-
if (this.fieldRef && !this.fieldSlot)
|
|
889
|
-
this.fieldRef.destroy();
|
|
890
|
-
this.fieldRef = undefined;
|
|
891
|
-
this.fieldSlot = undefined;
|
|
892
|
-
this.lastPushedInputs = undefined;
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
detachFieldRef() {
|
|
896
|
-
if (!this.fieldRef)
|
|
897
|
-
return;
|
|
898
|
-
if (!this.fieldSlot) {
|
|
899
|
-
// Stranded fieldRef — destroy to prevent a leak.
|
|
900
|
-
this.fieldRef.destroy();
|
|
901
|
-
this.fieldRef = undefined;
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
this.captureFocusSnapshot();
|
|
905
|
-
const idx = this.fieldSlot.indexOf(this.fieldRef.hostView);
|
|
906
|
-
if (idx >= 0)
|
|
907
|
-
this.fieldSlot.detach(idx);
|
|
908
|
-
this.fieldSlot = undefined;
|
|
909
|
-
}
|
|
910
|
-
/** Capture active element + selection before the hostView is detached from DOM. */
|
|
911
|
-
captureFocusSnapshot() {
|
|
912
|
-
if (!this.fieldRef)
|
|
913
|
-
return;
|
|
914
|
-
const hostEl = this.fieldRef.location.nativeElement;
|
|
915
|
-
const active = document.activeElement;
|
|
916
|
-
if (!(active instanceof HTMLElement) || !hostEl.contains(active))
|
|
917
|
-
return;
|
|
918
|
-
const sel = active;
|
|
919
|
-
this.focusSnapshot = {
|
|
920
|
-
element: active,
|
|
921
|
-
selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
|
|
922
|
-
selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
/** Re-apply the pre-detach focus + caret after the hostView is back in the DOM. */
|
|
926
|
-
restoreFocusSnapshot() {
|
|
927
|
-
const snap = this.focusSnapshot;
|
|
928
|
-
this.focusSnapshot = undefined;
|
|
929
|
-
if (!snap || !snap.element.isConnected)
|
|
930
|
-
return;
|
|
931
|
-
snap.element.focus();
|
|
932
|
-
if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
|
|
933
|
-
snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
pushRawInputs(ref, rawInputs) {
|
|
937
|
-
// setInput is lenient in Angular 21 — unknown keys silently drop (not NG0303). reflectComponentType does NOT include hostDirective forwarded inputs (angular/angular#49734), so a declared-input filter here would skip every value flowing through NgForgeFieldShell / NgForgeField / NgForgeAction. Mapper-as-contract is enforced by the *_INPUTS lockstep type assertions, not at runtime here.
|
|
938
|
-
const last = this.lastPushedInputs;
|
|
939
|
-
for (const [key, value] of Object.entries(rawInputs)) {
|
|
940
|
-
if (last && last[key] === value)
|
|
941
|
-
continue;
|
|
942
|
-
ref.setInput(key, value);
|
|
943
|
-
}
|
|
944
|
-
this.lastPushedInputs = rawInputs;
|
|
932
|
+
// Push rawInputs to the innermost field on every change. The slot dedupes per-key, so the
|
|
933
|
+
// initial render's synchronous push from `renderInnermost` and the effect's first fire
|
|
934
|
+
// converge cleanly with no double-pushing.
|
|
935
|
+
explicitEffect([this.rawInputs], ([rawInputs]) => this.fieldComponent.pushInputs(rawInputs));
|
|
936
|
+
this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());
|
|
945
937
|
}
|
|
946
938
|
buildFieldInputs(rawInputs) {
|
|
947
939
|
const fieldTreeCandidate = rawInputs['field'];
|
|
@@ -4180,9 +4172,9 @@ class PageOrchestratorComponent {
|
|
|
4180
4172
|
}
|
|
4181
4173
|
}
|
|
4182
4174
|
}
|
|
4183
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4175
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)]] });
|
|
4184
4176
|
}
|
|
4185
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4177
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
4186
4178
|
type: Component,
|
|
4187
4179
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
4188
4180
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -4614,6 +4606,85 @@ function isValidNonPagedForm(fields) {
|
|
|
4614
4606
|
return result.mode === 'non-paged' && result.isValid;
|
|
4615
4607
|
}
|
|
4616
4608
|
|
|
4609
|
+
function createSlot() {
|
|
4610
|
+
let counter = 0;
|
|
4611
|
+
const templates = new Map();
|
|
4612
|
+
const itemOrder = [];
|
|
4613
|
+
const primitiveFieldKey = signal(undefined, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
|
|
4614
|
+
return {
|
|
4615
|
+
templates,
|
|
4616
|
+
itemOrder,
|
|
4617
|
+
primitiveFieldKey,
|
|
4618
|
+
nextId: () => `item-${counter++}`,
|
|
4619
|
+
setTemplate(itemId, t) {
|
|
4620
|
+
templates.set(itemId, t);
|
|
4621
|
+
},
|
|
4622
|
+
getTemplate(itemId) {
|
|
4623
|
+
return templates.get(itemId);
|
|
4624
|
+
},
|
|
4625
|
+
hasTemplate(itemId) {
|
|
4626
|
+
return templates.has(itemId);
|
|
4627
|
+
},
|
|
4628
|
+
setIdAt(index, itemId) {
|
|
4629
|
+
itemOrder[index] = itemId;
|
|
4630
|
+
},
|
|
4631
|
+
removeAt(index) {
|
|
4632
|
+
const [removed] = itemOrder.splice(index, 1);
|
|
4633
|
+
if (removed !== undefined)
|
|
4634
|
+
templates.delete(removed);
|
|
4635
|
+
},
|
|
4636
|
+
moveItem(fromIndex, toIndex) {
|
|
4637
|
+
const [id] = itemOrder.splice(fromIndex, 1);
|
|
4638
|
+
if (id !== undefined)
|
|
4639
|
+
itemOrder.splice(toIndex, 0, id);
|
|
4640
|
+
},
|
|
4641
|
+
truncate(length) {
|
|
4642
|
+
const removed = itemOrder.splice(length);
|
|
4643
|
+
for (const id of removed)
|
|
4644
|
+
templates.delete(id);
|
|
4645
|
+
},
|
|
4646
|
+
clear() {
|
|
4647
|
+
templates.clear();
|
|
4648
|
+
itemOrder.length = 0;
|
|
4649
|
+
counter = 0;
|
|
4650
|
+
primitiveFieldKey.set(undefined);
|
|
4651
|
+
},
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Form-scoped registry of per-array durable state. One slot per array path
|
|
4656
|
+
* (e.g. 'members', 'team.members', 'orders.lineItems'). Survives
|
|
4657
|
+
* `ArrayFieldComponent` lifecycle, dies with the `DynamicForm`. Cleared on
|
|
4658
|
+
* schema swap by the `formSetup` effect in `FormStateManager`.
|
|
4659
|
+
*
|
|
4660
|
+
* `ArrayFieldComponent` injects this service and obtains its slot via
|
|
4661
|
+
* `slotFor(arrayPath)` on mount. Replaces the prior per-component
|
|
4662
|
+
* `ARRAY_TEMPLATE_REGISTRY` + `ARRAY_ITEM_ID_GENERATOR` providers.
|
|
4663
|
+
*/
|
|
4664
|
+
class ArrayItemRegistryService {
|
|
4665
|
+
slots = new Map();
|
|
4666
|
+
/** Get-or-create a slot for the array at the given path. */
|
|
4667
|
+
slotFor(arrayPath) {
|
|
4668
|
+
let slot = this.slots.get(arrayPath);
|
|
4669
|
+
if (!slot) {
|
|
4670
|
+
slot = createSlot();
|
|
4671
|
+
this.slots.set(arrayPath, slot);
|
|
4672
|
+
}
|
|
4673
|
+
return slot;
|
|
4674
|
+
}
|
|
4675
|
+
/** Drop all slots — called from the `formSetup` effect on schema change. */
|
|
4676
|
+
clearAll() {
|
|
4677
|
+
for (const slot of this.slots.values())
|
|
4678
|
+
slot.clear();
|
|
4679
|
+
this.slots.clear();
|
|
4680
|
+
}
|
|
4681
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4682
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService });
|
|
4683
|
+
}
|
|
4684
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService, decorators: [{
|
|
4685
|
+
type: Injectable
|
|
4686
|
+
}] });
|
|
4687
|
+
|
|
4617
4688
|
class SchemaRegistryService {
|
|
4618
4689
|
registeredSchemas = new Map();
|
|
4619
4690
|
/**
|
|
@@ -4860,8 +4931,6 @@ class HttpConditionFunctionCacheService {
|
|
|
4860
4931
|
* );
|
|
4861
4932
|
* // user.value() keeps the old user data during loading transitions
|
|
4862
4933
|
* ```
|
|
4863
|
-
*
|
|
4864
|
-
* @experimental Uses Angular's experimental resource composition APIs.
|
|
4865
4934
|
*/
|
|
4866
4935
|
function withPreviousValue(input) {
|
|
4867
4936
|
const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
|
|
@@ -8794,6 +8863,40 @@ function createRenderReadySignal(inputs, definition) {
|
|
|
8794
8863
|
return requiredInputs.every((inputName) => currentInputs[inputName] !== undefined);
|
|
8795
8864
|
});
|
|
8796
8865
|
}
|
|
8866
|
+
/**
|
|
8867
|
+
* Reads the current hidden state from mapper inputs. Uniform across all field
|
|
8868
|
+
* types — value-bearing leaves source from the `FieldTree`'s `state.hidden()`
|
|
8869
|
+
* (where Angular Signal Forms cascades `hidden(path, …)`), and non-field outputs
|
|
8870
|
+
* (button/text/containers) source from the mapper's explicit `inputs.hidden`.
|
|
8871
|
+
*
|
|
8872
|
+
* The `@if (!field.hidden())` gate at every `*dfFieldOutlet` consumption site uses
|
|
8873
|
+
* this signal, removing hidden fields from the DOM. That's required to silence
|
|
8874
|
+
* Angular's `NG01916: Field 'X' is hidden but is being rendered` warning — and
|
|
8875
|
+
* is the prescribed pattern from the
|
|
8876
|
+
* [`hidden` API docs](https://angular.dev/api/forms/signals/hidden).
|
|
8877
|
+
*
|
|
8878
|
+
* Container internal state (e.g. an `ArrayFieldComponent`'s items + dynamic
|
|
8879
|
+
* templates + id counter) survives this destroy/recreate via the form-scoped
|
|
8880
|
+
* `ArrayItemRegistryService` keyed by array path.
|
|
8881
|
+
*/
|
|
8882
|
+
function createHiddenSignal(inputs) {
|
|
8883
|
+
return computed(() => {
|
|
8884
|
+
const currentInputs = inputs();
|
|
8885
|
+
const fieldCandidate = currentInputs['field'];
|
|
8886
|
+
// Value-bearing leaves expose `field` as a callable `FieldTree` whose invocation returns a
|
|
8887
|
+
// `FieldState` with a `hidden` accessor. `isSignal()` doesn't recognise the Proxy-wrapped
|
|
8888
|
+
// FieldTree (no SIGNAL symbol), so we duck-type the shape and call `state.hidden()` directly.
|
|
8889
|
+
if (typeof fieldCandidate === 'function') {
|
|
8890
|
+
const state = fieldCandidate();
|
|
8891
|
+
if (state && typeof state.hidden === 'function') {
|
|
8892
|
+
return state.hidden() === true;
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8895
|
+
// Non-value fields (button, text, containers) carry an explicit `hidden` input set by
|
|
8896
|
+
// `applyHiddenLogic` based on the mapper-side evaluation.
|
|
8897
|
+
return currentInputs['hidden'] === true;
|
|
8898
|
+
});
|
|
8899
|
+
}
|
|
8797
8900
|
/**
|
|
8798
8901
|
* Resolves a single field definition to a ResolvedField using RxJS.
|
|
8799
8902
|
* Loads the component asynchronously and maps inputs in the injection context.
|
|
@@ -8830,6 +8933,7 @@ function resolveField(fieldDef, context) {
|
|
|
8830
8933
|
injector: context.injector,
|
|
8831
8934
|
inputs,
|
|
8832
8935
|
renderReady: createRenderReadySignal(inputs, definition),
|
|
8936
|
+
hidden: createHiddenSignal(inputs),
|
|
8833
8937
|
};
|
|
8834
8938
|
}), catchError((error) => {
|
|
8835
8939
|
// Only call onError if component is not destroyed to avoid accessing cleaned-up state
|
|
@@ -8866,6 +8970,7 @@ function resolveFieldSync(fieldDef, context) {
|
|
|
8866
8970
|
injector: context.injector,
|
|
8867
8971
|
inputs,
|
|
8868
8972
|
renderReady: createRenderReadySignal(inputs, definition),
|
|
8973
|
+
hidden: createHiddenSignal(inputs),
|
|
8869
8974
|
};
|
|
8870
8975
|
}
|
|
8871
8976
|
/**
|
|
@@ -8892,13 +8997,14 @@ function reconcileFields(prev, curr) {
|
|
|
8892
8997
|
}
|
|
8893
8998
|
// Same component instance + injector, but the field definition changed.
|
|
8894
8999
|
// Preserve component identity (so ngComponentOutlet keeps the same instance)
|
|
8895
|
-
// while swapping in the freshly-mapped inputs/renderReady signals
|
|
8896
|
-
// updates (e.g. select options) reach the component.
|
|
9000
|
+
// while swapping in the freshly-mapped inputs/renderReady/hidden signals
|
|
9001
|
+
// so prop updates (e.g. select options) reach the component.
|
|
8897
9002
|
return {
|
|
8898
9003
|
...existing,
|
|
8899
9004
|
fieldDef: field.fieldDef,
|
|
8900
9005
|
inputs: field.inputs,
|
|
8901
9006
|
renderReady: field.renderReady,
|
|
9007
|
+
hidden: field.hidden,
|
|
8902
9008
|
};
|
|
8903
9009
|
}
|
|
8904
9010
|
// New field, type changed, or context changed (new injector) - use new field
|
|
@@ -10083,6 +10189,12 @@ class FormStateManager {
|
|
|
10083
10189
|
eventBus = inject(EventBus);
|
|
10084
10190
|
functionRegistry = inject(FunctionRegistryService);
|
|
10085
10191
|
schemaRegistry = inject(SchemaRegistryService);
|
|
10192
|
+
// Optional so unit-test setups that drive FormStateManager directly without provideDynamicFormDI()
|
|
10193
|
+
// (e.g. mocked RootFormRegistryService specs) don't have to manually wire the registry. Real
|
|
10194
|
+
// DynamicForm usage always provides it via coreProviders; if you hit the dev warning below in
|
|
10195
|
+
// your own setup, you're using DynamicForm without provideDynamicFormDI() — fix the providers
|
|
10196
|
+
// chain rather than ignoring it.
|
|
10197
|
+
arrayItemRegistry = inject(ArrayItemRegistryService, { optional: true });
|
|
10086
10198
|
/** Host component dependencies (config, formOptions, value). */
|
|
10087
10199
|
deps = (() => {
|
|
10088
10200
|
const raw = inject(FORM_STATE_DEPS);
|
|
@@ -10709,12 +10821,24 @@ class FormStateManager {
|
|
|
10709
10821
|
}
|
|
10710
10822
|
});
|
|
10711
10823
|
// Schema change resets the saved store + transition tracker so values from a stale schema
|
|
10712
|
-
// can't leak into the new one.
|
|
10824
|
+
// can't leak into the new one. Also drops every per-array slot in the registry so a fresh
|
|
10825
|
+
// schema starts with an empty template/itemOrder/id-counter state.
|
|
10826
|
+
let warnedAboutMissingArrayRegistry = false;
|
|
10713
10827
|
explicitEffect([this.formSetup], () => {
|
|
10714
10828
|
this.prevFieldStateSnapshot.set({});
|
|
10715
10829
|
if (Object.keys(this.excludedValueStore()).length > 0) {
|
|
10716
10830
|
this.excludedValueStore.set({});
|
|
10717
10831
|
}
|
|
10832
|
+
if (this.arrayItemRegistry) {
|
|
10833
|
+
this.arrayItemRegistry.clearAll();
|
|
10834
|
+
}
|
|
10835
|
+
else if (DEV_MODE && !warnedAboutMissingArrayRegistry) {
|
|
10836
|
+
warnedAboutMissingArrayRegistry = true;
|
|
10837
|
+
this.logger.warn('ArrayItemRegistryService was not provided to FormStateManager. ' +
|
|
10838
|
+
'Array fields will lose dynamically-added item templates across hide/show cycles. ' +
|
|
10839
|
+
'This usually means DynamicForm was bootstrapped without provideDynamicFormDI() — ' +
|
|
10840
|
+
'check your providers chain.');
|
|
10841
|
+
}
|
|
10718
10842
|
});
|
|
10719
10843
|
// Outward sync uses boundFormValue (honors only explicit excludeValueIf* opt-ins) so
|
|
10720
10844
|
// global defaults that target submission output don't reshape the host's bound model.
|
|
@@ -14452,6 +14576,7 @@ function coreProviders() {
|
|
|
14452
14576
|
deps: [FormStateManager],
|
|
14453
14577
|
},
|
|
14454
14578
|
FieldContextRegistryService,
|
|
14579
|
+
ArrayItemRegistryService,
|
|
14455
14580
|
{
|
|
14456
14581
|
provide: DEFAULT_PROPS,
|
|
14457
14582
|
useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultProps),
|
|
@@ -14892,7 +15017,13 @@ class DynamicForm {
|
|
|
14892
15017
|
}
|
|
14893
15018
|
@case ('non-paged') {
|
|
14894
15019
|
@for (field of resolvedFields(); track field.key) {
|
|
14895
|
-
|
|
15020
|
+
<!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
|
|
15021
|
+
the template @if removes the host from the DOM (Angular's prescribed pattern), and
|
|
15022
|
+
the directive's gate stops a same-CD-pass mount before [formField] can warn.
|
|
15023
|
+
Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
|
|
15024
|
+
@if (!field.hidden()) {
|
|
15025
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15026
|
+
}
|
|
14896
15027
|
}
|
|
14897
15028
|
}
|
|
14898
15029
|
@default {
|
|
@@ -14912,7 +15043,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
14912
15043
|
}
|
|
14913
15044
|
@case ('non-paged') {
|
|
14914
15045
|
@for (field of resolvedFields(); track field.key) {
|
|
14915
|
-
|
|
15046
|
+
<!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
|
|
15047
|
+
the template @if removes the host from the DOM (Angular's prescribed pattern), and
|
|
15048
|
+
the directive's gate stops a same-CD-pass mount before [formField] can warn.
|
|
15049
|
+
Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
|
|
15050
|
+
@if (!field.hidden()) {
|
|
15051
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15052
|
+
}
|
|
14916
15053
|
}
|
|
14917
15054
|
}
|
|
14918
15055
|
@default {
|
|
@@ -15495,8 +15632,14 @@ function containerFieldMapper(fieldDef) {
|
|
|
15495
15632
|
* ```
|
|
15496
15633
|
*/
|
|
15497
15634
|
/**
|
|
15498
|
-
* Base
|
|
15499
|
-
*
|
|
15635
|
+
* Base for container field types that consume the mapper-emitted `field` input
|
|
15636
|
+
* (row, group, array, page, container).
|
|
15637
|
+
*/
|
|
15638
|
+
const CONTAINER_FIELD_TYPES_BASE = {
|
|
15639
|
+
renderReadyWhen: ['field'],
|
|
15640
|
+
};
|
|
15641
|
+
/**
|
|
15642
|
+
* Base for display-only fields (`text`) that render immediately without
|
|
15500
15643
|
* waiting for form value integration.
|
|
15501
15644
|
*/
|
|
15502
15645
|
const DISPLAY_FIELD_TYPES_BASE = {
|
|
@@ -15515,31 +15658,35 @@ const BUILT_IN_FIELDS = [
|
|
|
15515
15658
|
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
15516
15659
|
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
15517
15660
|
// renders the flex/grid layout via RowWrapperComponent.
|
|
15518
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15661
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs'),
|
|
15519
15662
|
mapper: rowFieldMapper,
|
|
15520
15663
|
valueHandling: 'flatten',
|
|
15664
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15521
15665
|
},
|
|
15522
15666
|
{
|
|
15523
15667
|
name: 'group',
|
|
15524
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-
|
|
15668
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs'),
|
|
15525
15669
|
mapper: groupFieldMapper,
|
|
15526
15670
|
valueHandling: 'include',
|
|
15671
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15527
15672
|
},
|
|
15528
15673
|
{
|
|
15529
15674
|
name: 'array',
|
|
15530
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-
|
|
15675
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs'),
|
|
15531
15676
|
mapper: arrayFieldMapper,
|
|
15532
15677
|
valueHandling: 'include',
|
|
15678
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15533
15679
|
},
|
|
15534
15680
|
{
|
|
15535
15681
|
name: 'page',
|
|
15536
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
15682
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs'),
|
|
15537
15683
|
mapper: pageFieldMapper,
|
|
15538
15684
|
valueHandling: 'flatten',
|
|
15685
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15539
15686
|
},
|
|
15540
15687
|
{
|
|
15541
15688
|
name: 'text',
|
|
15542
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
15689
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs'),
|
|
15543
15690
|
mapper: textFieldMapper,
|
|
15544
15691
|
valueHandling: 'exclude',
|
|
15545
15692
|
...DISPLAY_FIELD_TYPES_BASE,
|
|
@@ -15551,15 +15698,16 @@ const BUILT_IN_FIELDS = [
|
|
|
15551
15698
|
},
|
|
15552
15699
|
{
|
|
15553
15700
|
name: 'container',
|
|
15554
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15701
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs'),
|
|
15555
15702
|
mapper: containerFieldMapper,
|
|
15556
15703
|
valueHandling: 'flatten',
|
|
15704
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15557
15705
|
},
|
|
15558
15706
|
];
|
|
15559
15707
|
const BUILT_IN_WRAPPERS = [
|
|
15560
15708
|
{
|
|
15561
15709
|
wrapperName: 'css',
|
|
15562
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-
|
|
15710
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs'),
|
|
15563
15711
|
},
|
|
15564
15712
|
{
|
|
15565
15713
|
wrapperName: 'row',
|
|
@@ -16633,5 +16781,5 @@ function extractErrorParams(error) {
|
|
|
16633
16781
|
* Generated bundle index. Do not edit.
|
|
16634
16782
|
*/
|
|
16635
16783
|
|
|
16636
|
-
export {
|
|
16637
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
16784
|
+
export { SubmitEvent as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DynamicFormLogger as D, EventBus as E, FIELD_SIGNAL_CONTEXT as F, GROUP_CONTEXT as G, DynamicForm as H, DynamicFormError as I, EventDispatcher as J, FIELD_REGISTRY as K, FORM_OPTIONS as L, FormClearEvent as M, FormResetEvent as N, INITIALIZATION_TIMEOUT_MS as O, InsertArrayItemEvent as P, MoveArrayItemEvent as Q, RootFormRegistryService as R, NextPageEvent as S, NoopLogger as T, PageChangeEvent as U, PopArrayItemEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PrependArrayItemEvent as X, PreviousPageEvent as Y, RemoveAtIndexEvent as Z, ShiftArrayItemEvent as _, DEFAULT_WRAPPERS as a, WRAPPER_REGISTRY as a0, applyMetaToElement as a1, applyValidator as a2, applyValidators as a3, arrayEvent as a4, arrayFieldMapper as a5, baseFieldMapper as a6, buildBaseInputs as a7, containerFieldMapper as a8, createField as a9, resolveNextButtonDisabled as aA, resolveNonFieldDisabled as aB, resolveNonFieldHidden as aC, resolveSubmitButtonDisabled as aD, resolveTokens as aE, rowFieldMapper as aF, textFieldMapper as aG, toReadonlyFieldTree as aH, withEventFormValue as aI, withLoggerConfig as aJ, withPreviousValue as aK, withValidationExecutionDefaults as aL, withValueExclusionDefaults as aM, wrapperProps as aN, createWrappers as aa, evaluateNonFieldDisabled as ab, evaluateNonFieldHidden as ac, field as ad, formConfig as ae, getArrayLength as af, groupFieldMapper as ag, hasFormValue as ah, interpolateParams as ai, isArrayField as aj, isCheckedField as ak, isContainerField as al, isContainerTypedField as am, isDisplayOnlyField as an, isFormStateCondition as ao, isLeafField as ap, isPageField as aq, isRowField as ar, isSimplifiedArrayField as as, isValueBearingField as at, isValueField as au, isWrapperTypeDefinition as av, isWrappersBundle as aw, omit as ax, pageFieldMapper as ay, provideDynamicForm as az, isSameWrapperChain as b, createFieldResolutionPipe as c, derivedFromDeferred as d, createWrapperChainController as e, DfFieldOutlet as f, isEqual as g, createSchemaFromFields as h, injectFieldRegistry as i, createHiddenSignal as j, createRenderReadySignal as k, ArrayItemRegistryService as l, mapFieldToInputs as m, getNormalizedArrayMetadata as n, getFieldValueHandling as o, getFieldDefaultValue as p, isGroupField as q, resolveWrappers as r, ComponentInitializedEvent as s, DynamicTextPipe as t, dynamicTextToObservable as u, validatePageNesting as v, AppendArrayItemEvent as w, ConsoleLogger as x, DEFAULT_PROPS as y, DEFAULT_VALIDATION_MESSAGES as z };
|
|
16785
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map
|