@ng-forge/dynamic-forms 0.9.0-next.12 → 0.9.0-next.14
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-BgCrTME1.mjs → ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs} +9 -9
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs} +6 -6
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs.map → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs → ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs} +9 -9
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +334 -27
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs} +1558 -172
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs +114 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs.map → ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs +34 -0
- package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map → ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs.map → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +244 -40
- package/types/ng-forge-dynamic-forms.d.ts +1719 -545
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs +0 -114
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +0 -34
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, reflectComponentType, Injector,
|
|
2
|
+
import { InjectionToken, inject, reflectComponentType, Injector, EnvironmentInjector, DestroyRef, computed, signal, input, ViewContainerRef, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, TemplateRef, model, contentChildren, makeEnvironmentProviders, Pipe } from '@angular/core';
|
|
3
3
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
4
4
|
import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
5
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';
|
|
@@ -8,6 +8,7 @@ import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1,
|
|
|
8
8
|
import { HttpClient } from '@angular/common/http';
|
|
9
9
|
import { isStandardSchemaMarker } from '@ng-forge/dynamic-forms/schema';
|
|
10
10
|
import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
|
|
11
|
+
import { NgComponentOutlet, AsyncPipe, NgTemplateOutlet } from '@angular/common';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Type guard for WrapperTypeDefinition.
|
|
@@ -97,28 +98,39 @@ const DEFAULT_WRAPPERS = new InjectionToken('DEFAULT_WRAPPERS');
|
|
|
97
98
|
*
|
|
98
99
|
* @example
|
|
99
100
|
* ```typescript
|
|
100
|
-
* // In a mapper function
|
|
101
|
+
* // In a mapper function — prefer the helper for required access (descriptive error).
|
|
101
102
|
* export function mapValueField(fieldDef: BaseValueField<any, any>): Binding[] {
|
|
102
|
-
* const context =
|
|
103
|
+
* const context = injectFieldSignalContext();
|
|
103
104
|
* const form = context.form();
|
|
104
105
|
* // ... use context
|
|
105
106
|
* }
|
|
106
107
|
*
|
|
107
|
-
* //
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* // Optional access — token directly, returns null if not provided.
|
|
109
|
+
* const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
|
|
110
|
+
* if (ctx) {
|
|
111
|
+
* // ...
|
|
110
112
|
* }
|
|
111
113
|
* ```
|
|
112
114
|
*/
|
|
113
|
-
const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT');
|
|
116
|
+
/**
|
|
117
|
+
* Throwing accessor for {@link FIELD_SIGNAL_CONTEXT}.
|
|
118
|
+
*
|
|
119
|
+
* Use when the consumer requires the context — group/array containers and
|
|
120
|
+
* mappers that depend on form structure. Throws a descriptive
|
|
121
|
+
* `DynamicFormError` instead of Angular's generic `NullInjectorError` when
|
|
122
|
+
* the token isn't provided.
|
|
123
|
+
*
|
|
124
|
+
* Consumers that may legitimately run outside a `DynamicForm` should keep
|
|
125
|
+
* using `inject(FIELD_SIGNAL_CONTEXT, { optional: true })`.
|
|
126
|
+
*/
|
|
127
|
+
function injectFieldSignalContext() {
|
|
128
|
+
const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
|
|
129
|
+
if (!ctx) {
|
|
130
|
+
throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');
|
|
131
|
+
}
|
|
132
|
+
return ctx;
|
|
133
|
+
}
|
|
122
134
|
/**
|
|
123
135
|
* Injection token for providing array context metadata to mappers and components.
|
|
124
136
|
*
|
|
@@ -659,6 +671,20 @@ function toReadonlyFieldTree(field) {
|
|
|
659
671
|
errors: state.errors,
|
|
660
672
|
};
|
|
661
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Write a value into a FieldTree's underlying signal. Signal Forms stores
|
|
676
|
+
* `FieldState.value` as a `WritableSignal`; this helper centralises the cast
|
|
677
|
+
* and warns if that contract ever changes (vs a silent no-op).
|
|
678
|
+
*/
|
|
679
|
+
function writeToFieldValue(value, next, logger) {
|
|
680
|
+
const candidate = value;
|
|
681
|
+
if (typeof candidate.set !== 'function') {
|
|
682
|
+
logger?.warn('writeToFieldValue: FieldState.value is not a WritableSignal — Signal Forms contract changed.');
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
candidate.set(next);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
662
688
|
/**
|
|
663
689
|
* DI-scoped cache for `ReadonlyFieldTree` views, keyed on the source FieldTree
|
|
664
690
|
* identity. A fresh WeakMap per root injector keeps renders isolated under SSR —
|
|
@@ -689,6 +715,52 @@ function toReadonlyFieldTreeCached(cache, field) {
|
|
|
689
715
|
return view;
|
|
690
716
|
}
|
|
691
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Convert a field's raw mapper outputs into the {@link WrapperFieldInputs}
|
|
720
|
+
* bag consumed by wrappers and addons.
|
|
721
|
+
*
|
|
722
|
+
* The transformation hides write access on the underlying `FieldTree` —
|
|
723
|
+
* the bag's `field` slot is a {@link ReadonlyFieldTree} view. The raw
|
|
724
|
+
* `FieldTree` is still pushed to the innermost field component (which
|
|
725
|
+
* legitimately needs to write); intermediaries (wrappers, addons) are
|
|
726
|
+
* read-only by contract.
|
|
727
|
+
*
|
|
728
|
+
* Pure, dependency-injection-free — caller supplies the cache so the same
|
|
729
|
+
* `ReadonlyFieldTree` view is returned for the same source `FieldTree`
|
|
730
|
+
* across renders. Each `<df-dynamic-form>` instance scopes its own cache
|
|
731
|
+
* (SSR-safe).
|
|
732
|
+
*
|
|
733
|
+
* @param rawInputs Mapper outputs (`{ key, label, props, field?: FieldTree, ... }`).
|
|
734
|
+
* @param cache `WeakMap<FieldTree, ReadonlyFieldTree>` provided via
|
|
735
|
+
* {@link READONLY_FIELD_TREE_CACHE}.
|
|
736
|
+
* @param fieldType Optional field-type discriminant. Injected here (rather
|
|
737
|
+
* than via the field mapper) because `type` is the
|
|
738
|
+
* registry key, not a component input — but addons and
|
|
739
|
+
* wrappers legitimately need it.
|
|
740
|
+
*/
|
|
741
|
+
function buildFieldInputs(rawInputs, cache, fieldType) {
|
|
742
|
+
const fieldTreeCandidate = rawInputs['field'];
|
|
743
|
+
// FieldTree is callable: `(): FieldState`. Anything else (undefined,
|
|
744
|
+
// primitive) means the field is rendered before the form is bound.
|
|
745
|
+
const hasFieldTree = fieldTreeCandidate !== undefined && typeof fieldTreeCandidate === 'function';
|
|
746
|
+
const readonlyField = hasFieldTree ? toReadonlyFieldTreeCached(cache, fieldTreeCandidate) : undefined;
|
|
747
|
+
// Lazy writer keyed on the raw FieldTree. Addons (e.g., `prime-button`
|
|
748
|
+
// presets) call this to mutate the host field. `writeToFieldValue`
|
|
749
|
+
// centralises the `Signal → WritableSignal` cast and warns at runtime
|
|
750
|
+
// if Signal Forms ever changes the contract.
|
|
751
|
+
const setValue = hasFieldTree
|
|
752
|
+
? (next) => writeToFieldValue(fieldTreeCandidate().value, next)
|
|
753
|
+
: undefined;
|
|
754
|
+
// Shallow spread — relies on the mapper contract (see WrapperFieldInputs)
|
|
755
|
+
// that rawInputs are emitted as fresh snapshots, not mutated in place.
|
|
756
|
+
return {
|
|
757
|
+
...rawInputs,
|
|
758
|
+
type: fieldType,
|
|
759
|
+
field: readonlyField,
|
|
760
|
+
setValue,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
692
764
|
const EMPTY_STATE = Object.freeze({ phase: 'empty' });
|
|
693
765
|
/**
|
|
694
766
|
* Encapsulates the imperative `ViewContainerRef` / `ComponentRef` lifecycle
|
|
@@ -731,13 +803,13 @@ class FieldComponentSlot {
|
|
|
731
803
|
* wrapper-chain rebuild); otherwise discards the old ref and creates a fresh
|
|
732
804
|
* one.
|
|
733
805
|
*/
|
|
734
|
-
mountOrReuse(slot, component, fieldInjector, environmentInjector, rawInputs) {
|
|
806
|
+
mountOrReuse(slot, component, fieldInjector, environmentInjector, rawInputs, fieldInputs) {
|
|
735
807
|
const current = this.state();
|
|
736
808
|
if (current.phase !== 'empty' && current.ref.componentType === component) {
|
|
737
809
|
slot.insert(current.ref.hostView);
|
|
738
810
|
this.lastInputs = null;
|
|
739
811
|
this.state.set(Object.freeze({ phase: 'mounted', ref: current.ref, slot }));
|
|
740
|
-
this.pushInputs(rawInputs);
|
|
812
|
+
this.pushInputs(rawInputs, fieldInputs);
|
|
741
813
|
if (current.phase === 'detached')
|
|
742
814
|
this.replayFocus(current.focusSnapshot);
|
|
743
815
|
return;
|
|
@@ -755,7 +827,7 @@ class FieldComponentSlot {
|
|
|
755
827
|
injector: createWrapperAwareInjector(fieldInjector, slot.injector),
|
|
756
828
|
});
|
|
757
829
|
this.state.set(Object.freeze({ phase: 'mounted', ref, slot }));
|
|
758
|
-
this.pushInputs(rawInputs);
|
|
830
|
+
this.pushInputs(rawInputs, fieldInputs);
|
|
759
831
|
}
|
|
760
832
|
/**
|
|
761
833
|
* Push mapper inputs onto the mounted component. Per-key reference compare
|
|
@@ -773,7 +845,7 @@ class FieldComponentSlot {
|
|
|
773
845
|
* `NgForgeAction`. Mapper-as-contract is enforced by the `*_INPUTS`
|
|
774
846
|
* lockstep type assertions, not at runtime here.
|
|
775
847
|
*/
|
|
776
|
-
pushInputs(rawInputs) {
|
|
848
|
+
pushInputs(rawInputs, fieldInputs) {
|
|
777
849
|
const current = this.state();
|
|
778
850
|
if (current.phase !== 'mounted')
|
|
779
851
|
return;
|
|
@@ -785,6 +857,14 @@ class FieldComponentSlot {
|
|
|
785
857
|
continue;
|
|
786
858
|
current.ref.setInput(key, value);
|
|
787
859
|
}
|
|
860
|
+
// Side-channel forwarding for the `fieldInputs` bag — gated on
|
|
861
|
+
// `setInputIfDeclared` so leaf fields that don't declare it (most
|
|
862
|
+
// non-addon-aware components) skip silently rather than triggering
|
|
863
|
+
// NG0303 in dev. Pushed unconditionally on every emission since the bag
|
|
864
|
+
// changes identity when `rawInputs` does.
|
|
865
|
+
if (fieldInputs !== undefined) {
|
|
866
|
+
setInputIfDeclared(current.ref, 'fieldInputs', fieldInputs);
|
|
867
|
+
}
|
|
788
868
|
this.lastInputs = rawInputs;
|
|
789
869
|
}
|
|
790
870
|
/**
|
|
@@ -905,11 +985,13 @@ class DfFieldOutlet {
|
|
|
905
985
|
*/
|
|
906
986
|
wrappers = computed(() => resolveWrappers(this.dfFieldOutlet().fieldDef, this.defaultWrappersSignal?.(), this.wrapperAutoAssociations), { ...(ngDevMode ? { debugName: "wrappers" } : /* istanbul ignore next */ {}), equal: isSameWrapperChain });
|
|
907
987
|
/**
|
|
908
|
-
* `fieldInputs` bag handed to every wrapper in the chain
|
|
909
|
-
*
|
|
910
|
-
*
|
|
988
|
+
* `fieldInputs` bag handed to every wrapper in the chain AND forwarded to
|
|
989
|
+
* the innermost field component (when it declares `fieldInputs` as an
|
|
990
|
+
* input). Memoised on `rawInputs` identity so repeated emissions with the
|
|
991
|
+
* same underlying object return the same view and don't cascade OnPush
|
|
992
|
+
* re-evaluations.
|
|
911
993
|
*/
|
|
912
|
-
fieldInputs = computed(() =>
|
|
994
|
+
fieldInputs = computed(() => buildFieldInputs(this.rawInputs(), this.readonlyFieldCache, this.dfFieldOutlet().fieldDef.type), ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
913
995
|
defaultEnvInjector = inject(EnvironmentInjector);
|
|
914
996
|
/** Environment injector for the innermost field component — `[environmentInjector]` input takes precedence over the directive's own DI. */
|
|
915
997
|
fieldEnvInjector = computed(() => this.dfFieldOutletEnvironmentInjector() ?? this.defaultEnvInjector, ...(ngDevMode ? [{ debugName: "fieldEnvInjector" }] : /* istanbul ignore next */ []));
|
|
@@ -926,33 +1008,20 @@ class DfFieldOutlet {
|
|
|
926
1008
|
beforeRebuild: () => this.fieldComponent.detach(),
|
|
927
1009
|
renderInnermost: (slot) => {
|
|
928
1010
|
const resolved = this.dfFieldOutlet();
|
|
929
|
-
this.fieldComponent.mountOrReuse(slot, resolved.component, resolved.injector, this.fieldEnvInjector(), this.rawInputs());
|
|
1011
|
+
this.fieldComponent.mountOrReuse(slot, resolved.component, resolved.injector, this.fieldEnvInjector(), this.rawInputs(), this.fieldInputs());
|
|
930
1012
|
},
|
|
931
1013
|
});
|
|
932
|
-
// Push rawInputs
|
|
933
|
-
//
|
|
1014
|
+
// Push rawInputs (plus the wrapper-style fieldInputs bag) to the innermost
|
|
1015
|
+
// field on every change. The slot dedupes per-key, so the initial render's
|
|
1016
|
+
// synchronous push from `renderInnermost` and the effect's first fire
|
|
934
1017
|
// converge cleanly with no double-pushing.
|
|
935
|
-
explicitEffect([this.rawInputs], ([rawInputs]) => this.fieldComponent.pushInputs(rawInputs));
|
|
1018
|
+
explicitEffect([this.rawInputs, this.fieldInputs], ([rawInputs, fieldInputs]) => this.fieldComponent.pushInputs(rawInputs, fieldInputs));
|
|
936
1019
|
this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());
|
|
937
1020
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
// A FieldTree is a callable (() => FieldState). We expose it to wrappers via a narrow
|
|
941
|
-
// read-only view; raw FieldTree is still pushed to the innermost component for writes.
|
|
942
|
-
const readonlyField = fieldTreeCandidate && typeof fieldTreeCandidate === 'function'
|
|
943
|
-
? toReadonlyFieldTreeCached(this.readonlyFieldCache, fieldTreeCandidate)
|
|
944
|
-
: undefined;
|
|
945
|
-
// Shallow spread — relies on the mapper contract (see WrapperFieldInputs)
|
|
946
|
-
// that rawInputs are emitted as fresh snapshots, not mutated in place.
|
|
947
|
-
return {
|
|
948
|
-
...rawInputs,
|
|
949
|
-
field: readonlyField,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DfFieldOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
953
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", type: DfFieldOutlet, isStandalone: true, selector: "[dfFieldOutlet]", inputs: { dfFieldOutlet: { classPropertyName: "dfFieldOutlet", publicName: "dfFieldOutlet", isSignal: true, isRequired: true, transformFunction: null }, dfFieldOutletEnvironmentInjector: { classPropertyName: "dfFieldOutletEnvironmentInjector", publicName: "dfFieldOutletEnvironmentInjector", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1021
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1022
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: DfFieldOutlet, isStandalone: true, selector: "[dfFieldOutlet]", inputs: { dfFieldOutlet: { classPropertyName: "dfFieldOutlet", publicName: "dfFieldOutlet", isSignal: true, isRequired: true, transformFunction: null }, dfFieldOutletEnvironmentInjector: { classPropertyName: "dfFieldOutletEnvironmentInjector", publicName: "dfFieldOutletEnvironmentInjector", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
954
1023
|
}
|
|
955
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1024
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldOutlet, decorators: [{
|
|
956
1025
|
type: Directive,
|
|
957
1026
|
args: [{
|
|
958
1027
|
selector: '[dfFieldOutlet]',
|
|
@@ -1252,10 +1321,10 @@ class EventBus {
|
|
|
1252
1321
|
}
|
|
1253
1322
|
return this.pipeline$.pipe(filter((event) => event.type === eventType));
|
|
1254
1323
|
}
|
|
1255
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1256
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1324
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1325
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus });
|
|
1257
1326
|
}
|
|
1258
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1327
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus, decorators: [{
|
|
1259
1328
|
type: Injectable
|
|
1260
1329
|
}] });
|
|
1261
1330
|
|
|
@@ -3329,10 +3398,10 @@ class FunctionRegistryService {
|
|
|
3329
3398
|
this.clearAsyncValidators();
|
|
3330
3399
|
this.clearHttpValidators();
|
|
3331
3400
|
}
|
|
3332
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3333
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3401
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3402
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService });
|
|
3334
3403
|
}
|
|
3335
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3404
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService, decorators: [{
|
|
3336
3405
|
type: Injectable
|
|
3337
3406
|
}] });
|
|
3338
3407
|
|
|
@@ -3783,10 +3852,10 @@ class FieldContextRegistryService {
|
|
|
3783
3852
|
},
|
|
3784
3853
|
};
|
|
3785
3854
|
}
|
|
3786
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3787
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3855
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3856
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService });
|
|
3788
3857
|
}
|
|
3789
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3858
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService, decorators: [{
|
|
3790
3859
|
type: Injectable
|
|
3791
3860
|
}] });
|
|
3792
3861
|
|
|
@@ -4141,8 +4210,8 @@ class PageOrchestratorComponent {
|
|
|
4141
4210
|
}
|
|
4142
4211
|
return false;
|
|
4143
4212
|
}
|
|
4144
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4145
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4213
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4214
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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: [
|
|
4146
4215
|
{
|
|
4147
4216
|
provide: FIELD_SIGNAL_CONTEXT,
|
|
4148
4217
|
useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
|
|
@@ -4172,9 +4241,9 @@ class PageOrchestratorComponent {
|
|
|
4172
4241
|
}
|
|
4173
4242
|
}
|
|
4174
4243
|
}
|
|
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-
|
|
4244
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)]] });
|
|
4176
4245
|
}
|
|
4177
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.
|
|
4246
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
4178
4247
|
type: Component,
|
|
4179
4248
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
4180
4249
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -4678,10 +4747,10 @@ class ArrayItemRegistryService {
|
|
|
4678
4747
|
slot.clear();
|
|
4679
4748
|
this.slots.clear();
|
|
4680
4749
|
}
|
|
4681
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4682
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4750
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4751
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService });
|
|
4683
4752
|
}
|
|
4684
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4753
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService, decorators: [{
|
|
4685
4754
|
type: Injectable
|
|
4686
4755
|
}] });
|
|
4687
4756
|
|
|
@@ -4720,10 +4789,10 @@ class SchemaRegistryService {
|
|
|
4720
4789
|
}
|
|
4721
4790
|
return schema;
|
|
4722
4791
|
}
|
|
4723
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4724
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4792
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4793
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService });
|
|
4725
4794
|
}
|
|
4726
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4795
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService, decorators: [{
|
|
4727
4796
|
type: Injectable
|
|
4728
4797
|
}] });
|
|
4729
4798
|
|
|
@@ -8393,9 +8462,13 @@ function buildClassName(fieldDef) {
|
|
|
8393
8462
|
* @returns Record of input names to values
|
|
8394
8463
|
*/
|
|
8395
8464
|
function buildBaseInputs(fieldDef, defaultProps) {
|
|
8396
|
-
const { key, label, className, tabIndex, props, meta } = fieldDef;
|
|
8465
|
+
const { key, label, className, tabIndex, props, meta, addons } = fieldDef;
|
|
8397
8466
|
const inputs = {};
|
|
8398
|
-
// Always include key
|
|
8467
|
+
// Always include key — required by components for accessibility and identification.
|
|
8468
|
+
// `type` is intentionally NOT propagated to the field component (it's the
|
|
8469
|
+
// registry discriminant, not a component input); wrappers and addons read it
|
|
8470
|
+
// from `WrapperFieldInputs.type` which is injected at the buildFieldInputs()
|
|
8471
|
+
// boundary by `DfFieldOutlet`.
|
|
8399
8472
|
inputs['key'] = key;
|
|
8400
8473
|
if (label !== undefined) {
|
|
8401
8474
|
inputs['label'] = label;
|
|
@@ -8425,6 +8498,13 @@ function buildBaseInputs(fieldDef, defaultProps) {
|
|
|
8425
8498
|
if (meta !== undefined) {
|
|
8426
8499
|
inputs['meta'] = meta;
|
|
8427
8500
|
}
|
|
8501
|
+
// Forward addons. Field components that don't declare an `addons` input
|
|
8502
|
+
// signal (Tier 3 — toggle, checkbox, radio, slider) ignore this key via
|
|
8503
|
+
// Angular's `setInput` no-op-on-unknown behaviour, so it's safe to forward
|
|
8504
|
+
// unconditionally.
|
|
8505
|
+
if (addons !== undefined) {
|
|
8506
|
+
inputs['addons'] = addons;
|
|
8507
|
+
}
|
|
8428
8508
|
return inputs;
|
|
8429
8509
|
}
|
|
8430
8510
|
/**
|
|
@@ -9496,6 +9576,272 @@ function validateFormConfig(fields, registry, logger) {
|
|
|
9496
9576
|
}
|
|
9497
9577
|
}
|
|
9498
9578
|
|
|
9579
|
+
/**
|
|
9580
|
+
* Walk a field's addons; return the survivors plus a list of warnings for
|
|
9581
|
+
* anything dropped.
|
|
9582
|
+
*
|
|
9583
|
+
* Pure: receives the registries as arguments (so the function is reusable
|
|
9584
|
+
* outside Angular's DI — tests, server-side validation, etc.).
|
|
9585
|
+
*
|
|
9586
|
+
* @param field The field to validate.
|
|
9587
|
+
* @param fieldRegistry Snapshot of `FIELD_REGISTRY` map.
|
|
9588
|
+
* @param kindRegistry Snapshot of `ADDON_KIND_REGISTRY` map.
|
|
9589
|
+
* @param source Whether the config originated from JSON (lenient drop
|
|
9590
|
+
* of code-only kinds) or inline TypeScript.
|
|
9591
|
+
*/
|
|
9592
|
+
function validateFieldAddons(field, fieldRegistry, kindRegistry, source) {
|
|
9593
|
+
const warnings = [];
|
|
9594
|
+
const incoming = field.addons;
|
|
9595
|
+
if (!incoming?.length) {
|
|
9596
|
+
return { addons: [], warnings };
|
|
9597
|
+
}
|
|
9598
|
+
const fieldType = field.type;
|
|
9599
|
+
const fieldDef = fieldRegistry.get(fieldType);
|
|
9600
|
+
if (!fieldDef) {
|
|
9601
|
+
warnings.push({ type: 'unknown-field-type', fieldKey: field.key, fieldType });
|
|
9602
|
+
return { addons: [], warnings };
|
|
9603
|
+
}
|
|
9604
|
+
const addonSupport = fieldDef.addons;
|
|
9605
|
+
if (!addonSupport) {
|
|
9606
|
+
warnings.push({ type: 'field-type-no-addon-support', fieldKey: field.key, fieldType });
|
|
9607
|
+
return { addons: [], warnings };
|
|
9608
|
+
}
|
|
9609
|
+
const allowedSlots = addonSupport.slots;
|
|
9610
|
+
const allowedKinds = addonSupport.allowedKinds;
|
|
9611
|
+
const registeredKinds = Array.from(kindRegistry.keys());
|
|
9612
|
+
const survivors = [];
|
|
9613
|
+
for (const addon of incoming) {
|
|
9614
|
+
// Slot allowed?
|
|
9615
|
+
if (!allowedSlots.includes(addon.slot)) {
|
|
9616
|
+
warnings.push({
|
|
9617
|
+
type: 'unknown-slot',
|
|
9618
|
+
fieldKey: field.key,
|
|
9619
|
+
fieldType,
|
|
9620
|
+
slot: String(addon.slot),
|
|
9621
|
+
allowedSlots,
|
|
9622
|
+
});
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
// Kind registered? `AnyAddon.kind` is already a string-literal union via
|
|
9626
|
+
// DynamicFormAddonRegistry; no need to cast.
|
|
9627
|
+
const kindDef = kindRegistry.get(addon.kind);
|
|
9628
|
+
if (!kindDef) {
|
|
9629
|
+
warnings.push({
|
|
9630
|
+
type: 'unknown-kind',
|
|
9631
|
+
fieldKey: field.key,
|
|
9632
|
+
kind: addon.kind,
|
|
9633
|
+
registeredKinds,
|
|
9634
|
+
});
|
|
9635
|
+
continue;
|
|
9636
|
+
}
|
|
9637
|
+
// Kind whitelisted (if `allowedKinds` was set)?
|
|
9638
|
+
if (allowedKinds && !allowedKinds.includes(kindDef.kind)) {
|
|
9639
|
+
warnings.push({
|
|
9640
|
+
type: 'kind-not-allowed',
|
|
9641
|
+
fieldKey: field.key,
|
|
9642
|
+
fieldType,
|
|
9643
|
+
kind: kindDef.kind,
|
|
9644
|
+
allowedKinds,
|
|
9645
|
+
});
|
|
9646
|
+
continue;
|
|
9647
|
+
}
|
|
9648
|
+
// Code-only kind dropped from JSON-source configs. A kind opts out of
|
|
9649
|
+
// JSON-safety via `AddonKindDefinition.jsonSafe: false` (defaults to true).
|
|
9650
|
+
if (source === 'json' && kindDef.jsonSafe === false) {
|
|
9651
|
+
warnings.push({
|
|
9652
|
+
type: 'code-only-kind-in-json',
|
|
9653
|
+
fieldKey: field.key,
|
|
9654
|
+
kind: kindDef.kind,
|
|
9655
|
+
});
|
|
9656
|
+
continue;
|
|
9657
|
+
}
|
|
9658
|
+
// Multi-set XOR + JSON-source function stripping. Both runs operate on
|
|
9659
|
+
// a working copy via `ensureCopy()` so the original config isn't
|
|
9660
|
+
// mutated — the survivor pushed at the bottom is either the original
|
|
9661
|
+
// addon (no strips) or the sanitized copy.
|
|
9662
|
+
let sanitized = addon;
|
|
9663
|
+
let stripped = false;
|
|
9664
|
+
const ensureCopy = () => {
|
|
9665
|
+
if (!stripped)
|
|
9666
|
+
sanitized = { ...sanitized };
|
|
9667
|
+
stripped = true;
|
|
9668
|
+
};
|
|
9669
|
+
// Snapshot which click variants the original config declared BEFORE
|
|
9670
|
+
// any stripping. Computing variants AFTER function-strip would
|
|
9671
|
+
// underreport the multi-variant warning (`action` would silently
|
|
9672
|
+
// vanish from the reported list).
|
|
9673
|
+
const declaredVariants = ['preset', 'actionRef', 'action'].filter((k) => sanitized[k] !== undefined);
|
|
9674
|
+
// JSON-source function strip — inline `action`/`hidden`/`disabled`/
|
|
9675
|
+
// `loading` functions can't round-trip through serialization, so they
|
|
9676
|
+
// get dropped here with a warning. Inline-source configs keep them.
|
|
9677
|
+
if (source === 'json') {
|
|
9678
|
+
const dropReactiveFn = (key) => {
|
|
9679
|
+
if (typeof sanitized[key] === 'function') {
|
|
9680
|
+
ensureCopy();
|
|
9681
|
+
delete sanitized[key];
|
|
9682
|
+
warnings.push({
|
|
9683
|
+
type: 'code-only-action-in-json',
|
|
9684
|
+
fieldKey: field.key,
|
|
9685
|
+
reason: `function '${key}' on kind '${kindDef.kind}' (use a signal/observable in code, or omit in JSON)`,
|
|
9686
|
+
});
|
|
9687
|
+
}
|
|
9688
|
+
};
|
|
9689
|
+
if (typeof sanitized.action === 'function') {
|
|
9690
|
+
ensureCopy();
|
|
9691
|
+
delete sanitized.action;
|
|
9692
|
+
warnings.push({
|
|
9693
|
+
type: 'code-only-action-in-json',
|
|
9694
|
+
fieldKey: field.key,
|
|
9695
|
+
reason: `inline 'action' function on kind '${kindDef.kind}'`,
|
|
9696
|
+
});
|
|
9697
|
+
}
|
|
9698
|
+
dropReactiveFn('hidden');
|
|
9699
|
+
dropReactiveFn('disabled');
|
|
9700
|
+
dropReactiveFn('loading');
|
|
9701
|
+
}
|
|
9702
|
+
// Multi-set XOR — applies to BOTH inline and JSON sources. TypeScript
|
|
9703
|
+
// discriminated unions already reject multi-set at compile time, but
|
|
9704
|
+
// configs that bypass type-checking (loose `any`, runtime composition,
|
|
9705
|
+
// JSON parsing) still need defence-in-depth. Keep the first variant,
|
|
9706
|
+
// strip the rest; mirrors the runtime precedence
|
|
9707
|
+
// (preset > actionRef > action) so the chosen handler matches what
|
|
9708
|
+
// the dispatcher would have picked.
|
|
9709
|
+
if (declaredVariants.length > 1) {
|
|
9710
|
+
ensureCopy();
|
|
9711
|
+
for (const k of declaredVariants.slice(1))
|
|
9712
|
+
delete sanitized[k];
|
|
9713
|
+
warnings.push({
|
|
9714
|
+
type: 'code-only-action-in-json',
|
|
9715
|
+
fieldKey: field.key,
|
|
9716
|
+
reason: `multiple click variants on kind '${kindDef.kind}' (${declaredVariants.join(', ')}); kept '${declaredVariants[0]}'`,
|
|
9717
|
+
});
|
|
9718
|
+
}
|
|
9719
|
+
if (stripped) {
|
|
9720
|
+
survivors.push(sanitized);
|
|
9721
|
+
continue;
|
|
9722
|
+
}
|
|
9723
|
+
// Per-kind shape validator (throws on violation).
|
|
9724
|
+
if (kindDef.validate) {
|
|
9725
|
+
try {
|
|
9726
|
+
kindDef.validate(addon, field.key);
|
|
9727
|
+
}
|
|
9728
|
+
catch (error) {
|
|
9729
|
+
warnings.push({
|
|
9730
|
+
type: 'shape-violation',
|
|
9731
|
+
fieldKey: field.key,
|
|
9732
|
+
kind: kindDef.kind,
|
|
9733
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
9734
|
+
});
|
|
9735
|
+
continue;
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
survivors.push(addon);
|
|
9739
|
+
}
|
|
9740
|
+
return { addons: survivors, warnings };
|
|
9741
|
+
}
|
|
9742
|
+
/**
|
|
9743
|
+
* Recursively walk a field tree, validating each field's addons. Returns a
|
|
9744
|
+
* sanitized copy of the input field array plus accumulated warnings.
|
|
9745
|
+
*/
|
|
9746
|
+
function walkAndValidateAddons(fields, fieldRegistry, kindRegistry, source) {
|
|
9747
|
+
const allWarnings = [];
|
|
9748
|
+
const sanitized = fields.map((field) => {
|
|
9749
|
+
let next = field;
|
|
9750
|
+
if (field.addons?.length) {
|
|
9751
|
+
const { addons, warnings } = validateFieldAddons(field, fieldRegistry, kindRegistry, source);
|
|
9752
|
+
allWarnings.push(...warnings);
|
|
9753
|
+
next = { ...next, addons };
|
|
9754
|
+
}
|
|
9755
|
+
// Recurse into containers — every container field type carries its
|
|
9756
|
+
// children on a `fields` array.
|
|
9757
|
+
const children = next.fields;
|
|
9758
|
+
if (Array.isArray(children)) {
|
|
9759
|
+
const { fields: visited, warnings } = walkAndValidateAddons(children, fieldRegistry, kindRegistry, source);
|
|
9760
|
+
allWarnings.push(...warnings);
|
|
9761
|
+
next = { ...next, fields: visited };
|
|
9762
|
+
}
|
|
9763
|
+
return next;
|
|
9764
|
+
});
|
|
9765
|
+
return { fields: sanitized, warnings: allWarnings };
|
|
9766
|
+
}
|
|
9767
|
+
|
|
9768
|
+
/**
|
|
9769
|
+
* Stable structural fingerprint for an `AddonWarning`, used by the
|
|
9770
|
+
* FormStateManager dedup cache. Includes every distinguishing field
|
|
9771
|
+
* (`type`, `fieldKey`, `kind`/`slot`/`reason` as relevant) so warnings
|
|
9772
|
+
* for different fields or different kinds never collapse to one log
|
|
9773
|
+
* line — independent of the human-readable format which may change.
|
|
9774
|
+
*/
|
|
9775
|
+
function addonWarningKey(w) {
|
|
9776
|
+
switch (w.type) {
|
|
9777
|
+
case 'unknown-field-type':
|
|
9778
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
9779
|
+
case 'field-type-no-addon-support':
|
|
9780
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
9781
|
+
case 'unknown-slot':
|
|
9782
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.slot}`;
|
|
9783
|
+
case 'unknown-kind':
|
|
9784
|
+
return `${w.type}|${w.fieldKey}|${w.kind}`;
|
|
9785
|
+
case 'kind-not-allowed':
|
|
9786
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.kind}`;
|
|
9787
|
+
case 'shape-violation':
|
|
9788
|
+
return `${w.type}|${w.fieldKey}|${w.kind}|${w.reason}`;
|
|
9789
|
+
case 'code-only-kind-in-json':
|
|
9790
|
+
return `${w.type}|${w.fieldKey}|${w.kind}`;
|
|
9791
|
+
case 'code-only-action-in-json':
|
|
9792
|
+
return `${w.type}|${w.fieldKey}|${w.reason}`;
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* Render an `AddonWarning` to a developer-friendly message.
|
|
9797
|
+
*
|
|
9798
|
+
* Returns the bare message — the `[Dynamic Forms]` prefix is added by the
|
|
9799
|
+
* library's logger (`ConsoleLogger`) and by `DynamicFormError`. Pass the
|
|
9800
|
+
* result of this function directly into `logger.warn(...)` to get the
|
|
9801
|
+
* standard prefixed line; use `logAddonWarnings()` for direct
|
|
9802
|
+
* `console.warn` output (which prefixes manually).
|
|
9803
|
+
*
|
|
9804
|
+
* Messages are designed to be actionable: they enumerate the relevant set
|
|
9805
|
+
* (registered kinds, allowed slots) and suggest a likely fix.
|
|
9806
|
+
*/
|
|
9807
|
+
function formatAddonWarning(w) {
|
|
9808
|
+
switch (w.type) {
|
|
9809
|
+
case 'unknown-field-type':
|
|
9810
|
+
return `Field '${w.fieldKey}': unknown field type '${w.fieldType}'. Addons dropped.`;
|
|
9811
|
+
case 'field-type-no-addon-support':
|
|
9812
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): this field type does not declare addon support. Addons dropped. ` +
|
|
9813
|
+
`If this is incorrect, add 'addons: { slots: [...] }' to the field type registration.`);
|
|
9814
|
+
case 'unknown-slot':
|
|
9815
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): slot '${w.slot}' is not supported. ` +
|
|
9816
|
+
`Allowed: ${w.allowedSlots.join(', ')}. Addon dropped.`);
|
|
9817
|
+
case 'unknown-kind':
|
|
9818
|
+
return (`Field '${w.fieldKey}': addon kind '${w.kind}' is not registered. ` +
|
|
9819
|
+
`Did you forget withCustomAddon({ kind: '${w.kind}', ... })? ` +
|
|
9820
|
+
`Currently registered: ${w.registeredKinds.join(', ') || '(none)'}. Addon dropped.`);
|
|
9821
|
+
case 'kind-not-allowed':
|
|
9822
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): addon kind '${w.kind}' is not in the allowed list. ` +
|
|
9823
|
+
`Allowed: ${w.allowedKinds.join(', ')}. Addon dropped.`);
|
|
9824
|
+
case 'shape-violation':
|
|
9825
|
+
return `Field '${w.fieldKey}': addon kind '${w.kind}' shape invalid: ${w.reason}. Addon dropped.`;
|
|
9826
|
+
case 'code-only-kind-in-json':
|
|
9827
|
+
return `Field '${w.fieldKey}': addon kind '${w.kind}' is code-only and cannot be used with JSON-derived configs. Addon dropped.`;
|
|
9828
|
+
case 'code-only-action-in-json':
|
|
9829
|
+
return `Field '${w.fieldKey}': ${w.reason}. Use 'preset' or 'actionRef' instead. Addon dropped.`;
|
|
9830
|
+
}
|
|
9831
|
+
}
|
|
9832
|
+
|
|
9833
|
+
/**
|
|
9834
|
+
* Global registry of addon kinds.
|
|
9835
|
+
*
|
|
9836
|
+
* Populated by `withCustomAddon(...)` features and adapter feature helpers
|
|
9837
|
+
* (e.g., `withPrimeNGAddons()`). Read by `<df-addon-slot>` to resolve a
|
|
9838
|
+
* kind discriminant to its renderer component.
|
|
9839
|
+
*/
|
|
9840
|
+
const ADDON_KIND_REGISTRY = new InjectionToken('ADDON_KIND_REGISTRY', {
|
|
9841
|
+
providedIn: 'root',
|
|
9842
|
+
factory: () => new Map(),
|
|
9843
|
+
});
|
|
9844
|
+
|
|
9499
9845
|
/**
|
|
9500
9846
|
* Injection token for global value exclusion defaults.
|
|
9501
9847
|
*
|
|
@@ -10203,12 +10549,28 @@ class FormStateManager {
|
|
|
10203
10549
|
'Ensure DynamicForm assigns its input signals to FORM_STATE_DEPS.');
|
|
10204
10550
|
}
|
|
10205
10551
|
// Safe cast: DynamicForm populates FormStateDeps with its concrete signals.
|
|
10552
|
+
// `source` is optional — `DynamicForm` always populates it, but other
|
|
10553
|
+
// callers of `FormStateManager` (e.g., specs) may omit it.
|
|
10206
10554
|
return raw;
|
|
10207
10555
|
})();
|
|
10208
10556
|
/** Global value exclusion defaults. */
|
|
10209
10557
|
valueExclusionDefaults = inject(VALUE_EXCLUSION_DEFAULTS);
|
|
10210
10558
|
/** Field registry for loading components. */
|
|
10211
10559
|
fieldRegistry = injectFieldRegistry();
|
|
10560
|
+
/** Addon-kind registry — feeds the addon validator at config-setup time. */
|
|
10561
|
+
addonKindRegistry = inject(ADDON_KIND_REGISTRY);
|
|
10562
|
+
/**
|
|
10563
|
+
* Fingerprints of addon warnings emitted by the last config setup.
|
|
10564
|
+
* Used to skip re-logging warnings that already fired for the previous
|
|
10565
|
+
* config — avoids spam when the form swaps config repeatedly but the
|
|
10566
|
+
* addon issues haven't changed.
|
|
10567
|
+
*
|
|
10568
|
+
* Scoping: per-FormStateManager (i.e., per `<df-dynamic-form>` component
|
|
10569
|
+
* instance). Replaced wholesale on every config setup — bounded by the
|
|
10570
|
+
* current config's warning count, never grows unbounded across swaps.
|
|
10571
|
+
* A warning that disappears and later returns re-logs as expected.
|
|
10572
|
+
*/
|
|
10573
|
+
lastAddonWarningKeys = new Set();
|
|
10212
10574
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10213
10575
|
// State Machine
|
|
10214
10576
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -10896,11 +11258,35 @@ class FormStateManager {
|
|
|
10896
11258
|
}
|
|
10897
11259
|
createFormSetupFromConfig(fields, mode, registry) {
|
|
10898
11260
|
const normalizedFields = normalizeSimplifiedArrays(fields);
|
|
10899
|
-
//
|
|
10900
|
-
// into full ArrayField.fields and
|
|
11261
|
+
// Two validation passes run after normalization so simplified array templates
|
|
11262
|
+
// are already expanded into full ArrayField.fields and reachable during traversal:
|
|
11263
|
+
// 1. Structural — duplicate keys, unknown field types, invalid regex patterns.
|
|
11264
|
+
// 2. Addon pass below — strips invalid/unsupported addons with lenient warnings.
|
|
10901
11265
|
validateFormConfig(normalizedFields, registry, this.logger);
|
|
10902
|
-
|
|
10903
|
-
|
|
11266
|
+
// Addon pass — strip invalid / unsupported addon entries and log a warning
|
|
11267
|
+
// for each one dropped. Lenient by design: the form keeps rendering even
|
|
11268
|
+
// when a JSON-source config ships an addon the FE doesn't understand.
|
|
11269
|
+
//
|
|
11270
|
+
// Warnings are deduped across consecutive config setups: a warning that
|
|
11271
|
+
// already fired for the previous config is skipped, so swapping config
|
|
11272
|
+
// back and forth doesn't spam the console.
|
|
11273
|
+
const source = this.deps.source?.() ?? 'inline';
|
|
11274
|
+
const { fields: sanitizedFields, warnings: addonWarnings } = walkAndValidateAddons(normalizedFields, registry, this.addonKindRegistry, source);
|
|
11275
|
+
const nextKeys = new Set();
|
|
11276
|
+
for (const w of addonWarnings) {
|
|
11277
|
+
// Dedup uses a structural fingerprint so different fields with the
|
|
11278
|
+
// same warning category never collide; the rendered message is what
|
|
11279
|
+
// we log.
|
|
11280
|
+
const key = addonWarningKey(w);
|
|
11281
|
+
nextKeys.add(key);
|
|
11282
|
+
if (!this.lastAddonWarningKeys.has(key)) {
|
|
11283
|
+
this.logger.warn(formatAddonWarning(w));
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
this.lastAddonWarningKeys = nextKeys;
|
|
11287
|
+
const validatedFields = sanitizedFields;
|
|
11288
|
+
const flattenedFields = this.fieldProcessors.memoizedFlattenFields(validatedFields, registry);
|
|
11289
|
+
const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(validatedFields, registry);
|
|
10904
11290
|
const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
|
|
10905
11291
|
const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
|
|
10906
11292
|
const fieldsToRender = mode === 'paged' ? [] : flattenedFieldsForRendering;
|
|
@@ -10928,10 +11314,10 @@ class FormStateManager {
|
|
|
10928
11314
|
ngOnDestroy() {
|
|
10929
11315
|
this.machine.dispatch({ type: Action.Destroy });
|
|
10930
11316
|
}
|
|
10931
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
10932
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
11317
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
11318
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager });
|
|
10933
11319
|
}
|
|
10934
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
11320
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager, decorators: [{
|
|
10935
11321
|
type: Injectable
|
|
10936
11322
|
}], ctorParameters: () => [] });
|
|
10937
11323
|
|
|
@@ -14015,7 +14401,11 @@ function isDynamicFormFeature(value) {
|
|
|
14015
14401
|
Array.isArray(value.ɵproviders));
|
|
14016
14402
|
}
|
|
14017
14403
|
/**
|
|
14018
|
-
* Helper to create a feature with proper typing
|
|
14404
|
+
* Helper to create a feature with proper typing.
|
|
14405
|
+
*
|
|
14406
|
+
* `kind` is the discriminant compared by string equality at the
|
|
14407
|
+
* provider-resolution site (and in feature-specific type guards). Each
|
|
14408
|
+
* feature module owns its own kind literal — no central registry.
|
|
14019
14409
|
*/
|
|
14020
14410
|
function createFeature(kind, providers) {
|
|
14021
14411
|
return {
|
|
@@ -14553,6 +14943,30 @@ function warnAboutWildcardDependencies(logger, entries, fieldCount) {
|
|
|
14553
14943
|
}
|
|
14554
14944
|
}
|
|
14555
14945
|
|
|
14946
|
+
/**
|
|
14947
|
+
* Multi-provider token populated by `provideAddonActions({...})` calls.
|
|
14948
|
+
* `ADDON_ACTION_REGISTRY` aggregates these into a single name → handler map.
|
|
14949
|
+
*
|
|
14950
|
+
* @internal
|
|
14951
|
+
*/
|
|
14952
|
+
const ADDON_ACTION_HANDLERS = new InjectionToken('ADDON_ACTION_HANDLERS');
|
|
14953
|
+
/**
|
|
14954
|
+
* Form-scoped name → handler map. Read by adapter button kinds at click
|
|
14955
|
+
* time for `actionRef: 'name'` lookups. Provided at form scope (not root)
|
|
14956
|
+
* because `ADDON_ACTION_HANDLERS` is form-scoped via `provideAddonActions`.
|
|
14957
|
+
*/
|
|
14958
|
+
const ADDON_ACTION_REGISTRY = new InjectionToken('ADDON_ACTION_REGISTRY');
|
|
14959
|
+
function createAddonActionRegistry() {
|
|
14960
|
+
const sources = inject(ADDON_ACTION_HANDLERS, { optional: true }) ?? [];
|
|
14961
|
+
const map = new Map();
|
|
14962
|
+
for (const handlers of sources) {
|
|
14963
|
+
for (const [name, fn] of Object.entries(handlers)) {
|
|
14964
|
+
map.set(name, fn);
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14967
|
+
return map;
|
|
14968
|
+
}
|
|
14969
|
+
|
|
14556
14970
|
/**
|
|
14557
14971
|
* Always-on providers for any DynamicForm: state machine, registries, signal-context tokens.
|
|
14558
14972
|
* Pure form bootstrap — no derivation, HTTP, or async machinery.
|
|
@@ -14567,7 +14981,7 @@ function coreProviders() {
|
|
|
14567
14981
|
FunctionRegistryService,
|
|
14568
14982
|
{
|
|
14569
14983
|
provide: FORM_STATE_DEPS,
|
|
14570
|
-
useFactory: () => ({ config: null, formOptions: null, value: null }),
|
|
14984
|
+
useFactory: () => ({ config: null, formOptions: null, value: null, source: null }),
|
|
14571
14985
|
},
|
|
14572
14986
|
FormStateManager,
|
|
14573
14987
|
{
|
|
@@ -14603,6 +15017,11 @@ function coreProviders() {
|
|
|
14603
15017
|
deps: [FormStateManager],
|
|
14604
15018
|
},
|
|
14605
15019
|
{ provide: DEPRECATION_WARNING_TRACKER, useFactory: createWarningTracker },
|
|
15020
|
+
// ADDON_ACTION_REGISTRY must live at form scope (not root) because the
|
|
15021
|
+
// multi-provider source `ADDON_ACTION_HANDLERS` is contributed by
|
|
15022
|
+
// `provideAddonActions(...)` features passed to `provideDynamicForm(...)`
|
|
15023
|
+
// — those entries are only visible inside the form's own injector tree.
|
|
15024
|
+
{ provide: ADDON_ACTION_REGISTRY, useFactory: createAddonActionRegistry },
|
|
14606
15025
|
];
|
|
14607
15026
|
}
|
|
14608
15027
|
/**
|
|
@@ -14794,13 +15213,90 @@ class EventDispatcher {
|
|
|
14794
15213
|
disconnect() {
|
|
14795
15214
|
this.bus = undefined;
|
|
14796
15215
|
}
|
|
14797
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
14798
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
15216
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
15217
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher });
|
|
15218
|
+
}
|
|
15219
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher, decorators: [{
|
|
15220
|
+
type: Injectable
|
|
15221
|
+
}] });
|
|
15222
|
+
|
|
15223
|
+
/**
|
|
15224
|
+
* Marks an `<ng-template>` projected into `<df-dynamic-form>` content as
|
|
15225
|
+
* available to template addons by name.
|
|
15226
|
+
*
|
|
15227
|
+
* The host form collects all `DfTemplate` instances via `contentChildren`
|
|
15228
|
+
* and exposes them through {@link DF_FIELD_TEMPLATES}, where the template
|
|
15229
|
+
* addon kind looks them up by `templateKey` at render time.
|
|
15230
|
+
*
|
|
15231
|
+
* @example
|
|
15232
|
+
* ```html
|
|
15233
|
+
* <df-dynamic-form [config]="config">
|
|
15234
|
+
* <ng-template dfTemplate="searchIcon" let-field>
|
|
15235
|
+
* <my-icon [field]="field" />
|
|
15236
|
+
* </ng-template>
|
|
15237
|
+
* </df-dynamic-form>
|
|
15238
|
+
* ```
|
|
15239
|
+
*
|
|
15240
|
+
* In the field config:
|
|
15241
|
+
* ```ts
|
|
15242
|
+
* { slot: 'prefix', kind: 'template', templateKey: 'searchIcon' }
|
|
15243
|
+
* ```
|
|
15244
|
+
*/
|
|
15245
|
+
class DfTemplate {
|
|
15246
|
+
/** Name this template registers under — referenced from `TemplateAddon.templateKey`. */
|
|
15247
|
+
dfTemplate = input.required(...(ngDevMode ? [{ debugName: "dfTemplate" }] : /* istanbul ignore next */ []));
|
|
15248
|
+
/** The wrapped template, captured for registration and `NgTemplateOutlet` rendering. */
|
|
15249
|
+
templateRef = inject(TemplateRef);
|
|
15250
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
15251
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: DfTemplate, isStandalone: true, selector: "ng-template[dfTemplate]", inputs: { dfTemplate: { classPropertyName: "dfTemplate", publicName: "dfTemplate", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["dfTemplate"], ngImport: i0 });
|
|
14799
15252
|
}
|
|
14800
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15253
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfTemplate, decorators: [{
|
|
15254
|
+
type: Directive,
|
|
15255
|
+
args: [{
|
|
15256
|
+
selector: 'ng-template[dfTemplate]',
|
|
15257
|
+
exportAs: 'dfTemplate',
|
|
15258
|
+
}]
|
|
15259
|
+
}], propDecorators: { dfTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "dfTemplate", required: true }] }] } });
|
|
15260
|
+
/**
|
|
15261
|
+
* Holder service published at the `<df-dynamic-form>` component scope.
|
|
15262
|
+
* `DynamicForm` collects projected `<ng-template dfTemplate="...">` via
|
|
15263
|
+
* `contentChildren(DfTemplate)` and pushes the resulting map through
|
|
15264
|
+
* `DfFieldTemplateRegistry.set(...)`. The {@link DF_FIELD_TEMPLATES}
|
|
15265
|
+
* injection token is provided as a factory that returns this service's
|
|
15266
|
+
* `map` signal, so template-addon components can resolve `templateKey`
|
|
15267
|
+
* without coupling to the host form's generic class.
|
|
15268
|
+
*/
|
|
15269
|
+
class DfFieldTemplateRegistry {
|
|
15270
|
+
_map = signal(new Map(), ...(ngDevMode ? [{ debugName: "_map" }] : /* istanbul ignore next */ []));
|
|
15271
|
+
/** Reactive view consumed by the `template` addon kind via DI. */
|
|
15272
|
+
map = this._map.asReadonly();
|
|
15273
|
+
/** Replace the registry contents — called from `DynamicForm` when projected templates change. */
|
|
15274
|
+
set(value) {
|
|
15275
|
+
this._map.set(value);
|
|
15276
|
+
}
|
|
15277
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
15278
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry });
|
|
15279
|
+
}
|
|
15280
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry, decorators: [{
|
|
14801
15281
|
type: Injectable
|
|
14802
15282
|
}] });
|
|
14803
15283
|
|
|
15284
|
+
/**
|
|
15285
|
+
* Map of named `TemplateRef`s collected by `<df-dynamic-form>` from
|
|
15286
|
+
* `<ng-template dfTemplate="...">` children projected into its content.
|
|
15287
|
+
* The directive selector is `dfTemplate` (see `DfTemplate`); the input
|
|
15288
|
+
* value is the name addons reference via `templateKey`.
|
|
15289
|
+
*
|
|
15290
|
+
* The `template` addon kind looks up its `templateKey` in this map at
|
|
15291
|
+
* render time. Exposed as a `Signal` so projections can be added or
|
|
15292
|
+
* removed reactively (e.g., when the host conditionally projects).
|
|
15293
|
+
*
|
|
15294
|
+
* Provided at the form-component level (not root) so multiple
|
|
15295
|
+
* `<df-dynamic-form>` instances on the same page have independent
|
|
15296
|
+
* template scopes.
|
|
15297
|
+
*/
|
|
15298
|
+
const DF_FIELD_TEMPLATES = new InjectionToken('DF_FIELD_TEMPLATES');
|
|
15299
|
+
|
|
14804
15300
|
/**
|
|
14805
15301
|
* Dynamic form component — renders a form based on configuration.
|
|
14806
15302
|
* Delegates state management to `FormStateManager`.
|
|
@@ -14820,6 +15316,13 @@ class DynamicForm {
|
|
|
14820
15316
|
formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : /* istanbul ignore next */ []));
|
|
14821
15317
|
/** Form values for two-way data binding. */
|
|
14822
15318
|
value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
15319
|
+
/**
|
|
15320
|
+
* How lenient the addon validator is on `config.fields[...].addons`.
|
|
15321
|
+
* `'inline'` (default) keeps everything; `'json'` drops code-only kinds
|
|
15322
|
+
* with a warning. Read at form init and on every config swap — change
|
|
15323
|
+
* `source` AND the config to force re-validation mid-flight.
|
|
15324
|
+
*/
|
|
15325
|
+
source = input('inline', ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
|
|
14823
15326
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14824
15327
|
// Dependencies
|
|
14825
15328
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -14829,6 +15332,12 @@ class DynamicForm {
|
|
|
14829
15332
|
eventBus = inject(EventBus);
|
|
14830
15333
|
logger = inject(DynamicFormLogger);
|
|
14831
15334
|
dispatcher = inject(EventDispatcher, { optional: true });
|
|
15335
|
+
templateRegistry = inject(DfFieldTemplateRegistry);
|
|
15336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15337
|
+
// Projected templates — collected for the `template` addon kind via DI.
|
|
15338
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15339
|
+
/** All `<ng-template dfTemplate="...">` instances projected into this form. */
|
|
15340
|
+
_projectedTemplates = contentChildren(DfTemplate, ...(ngDevMode ? [{ debugName: "_projectedTemplates" }] : /* istanbul ignore next */ []));
|
|
14832
15341
|
/** State manager that owns all form state. Initialized via connectDeps() to guarantee
|
|
14833
15342
|
* FORM_STATE_DEPS is populated before FormStateManager is injected. */
|
|
14834
15343
|
stateManager = this.connectDeps();
|
|
@@ -14981,6 +15490,15 @@ class DynamicForm {
|
|
|
14981
15490
|
}, { injector: this.injector });
|
|
14982
15491
|
}
|
|
14983
15492
|
});
|
|
15493
|
+
// Mirror projected `<ng-template dfTemplate="...">` into the template
|
|
15494
|
+
// registry so `template` addons can resolve `templateKey` via DI.
|
|
15495
|
+
explicitEffect([this._projectedTemplates], ([templates]) => {
|
|
15496
|
+
const map = new Map();
|
|
15497
|
+
for (const t of templates) {
|
|
15498
|
+
map.set(t.dfTemplate(), t.templateRef);
|
|
15499
|
+
}
|
|
15500
|
+
this.templateRegistry.set(map);
|
|
15501
|
+
});
|
|
14984
15502
|
}
|
|
14985
15503
|
/**
|
|
14986
15504
|
* Populates FORM_STATE_DEPS with this component's input signals, then injects
|
|
@@ -14993,6 +15511,7 @@ class DynamicForm {
|
|
|
14993
15511
|
deps.config = this.config;
|
|
14994
15512
|
deps.formOptions = this.formOptions;
|
|
14995
15513
|
deps.value = this.value;
|
|
15514
|
+
deps.source = this.source;
|
|
14996
15515
|
return inject((FormStateManager));
|
|
14997
15516
|
}
|
|
14998
15517
|
setupEventHandlers() {
|
|
@@ -15008,8 +15527,12 @@ class DynamicForm {
|
|
|
15008
15527
|
error: (err) => this.logger.error('Submission handler error', err),
|
|
15009
15528
|
});
|
|
15010
15529
|
}
|
|
15011
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
15012
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
15530
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15531
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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 }, source: { classPropertyName: "source", publicName: "source", 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: [
|
|
15532
|
+
provideDynamicFormDI(),
|
|
15533
|
+
DfFieldTemplateRegistry,
|
|
15534
|
+
{ provide: DF_FIELD_TEMPLATES, useFactory: () => inject(DfFieldTemplateRegistry).map },
|
|
15535
|
+
], queries: [{ propertyName: "_projectedTemplates", predicate: DfTemplate, isSignal: true }], ngImport: i0, template: `
|
|
15013
15536
|
@if (shouldRender()) {
|
|
15014
15537
|
@switch (formModeDetection().mode) {
|
|
15015
15538
|
@case ('paged') {
|
|
@@ -15031,9 +15554,9 @@ class DynamicForm {
|
|
|
15031
15554
|
}
|
|
15032
15555
|
}
|
|
15033
15556
|
}
|
|
15034
|
-
`, isInline: true, styles: ["
|
|
15557
|
+
`, isInline: true, styles: [".df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);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, .5rem);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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-sm, .5rem)}.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, 12),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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-md, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-lg, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-xl, .5rem)}.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, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--df-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{gap:0}\n"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }, { kind: "component", type: PageOrchestratorComponent, selector: "div[page-orchestrator]", inputs: ["pageFields", "form", "fieldSignalContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15035
15558
|
}
|
|
15036
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15559
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicForm, decorators: [{
|
|
15037
15560
|
type: Component,
|
|
15038
15561
|
args: [{ selector: 'form[dynamic-form]', imports: [DfFieldOutlet, PageOrchestratorComponent], template: `
|
|
15039
15562
|
@if (shouldRender()) {
|
|
@@ -15057,7 +15580,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
15057
15580
|
}
|
|
15058
15581
|
}
|
|
15059
15582
|
}
|
|
15060
|
-
`, providers: [
|
|
15583
|
+
`, providers: [
|
|
15584
|
+
provideDynamicFormDI(),
|
|
15585
|
+
DfFieldTemplateRegistry,
|
|
15586
|
+
{ provide: DF_FIELD_TEMPLATES, useFactory: () => inject(DfFieldTemplateRegistry).map },
|
|
15587
|
+
], host: {
|
|
15061
15588
|
class: 'df-dynamic-form df-form',
|
|
15062
15589
|
novalidate: '', // Disable browser validation - Angular Signal Forms handles validation
|
|
15063
15590
|
'[class.disabled]': 'disabled()',
|
|
@@ -15065,8 +15592,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
15065
15592
|
'[class.df-form-non-paged]': 'formModeDetection().mode === "non-paged"',
|
|
15066
15593
|
'[attr.data-form-mode]': 'formModeDetection().mode',
|
|
15067
15594
|
'(submit)': 'onNativeSubmit($event)',
|
|
15068
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["
|
|
15069
|
-
}], 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"] }] } });
|
|
15595
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);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, .5rem);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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-sm, .5rem)}.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, 12),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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-md, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-lg, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-xl, .5rem)}.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, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--df-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{gap:0}\n"] }]
|
|
15596
|
+
}], 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"] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], _projectedTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => DfTemplate), { isSignal: true }] }], 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"] }] } });
|
|
15070
15597
|
/**
|
|
15071
15598
|
* Recursively counts container fields (page, row, group, array, wrapper) in a field tree.
|
|
15072
15599
|
* Descends into container children including array item templates to ensure
|
|
@@ -15096,6 +15623,147 @@ function countContainersRecursive(fields) {
|
|
|
15096
15623
|
return count;
|
|
15097
15624
|
}
|
|
15098
15625
|
|
|
15626
|
+
/**
|
|
15627
|
+
* Cache for resolved addon-kind components.
|
|
15628
|
+
*
|
|
15629
|
+
* Provided per-form by `provideDynamicFormDI()` so two `<df-dynamic-form>`
|
|
15630
|
+
* instances with conflicting `loadComponent` factories for the same kind
|
|
15631
|
+
* never share a stale entry. Has no `providedIn: 'root'` factory — the
|
|
15632
|
+
* registry consumer is always inside a form scope, and a root fallback
|
|
15633
|
+
* would only hide misconfiguration (an SSR cross-request hit if a future
|
|
15634
|
+
* code path forgot to provide it locally).
|
|
15635
|
+
*
|
|
15636
|
+
* Mirrors `COMPONENT_CACHE` (field types) and `WRAPPER_COMPONENT_CACHE`
|
|
15637
|
+
* (wrappers).
|
|
15638
|
+
*
|
|
15639
|
+
* @internal
|
|
15640
|
+
*/
|
|
15641
|
+
const ADDON_KIND_COMPONENT_CACHE = new InjectionToken('ADDON_KIND_COMPONENT_CACHE');
|
|
15642
|
+
/**
|
|
15643
|
+
* Injection function for accessing the addon kind registry.
|
|
15644
|
+
*
|
|
15645
|
+
* Usage parallels {@link injectFieldRegistry}: lookup, lazy load, cache.
|
|
15646
|
+
* Must be called within an injection context.
|
|
15647
|
+
*
|
|
15648
|
+
* @example
|
|
15649
|
+
* ```typescript
|
|
15650
|
+
* const addonKinds = injectAddonKindRegistry();
|
|
15651
|
+
* const Component = await addonKinds.loadKindComponent('prime-icon');
|
|
15652
|
+
* ```
|
|
15653
|
+
*/
|
|
15654
|
+
function injectAddonKindRegistry() {
|
|
15655
|
+
const registry = inject(ADDON_KIND_REGISTRY);
|
|
15656
|
+
const componentCache = inject(ADDON_KIND_COMPONENT_CACHE);
|
|
15657
|
+
return {
|
|
15658
|
+
/** Look up a kind definition by its discriminant. */
|
|
15659
|
+
getKind(kind) {
|
|
15660
|
+
return registry.get(kind);
|
|
15661
|
+
},
|
|
15662
|
+
/** Whether a kind is currently registered. */
|
|
15663
|
+
hasKind(kind) {
|
|
15664
|
+
return registry.has(kind);
|
|
15665
|
+
},
|
|
15666
|
+
/** All registered kind names — useful for actionable validator warnings. */
|
|
15667
|
+
getKindNames() {
|
|
15668
|
+
return Array.from(registry.keys());
|
|
15669
|
+
},
|
|
15670
|
+
/**
|
|
15671
|
+
* Resolve a kind to its renderer component. Caches on first resolution.
|
|
15672
|
+
*
|
|
15673
|
+
* @throws DynamicFormError when the kind is not registered.
|
|
15674
|
+
*/
|
|
15675
|
+
async loadKindComponent(kind) {
|
|
15676
|
+
const definition = registry.get(kind);
|
|
15677
|
+
if (!definition) {
|
|
15678
|
+
const known = Array.from(registry.keys()).join(', ') || '(none)';
|
|
15679
|
+
throw new DynamicFormError(`Addon kind '${kind}' is not registered. Currently registered kinds: ${known}.`);
|
|
15680
|
+
}
|
|
15681
|
+
const cached = componentCache.get(kind);
|
|
15682
|
+
if (cached) {
|
|
15683
|
+
return cached;
|
|
15684
|
+
}
|
|
15685
|
+
try {
|
|
15686
|
+
const component = resolveDefaultExport(await definition.loadComponent());
|
|
15687
|
+
if (component) {
|
|
15688
|
+
componentCache.set(kind, component);
|
|
15689
|
+
return component;
|
|
15690
|
+
}
|
|
15691
|
+
throw new DynamicFormError(`Addon kind '${kind}' loadComponent resolved to null.`);
|
|
15692
|
+
}
|
|
15693
|
+
catch (error) {
|
|
15694
|
+
if (error instanceof DynamicFormError)
|
|
15695
|
+
throw error;
|
|
15696
|
+
throw new DynamicFormError(`Failed to load component for addon kind '${kind}': ${error instanceof Error ? error.message : String(error)}`);
|
|
15697
|
+
}
|
|
15698
|
+
},
|
|
15699
|
+
/**
|
|
15700
|
+
* Synchronous lookup of an already-loaded kind component, or undefined.
|
|
15701
|
+
* Returns `undefined` when the kind isn't registered in this scope even
|
|
15702
|
+
* if a prior scope cached a component under the same name — prevents
|
|
15703
|
+
* cross-form cache hits when registrations diverge.
|
|
15704
|
+
*/
|
|
15705
|
+
getLoadedKindComponent(kind) {
|
|
15706
|
+
if (!registry.has(kind))
|
|
15707
|
+
return undefined;
|
|
15708
|
+
return componentCache.get(kind);
|
|
15709
|
+
},
|
|
15710
|
+
/** Direct read-only access to the registry map. */
|
|
15711
|
+
get raw() {
|
|
15712
|
+
return registry;
|
|
15713
|
+
},
|
|
15714
|
+
};
|
|
15715
|
+
}
|
|
15716
|
+
|
|
15717
|
+
/**
|
|
15718
|
+
* Built-in addon kinds shipped by core.
|
|
15719
|
+
*
|
|
15720
|
+
* Universally adapter-independent: `text`, `template`, `component`. Adapters
|
|
15721
|
+
* register their own kinds (e.g., `prime-icon`, `prime-button`) via
|
|
15722
|
+
* `withCustomAddon(...)` features.
|
|
15723
|
+
*/
|
|
15724
|
+
const BUILT_IN_ADDON_KINDS = [
|
|
15725
|
+
{
|
|
15726
|
+
kind: 'text',
|
|
15727
|
+
loadComponent: () => Promise.resolve().then(function () { return textAddon_component; }).then((m) => m.TextAddonComponent),
|
|
15728
|
+
validate: (addon, fieldKey) => {
|
|
15729
|
+
const text = addon.text;
|
|
15730
|
+
// Validates structural presence: rejects missing / null / non-string-non-callable,
|
|
15731
|
+
// and rejects literal empty strings. DynamicText values that resolve to empty
|
|
15732
|
+
// at runtime (signal/observable yielding '') are NOT caught here — they're
|
|
15733
|
+
// legal as far as the config is concerned, just render an empty span.
|
|
15734
|
+
if (text === undefined || text === null) {
|
|
15735
|
+
throw new DynamicFormError(`Addon kind 'text' requires a 'text' field (field: '${fieldKey}').`);
|
|
15736
|
+
}
|
|
15737
|
+
if (typeof text === 'string' && text.length === 0) {
|
|
15738
|
+
throw new DynamicFormError(`Addon kind 'text' requires a non-empty 'text' string (field: '${fieldKey}').`);
|
|
15739
|
+
}
|
|
15740
|
+
},
|
|
15741
|
+
},
|
|
15742
|
+
{
|
|
15743
|
+
kind: 'template',
|
|
15744
|
+
loadComponent: () => Promise.resolve().then(function () { return templateAddon_component; }).then((m) => m.TemplateAddonComponent),
|
|
15745
|
+
validate: (addon, fieldKey) => {
|
|
15746
|
+
const key = addon.templateKey;
|
|
15747
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
15748
|
+
throw new DynamicFormError(`Addon kind 'template' requires a non-empty 'templateKey' string (field: '${fieldKey}').`);
|
|
15749
|
+
}
|
|
15750
|
+
},
|
|
15751
|
+
},
|
|
15752
|
+
{
|
|
15753
|
+
kind: 'component',
|
|
15754
|
+
// The shape carries a function loader — not serialisable; dropped by the
|
|
15755
|
+
// validator when the form config originated from JSON.
|
|
15756
|
+
jsonSafe: false,
|
|
15757
|
+
loadComponent: () => Promise.resolve().then(function () { return componentAddon_component; }).then((m) => m.ComponentAddonComponent),
|
|
15758
|
+
validate: (addon, fieldKey) => {
|
|
15759
|
+
const loader = addon.component;
|
|
15760
|
+
if (typeof loader !== 'function') {
|
|
15761
|
+
throw new DynamicFormError(`Addon kind 'component' requires a 'component' loader function (field: '${fieldKey}').`);
|
|
15762
|
+
}
|
|
15763
|
+
},
|
|
15764
|
+
},
|
|
15765
|
+
];
|
|
15766
|
+
|
|
15099
15767
|
/**
|
|
15100
15768
|
* No-op logger for when no logger is provided.
|
|
15101
15769
|
* Used as fallback in button logic evaluation to avoid breaking downstream packages.
|
|
@@ -15658,35 +16326,35 @@ const BUILT_IN_FIELDS = [
|
|
|
15658
16326
|
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
15659
16327
|
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
15660
16328
|
// renders the flex/grid layout via RowWrapperComponent.
|
|
15661
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
16329
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
|
|
15662
16330
|
mapper: rowFieldMapper,
|
|
15663
16331
|
valueHandling: 'flatten',
|
|
15664
16332
|
...CONTAINER_FIELD_TYPES_BASE,
|
|
15665
16333
|
},
|
|
15666
16334
|
{
|
|
15667
16335
|
name: 'group',
|
|
15668
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-
|
|
16336
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs'),
|
|
15669
16337
|
mapper: groupFieldMapper,
|
|
15670
16338
|
valueHandling: 'include',
|
|
15671
16339
|
...CONTAINER_FIELD_TYPES_BASE,
|
|
15672
16340
|
},
|
|
15673
16341
|
{
|
|
15674
16342
|
name: 'array',
|
|
15675
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-
|
|
16343
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs'),
|
|
15676
16344
|
mapper: arrayFieldMapper,
|
|
15677
16345
|
valueHandling: 'include',
|
|
15678
16346
|
...CONTAINER_FIELD_TYPES_BASE,
|
|
15679
16347
|
},
|
|
15680
16348
|
{
|
|
15681
16349
|
name: 'page',
|
|
15682
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
16350
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs'),
|
|
15683
16351
|
mapper: pageFieldMapper,
|
|
15684
16352
|
valueHandling: 'flatten',
|
|
15685
16353
|
...CONTAINER_FIELD_TYPES_BASE,
|
|
15686
16354
|
},
|
|
15687
16355
|
{
|
|
15688
16356
|
name: 'text',
|
|
15689
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
16357
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs'),
|
|
15690
16358
|
mapper: textFieldMapper,
|
|
15691
16359
|
valueHandling: 'exclude',
|
|
15692
16360
|
...DISPLAY_FIELD_TYPES_BASE,
|
|
@@ -15698,7 +16366,7 @@ const BUILT_IN_FIELDS = [
|
|
|
15698
16366
|
},
|
|
15699
16367
|
{
|
|
15700
16368
|
name: 'container',
|
|
15701
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
16369
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
|
|
15702
16370
|
mapper: containerFieldMapper,
|
|
15703
16371
|
valueHandling: 'flatten',
|
|
15704
16372
|
...CONTAINER_FIELD_TYPES_BASE,
|
|
@@ -15707,14 +16375,23 @@ const BUILT_IN_FIELDS = [
|
|
|
15707
16375
|
const BUILT_IN_WRAPPERS = [
|
|
15708
16376
|
{
|
|
15709
16377
|
wrapperName: 'css',
|
|
15710
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-
|
|
16378
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs'),
|
|
15711
16379
|
},
|
|
15712
16380
|
{
|
|
15713
16381
|
wrapperName: 'row',
|
|
15714
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-row-wrapper.component-
|
|
16382
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs'),
|
|
15715
16383
|
},
|
|
15716
16384
|
];
|
|
15717
16385
|
|
|
16386
|
+
/**
|
|
16387
|
+
* Multi-provider token used by `withCustomAddon(...)` features to contribute
|
|
16388
|
+
* addon kind definitions. `provideDynamicForm` aggregates all values into
|
|
16389
|
+
* the form-scoped `ADDON_KIND_REGISTRY` map.
|
|
16390
|
+
*
|
|
16391
|
+
* @internal
|
|
16392
|
+
*/
|
|
16393
|
+
const ADDON_KIND_DEFINITIONS = new InjectionToken('ADDON_KIND_DEFINITIONS');
|
|
16394
|
+
|
|
15718
16395
|
/**
|
|
15719
16396
|
* Bundle wrapper registrations into a single object that can be passed to
|
|
15720
16397
|
* `provideDynamicForm(...)`.
|
|
@@ -15831,8 +16508,6 @@ function provideDynamicForm(...items) {
|
|
|
15831
16508
|
const defaultLoggerProvider = hasLoggerFeature ? [] : [{ provide: DynamicFormLogger, useValue: new ConsoleLogger() }];
|
|
15832
16509
|
return makeEnvironmentProviders([
|
|
15833
16510
|
...defaultLoggerProvider,
|
|
15834
|
-
// Always provide default Signal Forms classes (ng-touched, ng-invalid, etc.)
|
|
15835
|
-
provideSignalFormsConfig({ classes: NG_STATUS_CLASSES }),
|
|
15836
16511
|
{
|
|
15837
16512
|
provide: FIELD_REGISTRY,
|
|
15838
16513
|
useFactory: () => {
|
|
@@ -15883,6 +16558,31 @@ function provideDynamicForm(...items) {
|
|
|
15883
16558
|
return autoMap;
|
|
15884
16559
|
},
|
|
15885
16560
|
},
|
|
16561
|
+
// Form-scoped addon kind registry. Built from BUILT_IN_ADDON_KINDS plus
|
|
16562
|
+
// every kind contributed by `withCustomAddon(...)` features (via the
|
|
16563
|
+
// ADDON_KIND_DEFINITIONS multi-provider). Overrides the empty
|
|
16564
|
+
// `providedIn: 'root'` default so contributions from form-scoped
|
|
16565
|
+
// providers are visible.
|
|
16566
|
+
{
|
|
16567
|
+
provide: ADDON_KIND_REGISTRY,
|
|
16568
|
+
useFactory: () => {
|
|
16569
|
+
const logger = inject(DynamicFormLogger);
|
|
16570
|
+
const contributed = inject(ADDON_KIND_DEFINITIONS, { optional: true }) ?? [];
|
|
16571
|
+
const registry = new Map();
|
|
16572
|
+
for (const def of BUILT_IN_ADDON_KINDS) {
|
|
16573
|
+
registry.set(def.kind, def);
|
|
16574
|
+
}
|
|
16575
|
+
for (const def of contributed) {
|
|
16576
|
+
if (registry.has(def.kind)) {
|
|
16577
|
+
logger.warn(`Addon kind "${def.kind}" is already registered. Overwriting.`);
|
|
16578
|
+
}
|
|
16579
|
+
registry.set(def.kind, def);
|
|
16580
|
+
}
|
|
16581
|
+
return registry;
|
|
16582
|
+
},
|
|
16583
|
+
},
|
|
16584
|
+
// Per-form addon-component cache (no root fallback — isolated by form scope).
|
|
16585
|
+
{ provide: ADDON_KIND_COMPONENT_CACHE, useFactory: () => new Map() },
|
|
15886
16586
|
...featureProviders,
|
|
15887
16587
|
]);
|
|
15888
16588
|
}
|
|
@@ -16059,6 +16759,35 @@ function withValidationExecutionDefaults(config) {
|
|
|
16059
16759
|
return createFeature('validation-execution', [{ provide: VALIDATION_EXECUTION_DEFAULTS, useValue: resolved }]);
|
|
16060
16760
|
}
|
|
16061
16761
|
|
|
16762
|
+
/**
|
|
16763
|
+
* Opt into Angular Signal Forms' legacy `.ng-touched` / `.ng-invalid` /
|
|
16764
|
+
* `.ng-dirty` / `.ng-pristine` / `.ng-untouched` / `.ng-pending` /
|
|
16765
|
+
* `.ng-valid` CSS classes on form-bound elements.
|
|
16766
|
+
*
|
|
16767
|
+
* Wires Angular's `@angular/forms/signals/compat` `NG_STATUS_CLASSES` strategy
|
|
16768
|
+
* via `provideSignalFormsConfig`. Without this feature the modern Signal Forms
|
|
16769
|
+
* class strategy is active, which omits the legacy `ng-*` classes.
|
|
16770
|
+
*
|
|
16771
|
+
* @example Opt in once at the application root
|
|
16772
|
+
* ```typescript
|
|
16773
|
+
* bootstrapApplication(AppComponent, {
|
|
16774
|
+
* providers: [
|
|
16775
|
+
* provideDynamicForm(
|
|
16776
|
+
* ...withMaterialFields(),
|
|
16777
|
+
* withLegacyStatusClasses()
|
|
16778
|
+
* )
|
|
16779
|
+
* ]
|
|
16780
|
+
* });
|
|
16781
|
+
* ```
|
|
16782
|
+
*
|
|
16783
|
+
* @returns A DynamicFormFeature that enables the legacy `ng-*` class strategy
|
|
16784
|
+
*
|
|
16785
|
+
* @public
|
|
16786
|
+
*/
|
|
16787
|
+
function withLegacyStatusClasses() {
|
|
16788
|
+
return createFeature('legacy-status-classes', [...provideSignalFormsConfig({ classes: NG_STATUS_CLASSES })]);
|
|
16789
|
+
}
|
|
16790
|
+
|
|
16062
16791
|
/**
|
|
16063
16792
|
* Zero-cost type carrier used in wrapper registrations.
|
|
16064
16793
|
*
|
|
@@ -16086,43 +16815,750 @@ function wrapperProps() {
|
|
|
16086
16815
|
return undefined;
|
|
16087
16816
|
}
|
|
16088
16817
|
|
|
16089
|
-
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
// downstream narrowing consistent with getFieldDefaultValue's behavior.
|
|
16093
|
-
return 'value' in field || 'nullable' in field;
|
|
16094
|
-
}
|
|
16095
|
-
|
|
16096
|
-
function isCheckedField(field) {
|
|
16097
|
-
return field.type === 'checkbox';
|
|
16818
|
+
/** Type guard narrowing {@link AddonActionContext} to its field-bound variant. */
|
|
16819
|
+
function isFieldBoundContext(ctx) {
|
|
16820
|
+
return ctx.form !== null;
|
|
16098
16821
|
}
|
|
16099
16822
|
|
|
16100
16823
|
/**
|
|
16101
|
-
*
|
|
16824
|
+
* Normalises a {@link DynamicValue} into a `Signal<T>`.
|
|
16102
16825
|
*
|
|
16103
|
-
*
|
|
16104
|
-
*
|
|
16826
|
+
* Accepts a static value, a `Signal<T>`, or an `Observable<T>`. The result
|
|
16827
|
+
* is a `Signal<T>` that reactively reflects the current value, suitable
|
|
16828
|
+
* for direct use in Angular templates.
|
|
16105
16829
|
*
|
|
16106
|
-
*
|
|
16830
|
+
* Static values yield a `computed` that always returns the same constant —
|
|
16831
|
+
* preferable to wrapping in `signal()` because the computed never causes
|
|
16832
|
+
* change-detection invalidation.
|
|
16107
16833
|
*
|
|
16108
|
-
*
|
|
16109
|
-
*
|
|
16110
|
-
*
|
|
16834
|
+
* Pass an `injector` when calling outside an Angular injection context
|
|
16835
|
+
* (e.g., from inside a `computed`). `toSignal` needs a `DestroyRef` to
|
|
16836
|
+
* unsubscribe — without an injector it falls back to `inject(DestroyRef)`,
|
|
16837
|
+
* which throws when called outside injection context.
|
|
16111
16838
|
*
|
|
16112
16839
|
* @example
|
|
16113
16840
|
* ```typescript
|
|
16114
|
-
*
|
|
16841
|
+
* const isHidden = resolveDynamicValue(addon().hidden, false);
|
|
16842
|
+
* // template: \@if (isHidden()) { ... }
|
|
16843
|
+
* ```
|
|
16844
|
+
*/
|
|
16845
|
+
function resolveDynamicValue(value, fallback, injector) {
|
|
16846
|
+
if (value === undefined) {
|
|
16847
|
+
return computed(() => fallback);
|
|
16848
|
+
}
|
|
16849
|
+
if (isSignal(value)) {
|
|
16850
|
+
return value;
|
|
16851
|
+
}
|
|
16852
|
+
if (isObservable(value)) {
|
|
16853
|
+
return toSignal(value, { initialValue: fallback, injector });
|
|
16854
|
+
}
|
|
16855
|
+
// Static value
|
|
16856
|
+
return computed(() => value);
|
|
16857
|
+
}
|
|
16858
|
+
|
|
16859
|
+
/**
|
|
16860
|
+
* Dispatcher component that renders an addon by looking up its `kind` in
|
|
16861
|
+
* `ADDON_KIND_REGISTRY` and instantiating the registered component.
|
|
16115
16862
|
*
|
|
16116
|
-
*
|
|
16117
|
-
* eventBus.dispatch(arrayEvent('contacts').append([
|
|
16118
|
-
* { key: 'name', type: 'input', label: 'Name' },
|
|
16119
|
-
* { key: 'email', type: 'input', label: 'Email' }
|
|
16120
|
-
* ]));
|
|
16863
|
+
* Used by every adapter's field component to render slot content:
|
|
16121
16864
|
*
|
|
16122
|
-
*
|
|
16123
|
-
*
|
|
16124
|
-
*
|
|
16125
|
-
*
|
|
16865
|
+
* ```html
|
|
16866
|
+
* \@for (a of addonsAt('prefix'); track $index) {
|
|
16867
|
+
* <df-addon-slot [addon]="a" />
|
|
16868
|
+
* }
|
|
16869
|
+
* ```
|
|
16870
|
+
*
|
|
16871
|
+
* Behaviour:
|
|
16872
|
+
* - Resolves and caches the kind's component asynchronously on first render.
|
|
16873
|
+
* - Forwards the `slot` HTML attribute on the host so Ionic shadow-DOM
|
|
16874
|
+
* projection works (`<ion-input>` projects children with `slot="start"`
|
|
16875
|
+
* into its native start slot).
|
|
16876
|
+
* - Honours the addon's reactive `hidden` flag — when `true`, the host is
|
|
16877
|
+
* `display: none` so it occupies no layout but stays in DOM (cheaper than
|
|
16878
|
+
* tearing down the component).
|
|
16879
|
+
* - Forwards `className` to the host element.
|
|
16880
|
+
* - ARIA semantics are owned by individual kind components — decorative
|
|
16881
|
+
* kinds (icons, text) set `aria-hidden="true"` on themselves; interactive
|
|
16882
|
+
* kinds (buttons) handle their own `aria-label`.
|
|
16883
|
+
*
|
|
16884
|
+
* If the kind cannot be resolved (registry miss, load failure), the
|
|
16885
|
+
* dispatcher logs a warning and renders nothing — never throws. The
|
|
16886
|
+
* runtime addon validator should have caught unknown kinds at config init,
|
|
16887
|
+
* but this provides a defence-in-depth fallback for misconfigured registries.
|
|
16888
|
+
*/
|
|
16889
|
+
class DfAddonSlot {
|
|
16890
|
+
registry = injectAddonKindRegistry();
|
|
16891
|
+
logger = inject(DynamicFormLogger);
|
|
16892
|
+
hostInjector = inject(Injector);
|
|
16893
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
16894
|
+
/**
|
|
16895
|
+
* Optional read-only view of the host field's mapped inputs — same shape
|
|
16896
|
+
* wrappers receive (`field?: ReadonlyFieldTree`, `key`, `props`, etc.).
|
|
16897
|
+
* Forwarded to kind components so they can read field state without
|
|
16898
|
+
* re-injecting `FIELD_SIGNAL_CONTEXT` and rebuilding context themselves.
|
|
16899
|
+
*
|
|
16900
|
+
* `undefined` when the addon is rendered outside a field (rare).
|
|
16901
|
+
*/
|
|
16902
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
16903
|
+
/**
|
|
16904
|
+
* Pre-resolved `hidden` signal supplied by the host field. When provided,
|
|
16905
|
+
* the dispatcher reads this instead of re-resolving `addon.hidden` —
|
|
16906
|
+
* lets the host (e.g., `prime-input`) share a single subscription per
|
|
16907
|
+
* Observable-typed `hidden` between its own visibility filter and the
|
|
16908
|
+
* slot's `[style.display]` binding.
|
|
16909
|
+
*/
|
|
16910
|
+
hidden = input(undefined, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
16911
|
+
/** `slot` HTML attribute forwarded to host — needed for Ionic shadow projection. */
|
|
16912
|
+
slotAttr = computed(() => this.addon().slot, ...(ngDevMode ? [{ debugName: "slotAttr" }] : /* istanbul ignore next */ []));
|
|
16913
|
+
className = computed(() => this.addon().className ?? null, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
16914
|
+
/**
|
|
16915
|
+
* Locally-resolved `hidden` signal — always reflects `addon.hidden`
|
|
16916
|
+
* regardless of whether the host supplied a pre-resolved signal.
|
|
16917
|
+
* Populated via `explicitEffect` so `resolveDynamicValue` (which may call
|
|
16918
|
+
* `toSignal` for Observable-typed values) does NOT run inside a reactive
|
|
16919
|
+
* context (NG0602). The WeakMap caches per `hidden`-value identity, so
|
|
16920
|
+
* swapping `addon` inputs that share the same Observable reuses the
|
|
16921
|
+
* subscription instead of leaking a fresh one per swap.
|
|
16922
|
+
*
|
|
16923
|
+
* Resolved unconditionally (not gated on host `hidden()` presence) so a
|
|
16924
|
+
* transition from `host hidden = Signal` → `undefined` doesn't expose a
|
|
16925
|
+
* brief window where neither value is current — the upstream effect
|
|
16926
|
+
* re-runs eventually, but the computed read can happen in between.
|
|
16927
|
+
*/
|
|
16928
|
+
_hiddenByValue = new WeakMap();
|
|
16929
|
+
_resolvedHidden = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedHidden" }] : /* istanbul ignore next */ []));
|
|
16930
|
+
/**
|
|
16931
|
+
* `hidden` resolved from `DynamicValue<boolean>` to a flat `Signal<boolean>`.
|
|
16932
|
+
* Prefers the host-supplied signal when present (avoids duplicate `toSignal`
|
|
16933
|
+
* subscriptions for Observable-typed `hidden`); falls back to the locally-
|
|
16934
|
+
* resolved signal, which is always populated by the addon-tracking effect.
|
|
16935
|
+
*/
|
|
16936
|
+
isHidden = computed(() => {
|
|
16937
|
+
const pre = this.hidden();
|
|
16938
|
+
if (pre)
|
|
16939
|
+
return pre();
|
|
16940
|
+
const local = this._resolvedHidden();
|
|
16941
|
+
return local ? local() : false;
|
|
16942
|
+
}, ...(ngDevMode ? [{ debugName: "isHidden" }] : /* istanbul ignore next */ []));
|
|
16943
|
+
/**
|
|
16944
|
+
* Latest async-loaded `{ kind, component }` pair. Tagging the result with
|
|
16945
|
+
* its kind lets `resolvedComponent` filter stale loads structurally — no
|
|
16946
|
+
* monotonic token needed. When `addon.kind` changes mid-load, the prior
|
|
16947
|
+
* load still completes and writes here, but the computed ignores it
|
|
16948
|
+
* because the recorded kind no longer matches the current one.
|
|
16949
|
+
*/
|
|
16950
|
+
asyncLoaded = signal(undefined, ...(ngDevMode ? [{ debugName: "asyncLoaded" }] : /* istanbul ignore next */ []));
|
|
16951
|
+
/**
|
|
16952
|
+
* Resolved kind component — composed declaratively from two sources of
|
|
16953
|
+
* truth: the registry's synchronous cache (warm hits, e.g., a second
|
|
16954
|
+
* `<df-addon-slot>` for an already-loaded kind) and the kind-tagged async
|
|
16955
|
+
* loader result. Reading `addon().kind` first makes the computed react to
|
|
16956
|
+
* addon swaps and naturally invalidate stale async-load results.
|
|
16957
|
+
*/
|
|
16958
|
+
resolvedComponent = computed(() => {
|
|
16959
|
+
const kind = this.addon().kind;
|
|
16960
|
+
const cached = this.registry.getLoadedKindComponent(kind);
|
|
16961
|
+
if (cached)
|
|
16962
|
+
return cached;
|
|
16963
|
+
const loaded = this.asyncLoaded();
|
|
16964
|
+
return loaded?.kind === kind ? loaded.component : undefined;
|
|
16965
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedComponent" }] : /* istanbul ignore next */ []));
|
|
16966
|
+
/** Inputs passed to the kind component — addon plus the wrapper-style host bag. */
|
|
16967
|
+
kindInputs = computed(() => ({
|
|
16968
|
+
addon: this.addon(),
|
|
16969
|
+
fieldInputs: this.fieldInputs(),
|
|
16970
|
+
}), ...(ngDevMode ? [{ debugName: "kindInputs" }] : /* istanbul ignore next */ []));
|
|
16971
|
+
constructor() {
|
|
16972
|
+
// Resolve `addon.hidden` outside any reactive context AND unconditionally —
|
|
16973
|
+
// the host-supplied signal is a perf hint, not a precondition. Keeping
|
|
16974
|
+
// `_resolvedHidden` always-fresh closes a race where `isHidden` could
|
|
16975
|
+
// return `false` between a host-`hidden` clear and the effect re-running.
|
|
16976
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
16977
|
+
const h = addon.hidden;
|
|
16978
|
+
const valueKey = typeof h === 'object' && h !== null ? h : null;
|
|
16979
|
+
let resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
|
|
16980
|
+
if (!resolved) {
|
|
16981
|
+
resolved = resolveDynamicValue(addon.hidden, false, this.hostInjector);
|
|
16982
|
+
if (valueKey)
|
|
16983
|
+
this._hiddenByValue.set(valueKey, resolved);
|
|
16984
|
+
}
|
|
16985
|
+
this._resolvedHidden.set(resolved);
|
|
16986
|
+
});
|
|
16987
|
+
// Kick off async loads on cache miss. `resolvedComponent` filters stale
|
|
16988
|
+
// results by kind identity, so this effect is a pure side-effect dispatcher.
|
|
16989
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
16990
|
+
const kind = addon.kind;
|
|
16991
|
+
if (this.registry.getLoadedKindComponent(kind))
|
|
16992
|
+
return; // sync path; computed picks it up
|
|
16993
|
+
this.registry
|
|
16994
|
+
.loadKindComponent(kind)
|
|
16995
|
+
.then((component) => this.asyncLoaded.set({ kind, component }))
|
|
16996
|
+
.catch((error) => {
|
|
16997
|
+
this.logger.warn(`Failed to load addon kind '${kind}': ${error instanceof Error ? error.message : String(error)}. ` +
|
|
16998
|
+
`Registered kinds: ${this.registry.getKindNames().join(', ') || '(none)'}.`);
|
|
16999
|
+
});
|
|
17000
|
+
});
|
|
17001
|
+
}
|
|
17002
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfAddonSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17003
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DfAddonSlot, isStandalone: true, selector: "df-addon-slot", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.slot": "slotAttr()", "class": "className()", "style.display": "isHidden() ? \"none\" : null" } }, ngImport: i0, template: `
|
|
17004
|
+
@if (resolvedComponent(); as cmp) {
|
|
17005
|
+
<ng-container *ngComponentOutlet="cmp; inputs: kindInputs()" />
|
|
17006
|
+
}
|
|
17007
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17008
|
+
}
|
|
17009
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfAddonSlot, decorators: [{
|
|
17010
|
+
type: Component,
|
|
17011
|
+
args: [{
|
|
17012
|
+
selector: 'df-addon-slot',
|
|
17013
|
+
imports: [NgComponentOutlet],
|
|
17014
|
+
template: `
|
|
17015
|
+
@if (resolvedComponent(); as cmp) {
|
|
17016
|
+
<ng-container *ngComponentOutlet="cmp; inputs: kindInputs()" />
|
|
17017
|
+
}
|
|
17018
|
+
`,
|
|
17019
|
+
host: {
|
|
17020
|
+
'[attr.slot]': 'slotAttr()',
|
|
17021
|
+
'[class]': 'className()',
|
|
17022
|
+
'[style.display]': 'isHidden() ? "none" : null',
|
|
17023
|
+
},
|
|
17024
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17025
|
+
}]
|
|
17026
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
|
|
17027
|
+
|
|
17028
|
+
/**
|
|
17029
|
+
* Pipe that handles dynamic text resolution with support for static strings,
|
|
17030
|
+
* Observables, and Signals.
|
|
17031
|
+
*
|
|
17032
|
+
* Supports:
|
|
17033
|
+
* - Static strings (pass-through)
|
|
17034
|
+
* - Observables (subscribed internally)
|
|
17035
|
+
* - Signals (converted to Observable using toObservable for reactivity)
|
|
17036
|
+
*
|
|
17037
|
+
* @example
|
|
17038
|
+
* ```html
|
|
17039
|
+
* <!-- Static string -->
|
|
17040
|
+
* {{ 'Hello World' | dynamicText | async }}
|
|
17041
|
+
*
|
|
17042
|
+
* <!-- Observable -->
|
|
17043
|
+
* {{ transloco.selectTranslate('key') | dynamicText | async }}
|
|
17044
|
+
*
|
|
17045
|
+
* <!-- Signal -->
|
|
17046
|
+
* {{ myTextSignal | dynamicText | async }}
|
|
17047
|
+
* ```
|
|
17048
|
+
*
|
|
17049
|
+
* @public
|
|
17050
|
+
*/
|
|
17051
|
+
class DynamicTextPipe {
|
|
17052
|
+
injector = inject(Injector);
|
|
17053
|
+
/**
|
|
17054
|
+
* Transforms dynamic text input into a resolved string value
|
|
17055
|
+
*
|
|
17056
|
+
* @param value - The dynamic text value to resolve
|
|
17057
|
+
* @returns The resolved string value as an Observable
|
|
17058
|
+
*/
|
|
17059
|
+
transform(value) {
|
|
17060
|
+
if (isObservable(value)) {
|
|
17061
|
+
return value;
|
|
17062
|
+
}
|
|
17063
|
+
if (isSignal(value)) {
|
|
17064
|
+
return toObservable(value, { injector: this.injector });
|
|
17065
|
+
}
|
|
17066
|
+
return of(value || '');
|
|
17067
|
+
}
|
|
17068
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
17069
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
17070
|
+
}
|
|
17071
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
17072
|
+
type: Pipe,
|
|
17073
|
+
args: [{
|
|
17074
|
+
name: 'dynamicText',
|
|
17075
|
+
}]
|
|
17076
|
+
}] });
|
|
17077
|
+
|
|
17078
|
+
/**
|
|
17079
|
+
* Renderer for the universal `text` addon kind.
|
|
17080
|
+
*
|
|
17081
|
+
* Renders the addon's `text` (a `DynamicText`) inside a span. Decorative —
|
|
17082
|
+
* sets `aria-hidden="true"` on the host so screen readers skip it; users
|
|
17083
|
+
* who need announcement should use a `*-button` kind with `ariaLabel` instead.
|
|
17084
|
+
*/
|
|
17085
|
+
class TextAddonComponent {
|
|
17086
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17087
|
+
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
|
|
17088
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17089
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17090
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: TextAddonComponent, isStandalone: true, selector: "df-text-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "\"true\"" } }, ngImport: i0, template: `<span>{{ addon().text | dynamicText | async }}</span>`, isInline: true, dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17091
|
+
}
|
|
17092
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextAddonComponent, decorators: [{
|
|
17093
|
+
type: Component,
|
|
17094
|
+
args: [{
|
|
17095
|
+
selector: 'df-text-addon',
|
|
17096
|
+
imports: [AsyncPipe, DynamicTextPipe],
|
|
17097
|
+
template: `<span>{{ addon().text | dynamicText | async }}</span>`,
|
|
17098
|
+
host: {
|
|
17099
|
+
'[attr.aria-hidden]': '"true"',
|
|
17100
|
+
},
|
|
17101
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17102
|
+
}]
|
|
17103
|
+
}], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17104
|
+
|
|
17105
|
+
var textAddon_component = /*#__PURE__*/Object.freeze({
|
|
17106
|
+
__proto__: null,
|
|
17107
|
+
TextAddonComponent: TextAddonComponent
|
|
17108
|
+
});
|
|
17109
|
+
|
|
17110
|
+
/**
|
|
17111
|
+
* Renderer for the universal `template` addon kind.
|
|
17112
|
+
*
|
|
17113
|
+
* Resolves the addon's `templateKey` against {@link DF_FIELD_TEMPLATES}
|
|
17114
|
+
* (populated by `<df-dynamic-form>` from `<ng-template dfTemplate="...">`
|
|
17115
|
+
* children) and renders via `NgTemplateOutlet`. The wrapper-style host bag
|
|
17116
|
+
* (`fieldInputs`) is forwarded as the template's implicit context — author
|
|
17117
|
+
* the template as `<ng-template dfTemplate="..." let-fi>{{ fi.key }}</ng-template>`.
|
|
17118
|
+
*
|
|
17119
|
+
* If the key is unresolved, logs a warning and renders nothing — keeps the
|
|
17120
|
+
* form alive when a backend references a template the FE has not registered.
|
|
17121
|
+
*/
|
|
17122
|
+
class TemplateAddonComponent {
|
|
17123
|
+
templates = inject(DF_FIELD_TEMPLATES, { optional: true });
|
|
17124
|
+
logger = inject(DynamicFormLogger);
|
|
17125
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17126
|
+
/** Forwarded by `df-addon-slot`; surfaced to the template as its implicit `$implicit` context. */
|
|
17127
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17128
|
+
// Pure computed: pure lookup, no side effects. Unresolved → null; the
|
|
17129
|
+
// `@if` in the template skips the outlet and renders nothing.
|
|
17130
|
+
template = computed(() => {
|
|
17131
|
+
const map = this.templates?.();
|
|
17132
|
+
const key = this.addon().templateKey;
|
|
17133
|
+
return map?.get(key) ?? null;
|
|
17134
|
+
}, ...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
|
|
17135
|
+
/** Implicit context payload — template authors bind via `let-fi`. */
|
|
17136
|
+
outletContext = computed(() => ({ $implicit: this.fieldInputs() }), ...(ngDevMode ? [{ debugName: "outletContext" }] : /* istanbul ignore next */ []));
|
|
17137
|
+
constructor() {
|
|
17138
|
+
// Logging side effect lives in an effect, not the computed — keeps
|
|
17139
|
+
// the computed pure so signal-graph evaluations don't duplicate warnings.
|
|
17140
|
+
explicitEffect([this.template, this.addon], ([tpl, addon]) => {
|
|
17141
|
+
if (!tpl) {
|
|
17142
|
+
this.logger.warn(`Template addon: no <ng-template dfTemplate="${addon.templateKey}"> found. Did you project it as a child of <df-dynamic-form>?`);
|
|
17143
|
+
}
|
|
17144
|
+
});
|
|
17145
|
+
}
|
|
17146
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TemplateAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17147
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TemplateAddonComponent, isStandalone: true, selector: "df-template-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
17148
|
+
@if (template(); as tpl) {
|
|
17149
|
+
<ng-container *ngTemplateOutlet="tpl; context: outletContext()" />
|
|
17150
|
+
}
|
|
17151
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17152
|
+
}
|
|
17153
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TemplateAddonComponent, decorators: [{
|
|
17154
|
+
type: Component,
|
|
17155
|
+
args: [{
|
|
17156
|
+
selector: 'df-template-addon',
|
|
17157
|
+
imports: [NgTemplateOutlet],
|
|
17158
|
+
template: `
|
|
17159
|
+
@if (template(); as tpl) {
|
|
17160
|
+
<ng-container *ngTemplateOutlet="tpl; context: outletContext()" />
|
|
17161
|
+
}
|
|
17162
|
+
`,
|
|
17163
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17164
|
+
}]
|
|
17165
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17166
|
+
|
|
17167
|
+
var templateAddon_component = /*#__PURE__*/Object.freeze({
|
|
17168
|
+
__proto__: null,
|
|
17169
|
+
TemplateAddonComponent: TemplateAddonComponent
|
|
17170
|
+
});
|
|
17171
|
+
|
|
17172
|
+
/**
|
|
17173
|
+
* Renderer for the universal `component` addon kind.
|
|
17174
|
+
*
|
|
17175
|
+
* Loads the user-supplied component lazily (`addon.component()`) and renders
|
|
17176
|
+
* it via `NgComponentOutlet` with `addon.inputs` forwarded as input bindings.
|
|
17177
|
+
* The wrapper-style `fieldInputs` bag is not propagated — user components
|
|
17178
|
+
* receive only what `addon.inputs` declares. Authors that need field state
|
|
17179
|
+
* should switch to the `template` kind or build a wrapper.
|
|
17180
|
+
*
|
|
17181
|
+
* @codeOnly The `component` loader is a function; this kind is dropped in
|
|
17182
|
+
* lenient validation when the config originated from JSON.
|
|
17183
|
+
*/
|
|
17184
|
+
class ComponentAddonComponent {
|
|
17185
|
+
logger = inject(DynamicFormLogger);
|
|
17186
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17187
|
+
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
|
|
17188
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17189
|
+
inputs = computed(() => (this.addon().inputs ?? {}), ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
|
|
17190
|
+
/**
|
|
17191
|
+
* Latest async-loaded `{ addon, component }` pair. Tagging the result with
|
|
17192
|
+
* its source addon lets `resolvedComponent` filter stale loads structurally
|
|
17193
|
+
* — same kind-tagging pattern `DfAddonSlot` uses. When `addon` swaps
|
|
17194
|
+
* mid-load, the prior loader still completes and writes here, but the
|
|
17195
|
+
* computed ignores it because the recorded addon no longer matches the
|
|
17196
|
+
* current one.
|
|
17197
|
+
*/
|
|
17198
|
+
asyncLoaded = signal(undefined, ...(ngDevMode ? [{ debugName: "asyncLoaded" }] : /* istanbul ignore next */ []));
|
|
17199
|
+
resolvedComponent = computed(() => {
|
|
17200
|
+
const current = this.addon();
|
|
17201
|
+
const loaded = this.asyncLoaded();
|
|
17202
|
+
return loaded?.addon === current ? loaded.component : undefined;
|
|
17203
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedComponent" }] : /* istanbul ignore next */ []));
|
|
17204
|
+
constructor() {
|
|
17205
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
17206
|
+
Promise.resolve(addon.component())
|
|
17207
|
+
.then((mod) => {
|
|
17208
|
+
const component = resolveDefaultExport(mod);
|
|
17209
|
+
if (component)
|
|
17210
|
+
this.asyncLoaded.set({ addon, component });
|
|
17211
|
+
})
|
|
17212
|
+
.catch((error) => {
|
|
17213
|
+
this.logger.warn(`Failed to load component addon (slot: '${addon.slot}'): ${error instanceof Error ? error.message : String(error)}`);
|
|
17214
|
+
});
|
|
17215
|
+
});
|
|
17216
|
+
}
|
|
17217
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17218
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ComponentAddonComponent, isStandalone: true, selector: "df-component-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
17219
|
+
@if (resolvedComponent(); as cmp) {
|
|
17220
|
+
<ng-container *ngComponentOutlet="cmp; inputs: inputs()" />
|
|
17221
|
+
}
|
|
17222
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17223
|
+
}
|
|
17224
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentAddonComponent, decorators: [{
|
|
17225
|
+
type: Component,
|
|
17226
|
+
args: [{
|
|
17227
|
+
selector: 'df-component-addon',
|
|
17228
|
+
imports: [NgComponentOutlet],
|
|
17229
|
+
template: `
|
|
17230
|
+
@if (resolvedComponent(); as cmp) {
|
|
17231
|
+
<ng-container *ngComponentOutlet="cmp; inputs: inputs()" />
|
|
17232
|
+
}
|
|
17233
|
+
`,
|
|
17234
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17235
|
+
}]
|
|
17236
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17237
|
+
|
|
17238
|
+
var componentAddon_component = /*#__PURE__*/Object.freeze({
|
|
17239
|
+
__proto__: null,
|
|
17240
|
+
ComponentAddonComponent: ComponentAddonComponent
|
|
17241
|
+
});
|
|
17242
|
+
|
|
17243
|
+
/**
|
|
17244
|
+
* Apply a built-in preset action against the addon's host field. Shared
|
|
17245
|
+
* across all adapters — each adapter's `ADDON_PRESET_HANDLER` provider
|
|
17246
|
+
* delegates here.
|
|
17247
|
+
*
|
|
17248
|
+
* Presets that require adapter-specific UI mechanics (clipboard, type
|
|
17249
|
+
* override) are best-effort — they no-op with a warning when the necessary
|
|
17250
|
+
* collaborator isn't available (e.g., toggle-password-visibility outside an
|
|
17251
|
+
* input field).
|
|
17252
|
+
*
|
|
17253
|
+
* @param adapterLabel Used in warnings — `'Material'` / `'Bootstrap'` / etc.
|
|
17254
|
+
* @param fieldLabel Used in warnings — `'mat-input'` / `'bs-input'` / etc.
|
|
17255
|
+
*/
|
|
17256
|
+
async function runPresetAction(preset, ctx, collaborators, adapterLabel, fieldLabel) {
|
|
17257
|
+
// Guard against orphan dispatches — none of the built-in presets do
|
|
17258
|
+
// anything useful without a host field, except `copy` and `paste` which
|
|
17259
|
+
// only need the clipboard API. The canonical discriminant per the
|
|
17260
|
+
// `AddonActionContext` discriminated union is `ctx.form` — non-null on
|
|
17261
|
+
// the field-bound variant, null on the orphan variant. `field.key` was
|
|
17262
|
+
// unreliable for nested-array scenarios where keys can legitimately be
|
|
17263
|
+
// empty strings.
|
|
17264
|
+
if (ctx.form === null && preset !== 'copy' && preset !== 'paste') {
|
|
17265
|
+
collaborators.logger.warn(`preset '${String(preset)}' fired without a host field context — ignoring.`);
|
|
17266
|
+
return;
|
|
17267
|
+
}
|
|
17268
|
+
switch (preset) {
|
|
17269
|
+
case 'clear': {
|
|
17270
|
+
// Preserve type semantics: strings clear to `''` (browser-friendly for
|
|
17271
|
+
// input.value), everything else clears to `undefined` (numeric / object
|
|
17272
|
+
// / date fields would otherwise become a string and break their type).
|
|
17273
|
+
const next = typeof ctx.value === 'string' ? '' : undefined;
|
|
17274
|
+
collaborators.fieldValueSetter?.(next);
|
|
17275
|
+
return;
|
|
17276
|
+
}
|
|
17277
|
+
case 'reset': {
|
|
17278
|
+
const defaultValue = collaborators.fieldDefaultValueGetter?.();
|
|
17279
|
+
collaborators.fieldValueSetter?.(defaultValue !== undefined ? defaultValue : typeof ctx.value === 'string' ? '' : undefined);
|
|
17280
|
+
return;
|
|
17281
|
+
}
|
|
17282
|
+
case 'paste': {
|
|
17283
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
17284
|
+
collaborators.logger.warn(`preset 'paste' requires a browser clipboard API (not available in this environment).`);
|
|
17285
|
+
return;
|
|
17286
|
+
}
|
|
17287
|
+
try {
|
|
17288
|
+
const text = await navigator.clipboard.readText();
|
|
17289
|
+
collaborators.fieldValueSetter?.(text);
|
|
17290
|
+
}
|
|
17291
|
+
catch (error) {
|
|
17292
|
+
collaborators.logger.warn(`preset 'paste' clipboard read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
17293
|
+
}
|
|
17294
|
+
return;
|
|
17295
|
+
}
|
|
17296
|
+
case 'copy': {
|
|
17297
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
17298
|
+
collaborators.logger.warn(`preset 'copy' requires a browser clipboard API (not available in this environment).`);
|
|
17299
|
+
return;
|
|
17300
|
+
}
|
|
17301
|
+
try {
|
|
17302
|
+
// Explicit null check — `String(null) === 'null'` would write the
|
|
17303
|
+
// literal string "null" to the clipboard.
|
|
17304
|
+
const v = ctx.value;
|
|
17305
|
+
await navigator.clipboard.writeText(v == null ? '' : String(v));
|
|
17306
|
+
}
|
|
17307
|
+
catch (error) {
|
|
17308
|
+
collaborators.logger.warn(`preset 'copy' clipboard write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
17309
|
+
}
|
|
17310
|
+
return;
|
|
17311
|
+
}
|
|
17312
|
+
case 'toggle-password-visibility': {
|
|
17313
|
+
const override = collaborators.typeOverride;
|
|
17314
|
+
if (!override) {
|
|
17315
|
+
collaborators.logger.warn(`preset 'toggle-password-visibility' has no effect outside a '${fieldLabel}' field.`);
|
|
17316
|
+
return;
|
|
17317
|
+
}
|
|
17318
|
+
// Refuse to flip non-password inputs. Re-check on EVERY dispatch (not
|
|
17319
|
+
// just when override is undefined) because `props().type` can change
|
|
17320
|
+
// reactively after the first toggle — e.g., 'password' → 'email'. Once
|
|
17321
|
+
// the override is touched, only the override is canonical for the
|
|
17322
|
+
// current visible/hidden state; the baseline still gates whether
|
|
17323
|
+
// toggling makes semantic sense for this field.
|
|
17324
|
+
const baseline = collaborators.baselineType?.();
|
|
17325
|
+
if (baseline !== undefined && baseline !== 'password') {
|
|
17326
|
+
collaborators.logger.warn(`preset 'toggle-password-visibility' refused: host '${fieldLabel}' has type '${baseline}', not 'password'.`);
|
|
17327
|
+
return;
|
|
17328
|
+
}
|
|
17329
|
+
const current = override();
|
|
17330
|
+
override.set(current === 'text' ? 'password' : 'text');
|
|
17331
|
+
return;
|
|
17332
|
+
}
|
|
17333
|
+
default:
|
|
17334
|
+
collaborators.logger.warn(`preset '${String(preset)}' is not implemented in the ${adapterLabel} adapter.`);
|
|
17335
|
+
}
|
|
17336
|
+
}
|
|
17337
|
+
|
|
17338
|
+
/**
|
|
17339
|
+
* Walk `FIELD_REGISTRY` and return every field type that opted into addons
|
|
17340
|
+
* (i.e., declared `addons` on its `FieldTypeDefinition`).
|
|
17341
|
+
*
|
|
17342
|
+
* Useful for tooling — admin UIs surfacing "which fields can carry addons?",
|
|
17343
|
+
* docs generators, MCP server cross-checks. Field types without an `addons`
|
|
17344
|
+
* registration ("Tier 3" — toggle, checkbox, radio, slider) are excluded.
|
|
17345
|
+
*
|
|
17346
|
+
* Must be called within an injection context.
|
|
17347
|
+
*
|
|
17348
|
+
* @example
|
|
17349
|
+
* ```typescript
|
|
17350
|
+
* @Component({ ... })
|
|
17351
|
+
* class FormBuilder {
|
|
17352
|
+
* protected readonly addonable = injectFieldsSupportingAddons();
|
|
17353
|
+
*
|
|
17354
|
+
* constructor() {
|
|
17355
|
+
* // -> [{ name: 'prime-input', slots: ['prefix', 'suffix'], allowedKinds: [...] }, ...]
|
|
17356
|
+
* }
|
|
17357
|
+
* }
|
|
17358
|
+
* ```
|
|
17359
|
+
*/
|
|
17360
|
+
function injectFieldsSupportingAddons() {
|
|
17361
|
+
const registry = inject(FIELD_REGISTRY);
|
|
17362
|
+
const entries = [];
|
|
17363
|
+
for (const [name, def] of registry.entries()) {
|
|
17364
|
+
if (def.addons === undefined)
|
|
17365
|
+
continue;
|
|
17366
|
+
entries.push({ name, slots: def.addons.slots, allowedKinds: def.addons.allowedKinds });
|
|
17367
|
+
}
|
|
17368
|
+
return entries;
|
|
17369
|
+
}
|
|
17370
|
+
|
|
17371
|
+
/**
|
|
17372
|
+
* Register a custom addon kind with the form.
|
|
17373
|
+
*
|
|
17374
|
+
* The provided definition is added to `ADDON_KIND_REGISTRY` at form
|
|
17375
|
+
* initialisation, alongside the built-in `text` / `template` / `component`
|
|
17376
|
+
* kinds and any kinds contributed by adapter feature helpers
|
|
17377
|
+
* (`withPrimeNGAddons()` etc.).
|
|
17378
|
+
*
|
|
17379
|
+
* @example
|
|
17380
|
+
* ```typescript
|
|
17381
|
+
* provideDynamicForm(
|
|
17382
|
+
* ...withMaterialFields(),
|
|
17383
|
+
* withCustomAddon({
|
|
17384
|
+
* kind: 'lucide-icon',
|
|
17385
|
+
* // Match how field types register: a default-exported component lets
|
|
17386
|
+
* // `loadComponent` return the import promise directly.
|
|
17387
|
+
* loadComponent: () => import('./lucide-icon-addon.component'),
|
|
17388
|
+
* validate: (addon) => {
|
|
17389
|
+
* if (typeof addon.icon !== 'string') {
|
|
17390
|
+
* throw new Error('lucide-icon: icon string is required');
|
|
17391
|
+
* }
|
|
17392
|
+
* },
|
|
17393
|
+
* }),
|
|
17394
|
+
* );
|
|
17395
|
+
* ```
|
|
17396
|
+
*
|
|
17397
|
+
* Multiple `withCustomAddon(...)` calls are supported; later registrations
|
|
17398
|
+
* for the same kind override earlier ones (with a warning logged).
|
|
17399
|
+
*/
|
|
17400
|
+
function withCustomAddon(definition) {
|
|
17401
|
+
return createFeature('addons', [
|
|
17402
|
+
{
|
|
17403
|
+
provide: ADDON_KIND_DEFINITIONS,
|
|
17404
|
+
useValue: definition,
|
|
17405
|
+
multi: true,
|
|
17406
|
+
},
|
|
17407
|
+
]);
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17410
|
+
/**
|
|
17411
|
+
* Register named action handlers reachable via JSON-driven addon configs.
|
|
17412
|
+
*
|
|
17413
|
+
* Once registered, a backend can ship
|
|
17414
|
+
* `{ kind: 'prime-button', actionRef: 'openProfile' }` and the FE will invoke
|
|
17415
|
+
* the corresponding handler at click time.
|
|
17416
|
+
*
|
|
17417
|
+
* Define handlers once and augment the registry from the same object:
|
|
17418
|
+
*
|
|
17419
|
+
* ```typescript
|
|
17420
|
+
* export const appActions = provideAddonActions({
|
|
17421
|
+
* openProfile: (ctx) => modalService.open(ProfileModal, { data: ctx.value }),
|
|
17422
|
+
* runWorkflow: (ctx) => workflowService.run(ctx.field.key, ctx.value),
|
|
17423
|
+
* });
|
|
17424
|
+
*
|
|
17425
|
+
* declare module '@ng-forge/dynamic-forms' {
|
|
17426
|
+
* // Pulls the keys straight off `appActions` — no manual list to keep in sync.
|
|
17427
|
+
* interface DynamicFormActionRegistry extends Record<NonNullable<typeof appActions['__handlerKeys']>, true> {}
|
|
17428
|
+
* }
|
|
17429
|
+
*
|
|
17430
|
+
* // Then in your bootstrap:
|
|
17431
|
+
* provideDynamicForm(...withPrimeNGFields(), appActions);
|
|
17432
|
+
* ```
|
|
17433
|
+
*
|
|
17434
|
+
* Multiple `provideAddonActions(...)` calls are merged; later names override
|
|
17435
|
+
* earlier ones for the same key.
|
|
17436
|
+
*/
|
|
17437
|
+
function provideAddonActions(handlers) {
|
|
17438
|
+
return createFeature('addon-actions', [
|
|
17439
|
+
{
|
|
17440
|
+
provide: ADDON_ACTION_HANDLERS,
|
|
17441
|
+
useValue: handlers,
|
|
17442
|
+
multi: true,
|
|
17443
|
+
},
|
|
17444
|
+
]);
|
|
17445
|
+
}
|
|
17446
|
+
|
|
17447
|
+
/**
|
|
17448
|
+
* Walk a `FormConfig`, validate every field's addons against the active
|
|
17449
|
+
* `FIELD_REGISTRY` and `ADDON_KIND_REGISTRY`, and return a sanitized copy
|
|
17450
|
+
* plus a list of dropped-addon warnings.
|
|
17451
|
+
*
|
|
17452
|
+
* Distinct from the INTERNAL `validateFormConfig` in
|
|
17453
|
+
* `utils/config-validation/config-validator.ts` (which throws on
|
|
17454
|
+
* structural errors like duplicate keys / unknown field types). This
|
|
17455
|
+
* helper is the PUBLIC, lenient addon-shape pass — renamed to
|
|
17456
|
+
* `sanitizeFormConfig` to avoid the name collision.
|
|
17457
|
+
*
|
|
17458
|
+
* Lenient by design: invalid addons are dropped (with warnings) rather than
|
|
17459
|
+
* thrown. The form keeps rendering even when the backend ships an addon the
|
|
17460
|
+
* FE doesn't understand.
|
|
17461
|
+
*
|
|
17462
|
+
* `<df-dynamic-form>` already runs this internally at form init — calling
|
|
17463
|
+
* the public helper AND passing the original config to the form will
|
|
17464
|
+
* double-emit warnings. Use this entry point only when you need the
|
|
17465
|
+
* sanitized config or warning list outside the form (e.g., admin-UI
|
|
17466
|
+
* diagnostics, build-time lints).
|
|
17467
|
+
*
|
|
17468
|
+
* Must be called within an Angular injection context — typically inside
|
|
17469
|
+
* an admin-UI component or a `runInInjectionContext()` block on the server.
|
|
17470
|
+
*
|
|
17471
|
+
* @example
|
|
17472
|
+
* ```typescript
|
|
17473
|
+
* @Component({ ... })
|
|
17474
|
+
* class AdminFormBuilder {
|
|
17475
|
+
* private readonly check = (cfg: FormConfig) => {
|
|
17476
|
+
* const { sanitized, warnings } = sanitizeFormConfig(cfg, { source: 'json' });
|
|
17477
|
+
* if (warnings.length > 0) {
|
|
17478
|
+
* this.diagnostics.set(warnings);
|
|
17479
|
+
* }
|
|
17480
|
+
* return sanitized;
|
|
17481
|
+
* };
|
|
17482
|
+
* }
|
|
17483
|
+
* ```
|
|
17484
|
+
*/
|
|
17485
|
+
function sanitizeFormConfig(config, options = {}) {
|
|
17486
|
+
const fieldRegistry = inject(FIELD_REGISTRY);
|
|
17487
|
+
const kindRegistry = inject(ADDON_KIND_REGISTRY);
|
|
17488
|
+
const source = options.source ?? 'inline';
|
|
17489
|
+
const { fields, warnings } = walkAndValidateAddons((config.fields ?? []), fieldRegistry, kindRegistry, source);
|
|
17490
|
+
return {
|
|
17491
|
+
sanitized: { ...config, fields: fields },
|
|
17492
|
+
warnings,
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
/**
|
|
17496
|
+
* Default warning sink. Pass a `DynamicFormLogger` to route through the
|
|
17497
|
+
* form's configured logger (respects `NoopLogger` opt-out); omit it to
|
|
17498
|
+
* fall back to `console.warn` for build-time / admin-UI use cases that
|
|
17499
|
+
* run outside an Angular injection context.
|
|
17500
|
+
*/
|
|
17501
|
+
function logAddonWarnings(warnings, logger) {
|
|
17502
|
+
for (const w of warnings) {
|
|
17503
|
+
if (logger) {
|
|
17504
|
+
logger.warn(formatAddonWarning(w));
|
|
17505
|
+
}
|
|
17506
|
+
else {
|
|
17507
|
+
console.warn('[Dynamic Forms]', formatAddonWarning(w));
|
|
17508
|
+
}
|
|
17509
|
+
}
|
|
17510
|
+
}
|
|
17511
|
+
/**
|
|
17512
|
+
* DI-free variant of {@link sanitizeFormConfig} for tooling that lives
|
|
17513
|
+
* outside Angular's injector — build-time linters, MCP-side checks, CLI
|
|
17514
|
+
* scripts.
|
|
17515
|
+
*
|
|
17516
|
+
* Caller passes the registries explicitly; the result has the same shape
|
|
17517
|
+
* as the DI-bound version.
|
|
17518
|
+
*/
|
|
17519
|
+
function sanitizeFormConfigPure(config, fieldRegistry, kindRegistry, options = {}) {
|
|
17520
|
+
const source = options.source ?? 'inline';
|
|
17521
|
+
const { fields, warnings } = walkAndValidateAddons((config.fields ?? []), fieldRegistry, kindRegistry, source);
|
|
17522
|
+
return { sanitized: { ...config, fields: fields }, warnings };
|
|
17523
|
+
}
|
|
17524
|
+
|
|
17525
|
+
function isValueField(field) {
|
|
17526
|
+
// `nullable: true` without an explicit `value` is still a value-bearing declaration:
|
|
17527
|
+
// the field resolves to `null` at runtime. Treating it as a value field keeps
|
|
17528
|
+
// downstream narrowing consistent with getFieldDefaultValue's behavior.
|
|
17529
|
+
return 'value' in field || 'nullable' in field;
|
|
17530
|
+
}
|
|
17531
|
+
|
|
17532
|
+
function isCheckedField(field) {
|
|
17533
|
+
return field.type === 'checkbox' || field.type === 'toggle';
|
|
17534
|
+
}
|
|
17535
|
+
|
|
17536
|
+
/**
|
|
17537
|
+
* Builder for array manipulation events.
|
|
17538
|
+
*
|
|
17539
|
+
* Provides a fluent, discoverable API for array operations.
|
|
17540
|
+
* Type `arrayEvent('key').` in your IDE to see all available operations.
|
|
17541
|
+
*
|
|
17542
|
+
* **BREAKING CHANGE**: Template is now required for add operations.
|
|
17543
|
+
*
|
|
17544
|
+
* Supports both primitive and object array items:
|
|
17545
|
+
* - Primitive: Pass a single field definition → extracts field value directly
|
|
17546
|
+
* - Object: Pass an array of field definitions → merges fields into object
|
|
17547
|
+
*
|
|
17548
|
+
* @example
|
|
17549
|
+
* ```typescript
|
|
17550
|
+
* import { arrayEvent } from '@ng-forge/dynamic-forms';
|
|
17551
|
+
*
|
|
17552
|
+
* // Object item: append { name, email } object
|
|
17553
|
+
* eventBus.dispatch(arrayEvent('contacts').append([
|
|
17554
|
+
* { key: 'name', type: 'input', label: 'Name' },
|
|
17555
|
+
* { key: 'email', type: 'input', label: 'Email' }
|
|
17556
|
+
* ]));
|
|
17557
|
+
*
|
|
17558
|
+
* // Primitive item: append single value
|
|
17559
|
+
* eventBus.dispatch(arrayEvent('tags').append(
|
|
17560
|
+
* { key: 'tag', type: 'input', label: 'Tag' }
|
|
17561
|
+
* ));
|
|
16126
17562
|
*
|
|
16127
17563
|
* // Removing items (no template needed)
|
|
16128
17564
|
* eventBus.dispatch(arrayEvent('contacts').pop()); // Remove last
|
|
@@ -16600,56 +18036,6 @@ function applyMetaToElement(element, meta, previouslyApplied) {
|
|
|
16600
18036
|
return newApplied;
|
|
16601
18037
|
}
|
|
16602
18038
|
|
|
16603
|
-
/**
|
|
16604
|
-
* Pipe that handles dynamic text resolution with support for static strings,
|
|
16605
|
-
* Observables, and Signals.
|
|
16606
|
-
*
|
|
16607
|
-
* Supports:
|
|
16608
|
-
* - Static strings (pass-through)
|
|
16609
|
-
* - Observables (subscribed internally)
|
|
16610
|
-
* - Signals (converted to Observable using toObservable for reactivity)
|
|
16611
|
-
*
|
|
16612
|
-
* @example
|
|
16613
|
-
* ```html
|
|
16614
|
-
* <!-- Static string -->
|
|
16615
|
-
* {{ 'Hello World' | dynamicText | async }}
|
|
16616
|
-
*
|
|
16617
|
-
* <!-- Observable -->
|
|
16618
|
-
* {{ transloco.selectTranslate('key') | dynamicText | async }}
|
|
16619
|
-
*
|
|
16620
|
-
* <!-- Signal -->
|
|
16621
|
-
* {{ myTextSignal | dynamicText | async }}
|
|
16622
|
-
* ```
|
|
16623
|
-
*
|
|
16624
|
-
* @public
|
|
16625
|
-
*/
|
|
16626
|
-
class DynamicTextPipe {
|
|
16627
|
-
injector = inject(Injector);
|
|
16628
|
-
/**
|
|
16629
|
-
* Transforms dynamic text input into a resolved string value
|
|
16630
|
-
*
|
|
16631
|
-
* @param value - The dynamic text value to resolve
|
|
16632
|
-
* @returns The resolved string value as an Observable
|
|
16633
|
-
*/
|
|
16634
|
-
transform(value) {
|
|
16635
|
-
if (isObservable(value)) {
|
|
16636
|
-
return value;
|
|
16637
|
-
}
|
|
16638
|
-
if (isSignal(value)) {
|
|
16639
|
-
return toObservable(value, { injector: this.injector });
|
|
16640
|
-
}
|
|
16641
|
-
return of(value || '');
|
|
16642
|
-
}
|
|
16643
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
16644
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
16645
|
-
}
|
|
16646
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
16647
|
-
type: Pipe,
|
|
16648
|
-
args: [{
|
|
16649
|
-
name: 'dynamicText',
|
|
16650
|
-
}]
|
|
16651
|
-
}] });
|
|
16652
|
-
|
|
16653
18039
|
/**
|
|
16654
18040
|
* Interpolates {{param}} placeholders in message with error values
|
|
16655
18041
|
*
|
|
@@ -16781,5 +18167,5 @@ function extractErrorParams(error) {
|
|
|
16781
18167
|
* Generated bundle index. Do not edit.
|
|
16782
18168
|
*/
|
|
16783
18169
|
|
|
16784
|
-
export {
|
|
16785
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
18170
|
+
export { arrayFieldMapper as $, ADDON_ACTION_HANDLERS as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DEFAULT_PROPS as D, EventBus as E, FIELD_REGISTRY as F, GROUP_CONTEXT as G, NoopLogger as H, INITIALIZATION_TIMEOUT_MS as I, PopArrayItemEvent as J, PrependArrayItemEvent as K, PreviousPageEvent as L, MoveArrayItemEvent as M, NextPageEvent as N, RootFormRegistryService as O, PageChangeEvent as P, SubmitEvent as Q, RemoveAtIndexEvent as R, ShiftArrayItemEvent as S, TemplateAddonComponent as T, TextAddonComponent as U, WRAPPER_REGISTRY as V, WRAPPER_AUTO_ASSOCIATIONS as W, applyMetaToElement as X, applyValidator as Y, applyValidators as Z, arrayEvent as _, ADDON_ACTION_REGISTRY as a, textFieldMapper as a$, baseFieldMapper as a0, buildBaseInputs as a1, containerFieldMapper as a2, createField as a3, createFieldResolutionPipe as a4, createHiddenSignal as a5, createRenderReadySignal as a6, createSchemaFromFields as a7, createWrapperChainController as a8, createWrappers as a9, isGroupField as aA, isLeafField as aB, isPageField as aC, isRowField as aD, isSameWrapperChain as aE, isSimplifiedArrayField as aF, isValueBearingField as aG, isValueField as aH, isWrapperTypeDefinition as aI, isWrappersBundle as aJ, logAddonWarnings as aK, mapFieldToInputs as aL, omit as aM, pageFieldMapper as aN, provideAddonActions as aO, provideDynamicForm as aP, resolveDynamicValue as aQ, resolveNextButtonDisabled as aR, resolveNonFieldDisabled as aS, resolveNonFieldHidden as aT, resolveSubmitButtonDisabled as aU, resolveTokens as aV, resolveWrappers as aW, rowFieldMapper as aX, runPresetAction as aY, sanitizeFormConfig as aZ, sanitizeFormConfigPure as a_, derivedFromDeferred as aa, dynamicTextToObservable as ab, evaluateNonFieldDisabled as ac, evaluateNonFieldHidden as ad, field as ae, formConfig as af, formatAddonWarning as ag, getArrayLength as ah, getFieldDefaultValue as ai, getFieldValueHandling as aj, getNormalizedArrayMetadata as ak, groupFieldMapper as al, hasFormValue as am, injectAddonKindRegistry as an, injectFieldRegistry as ao, injectFieldSignalContext as ap, injectFieldsSupportingAddons as aq, interpolateParams as ar, isArrayField as as, isCheckedField as at, isContainerField as au, isContainerTypedField as av, isDisplayOnlyField as aw, isEqual as ax, isFieldBoundContext as ay, isFormStateCondition as az, ADDON_KIND_COMPONENT_CACHE as b, toReadonlyFieldTree as b0, validateFieldAddons as b1, validatePageNesting as b2, walkAndValidateAddons as b3, withCustomAddon as b4, withEventFormValue as b5, withLegacyStatusClasses as b6, withLoggerConfig as b7, withPreviousValue as b8, withValidationExecutionDefaults as b9, withValueExclusionDefaults as ba, wrapperProps as bb, writeToFieldValue as bc, ADDON_KIND_DEFINITIONS as c, ADDON_KIND_REGISTRY as d, ARRAY_CONTEXT as e, AppendArrayItemEvent as f, ArrayItemRegistryService as g, ComponentAddonComponent as h, ComponentInitializedEvent as i, ConsoleLogger as j, DEFAULT_VALIDATION_MESSAGES as k, DEFAULT_WRAPPERS as l, DF_FIELD_TEMPLATES as m, DfAddonSlot as n, DfFieldOutlet as o, DfTemplate as p, DynamicForm as q, DynamicFormError as r, DynamicFormLogger as s, DynamicTextPipe as t, EventDispatcher as u, FIELD_SIGNAL_CONTEXT as v, FORM_OPTIONS as w, FormClearEvent as x, FormResetEvent as y, InsertArrayItemEvent as z };
|
|
18171
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map
|