@ng-forge/dynamic-forms 0.9.0-next.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CltYGHg9.mjs → ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs} +171 -73
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs} +12 -8
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-BTiqZ8Np.mjs → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-BTiqZ8Np.mjs.map → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-mdfySFjX.mjs → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs} +5 -5
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bt3kvlOY.mjs → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bt3kvlOY.mjs.map → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BxswrhOc.mjs → ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs} +36 -12
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +936 -54
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-QdV2vcCx.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs} +2423 -466
- 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-n4KTMOTm.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs +34 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Cf1mEKVW.mjs → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Cf1mEKVW.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 +2 -2
- package/types/ng-forge-dynamic-forms-integration.d.ts +587 -11
- package/types/ng-forge-dynamic-forms.d.ts +3841 -2428
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CltYGHg9.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-mdfySFjX.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BxswrhOc.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-QdV2vcCx.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs +0 -110
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +0 -34
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +0 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, computed, signal, Injector, isDevMode, afterRenderEffect, input, Directive, untracked, InjectionToken, ElementRef, EnvironmentInjector, runInInjectionContext } from '@angular/core';
|
|
3
|
+
import { injectFieldSignalContext, omit, buildBaseInputs, DEFAULT_PROPS, resolveNonFieldHidden, resolveNonFieldDisabled, RootFormRegistryService, ARRAY_CONTEXT, DynamicFormLogger, AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, FORM_OPTIONS, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement, DEFAULT_VALIDATION_MESSAGES, EventBus, resolveTokens, resolveDynamicValue, ADDON_ACTION_REGISTRY, writeToFieldValue } from '@ng-forge/dynamic-forms';
|
|
4
|
+
export { createWrappers, isEqual, isWrappersBundle, resolveNonFieldDisabled, resolveNonFieldHidden, wrapperProps } from '@ng-forge/dynamic-forms';
|
|
4
5
|
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
|
5
6
|
import { combineLatest, of } from 'rxjs';
|
|
6
7
|
import { switchMap, map } from 'rxjs/operators';
|
|
8
|
+
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
9
|
+
import { TestBed } from '@angular/core/testing';
|
|
10
|
+
import { form, schema } from '@angular/forms/signals';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
|
|
@@ -20,7 +24,7 @@ import { switchMap, map } from 'rxjs/operators';
|
|
|
20
24
|
* @returns The value field context with reactive field tree accessor
|
|
21
25
|
*/
|
|
22
26
|
function resolveValueFieldContext() {
|
|
23
|
-
const context =
|
|
27
|
+
const context = injectFieldSignalContext();
|
|
24
28
|
return {
|
|
25
29
|
getFieldTree: (fieldKey) => {
|
|
26
30
|
// Access context.form here (inside a function that will be called from a computed)
|
|
@@ -44,10 +48,9 @@ function resolveValueFieldContext() {
|
|
|
44
48
|
* @param fieldDef The value field definition
|
|
45
49
|
* @param ctx The resolved value field context
|
|
46
50
|
* @param defaultProps Default props from the form configuration (may be undefined if not configured)
|
|
47
|
-
* @param defaultValidationMessages Default validation messages from the form configuration (may be undefined if not configured)
|
|
48
51
|
* @returns Record of input names to values
|
|
49
52
|
*/
|
|
50
|
-
function buildValueFieldInputs(fieldDef, ctx, defaultProps
|
|
53
|
+
function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
|
|
51
54
|
const omittedFields = omit(fieldDef, ['value']);
|
|
52
55
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps);
|
|
53
56
|
const inputs = {
|
|
@@ -57,9 +60,6 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
|
|
|
57
60
|
if (fieldDef.placeholder !== undefined) {
|
|
58
61
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
59
62
|
}
|
|
60
|
-
if (defaultValidationMessages !== undefined) {
|
|
61
|
-
inputs['defaultValidationMessages'] = defaultValidationMessages;
|
|
62
|
-
}
|
|
63
63
|
// Resolve field tree reactively - this call is inside a computed,
|
|
64
64
|
// so it establishes the reactive dependency correctly
|
|
65
65
|
const fieldTree = ctx.getFieldTree(fieldDef.key);
|
|
@@ -92,13 +92,12 @@ function valueFieldMapper(fieldDef) {
|
|
|
92
92
|
// Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
|
|
93
93
|
const ctx = resolveValueFieldContext();
|
|
94
94
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
95
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
96
95
|
// The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
|
|
97
96
|
// 1. Accesses context.form (triggering the getter for array items)
|
|
98
97
|
// 2. Returns the field tree if available, or undefined if not yet
|
|
99
98
|
// 3. Re-runs when the form structure updates (reactive dependency)
|
|
100
99
|
return computed(() => {
|
|
101
|
-
return buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
100
|
+
return buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
102
101
|
});
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -112,13 +111,11 @@ function valueFieldMapper(fieldDef) {
|
|
|
112
111
|
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
113
112
|
*/
|
|
114
113
|
function checkboxFieldMapper(fieldDef) {
|
|
115
|
-
const context =
|
|
114
|
+
const context = injectFieldSignalContext();
|
|
116
115
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
117
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
118
116
|
return computed(() => {
|
|
119
117
|
const omittedFields = omit(fieldDef, ['value']);
|
|
120
118
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps());
|
|
121
|
-
const validationMessages = defaultValidationMessages();
|
|
122
119
|
const inputs = {
|
|
123
120
|
...baseInputs,
|
|
124
121
|
validationMessages: fieldDef.validationMessages ?? {},
|
|
@@ -126,9 +123,6 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
126
123
|
if (fieldDef.placeholder !== undefined) {
|
|
127
124
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
128
125
|
}
|
|
129
|
-
if (validationMessages !== undefined) {
|
|
130
|
-
inputs['defaultValidationMessages'] = validationMessages;
|
|
131
|
-
}
|
|
132
126
|
// Access form inside computed for reactivity and to handle cases where
|
|
133
127
|
// form may not be immediately available (e.g., during array item initialization)
|
|
134
128
|
const formRoot = context.form;
|
|
@@ -172,9 +166,8 @@ function toDate(value) {
|
|
|
172
166
|
function datepickerFieldMapper(fieldDef) {
|
|
173
167
|
const ctx = resolveValueFieldContext();
|
|
174
168
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
175
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
176
169
|
return computed(() => {
|
|
177
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
170
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
178
171
|
// Add datepicker-specific properties, converting strings to Date objects
|
|
179
172
|
if (fieldDef.minDate !== undefined) {
|
|
180
173
|
inputs['minDate'] = toDate(fieldDef.minDate);
|
|
@@ -200,10 +193,9 @@ function datepickerFieldMapper(fieldDef) {
|
|
|
200
193
|
function optionsFieldMapper(fieldDef) {
|
|
201
194
|
const ctx = resolveValueFieldContext();
|
|
202
195
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
203
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
204
196
|
return computed(() => {
|
|
205
197
|
// Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
|
|
206
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
198
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
207
199
|
// Add options property
|
|
208
200
|
inputs['options'] = fieldDef.options;
|
|
209
201
|
return inputs;
|
|
@@ -224,9 +216,8 @@ function optionsFieldMapper(fieldDef) {
|
|
|
224
216
|
function sliderFieldMapper(fieldDef) {
|
|
225
217
|
const ctx = resolveValueFieldContext();
|
|
226
218
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
227
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
228
219
|
return computed(() => {
|
|
229
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
220
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
230
221
|
// Add slider-specific properties (these are at field level, not in props)
|
|
231
222
|
if (fieldDef.minValue !== undefined) {
|
|
232
223
|
inputs['minValue'] = fieldDef.minValue;
|
|
@@ -570,7 +561,7 @@ function submitButtonFieldMapper(fieldDef) {
|
|
|
570
561
|
// =============================================================================
|
|
571
562
|
/** Mapper for next page button — preconfigures NextPageEvent. */
|
|
572
563
|
function nextButtonFieldMapper(fieldDef) {
|
|
573
|
-
const fieldSignalContext =
|
|
564
|
+
const fieldSignalContext = injectFieldSignalContext();
|
|
574
565
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
575
566
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
576
567
|
const formOptions = inject(FORM_OPTIONS);
|
|
@@ -730,6 +721,59 @@ function shouldShowErrors(field) {
|
|
|
730
721
|
});
|
|
731
722
|
}
|
|
732
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Resolve target set, compute combined attrs, apply via applyMetaToElement.
|
|
726
|
+
* Target resolution is cached by selector identity — re-resolves only on
|
|
727
|
+
* selector change or `dependents` change. afterRenderEffect is signal-tracked,
|
|
728
|
+
* so the callback re-runs only when computeAttrs / dependents differ.
|
|
729
|
+
* @internal
|
|
730
|
+
*/
|
|
731
|
+
function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
732
|
+
const appliedByElement = new WeakMap();
|
|
733
|
+
let cachedSelector;
|
|
734
|
+
let cachedTargets = [];
|
|
735
|
+
let previousAttrs;
|
|
736
|
+
let previousDeps;
|
|
737
|
+
const warnedSelectors = isDevMode() ? new Set() : undefined;
|
|
738
|
+
afterRenderEffect({
|
|
739
|
+
write: () => {
|
|
740
|
+
// Read dependents first so afterRenderEffect tracks them.
|
|
741
|
+
const currentDeps = options.dependents?.map((d) => d());
|
|
742
|
+
const attrs = computeAttrs();
|
|
743
|
+
const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;
|
|
744
|
+
const selectorChanged = selector !== cachedSelector;
|
|
745
|
+
const depsChanged = !!currentDeps &&
|
|
746
|
+
(!previousDeps || currentDeps.length !== previousDeps.length || currentDeps.some((d, i) => d !== previousDeps[i]));
|
|
747
|
+
if (selectorChanged || depsChanged || cachedTargets.length === 0) {
|
|
748
|
+
cachedSelector = selector;
|
|
749
|
+
cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];
|
|
750
|
+
}
|
|
751
|
+
previousDeps = currentDeps;
|
|
752
|
+
// Dev-mode safety net: selector typo or wrong descendant query leaves
|
|
753
|
+
// meta + aria unapplied. Warn the first time we'd apply non-empty attrs
|
|
754
|
+
// to zero matches for a given selector. Re-warns only when the selector
|
|
755
|
+
// string changes (so authors fixing one typo see the next).
|
|
756
|
+
if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
|
|
757
|
+
if (!warnedSelectors.has(selector)) {
|
|
758
|
+
warnedSelectors.add(selector);
|
|
759
|
+
const tag = elementRef.nativeElement.tagName.toLowerCase();
|
|
760
|
+
console.warn(`[Dynamic Forms] ngForgeControl selector "${selector}" matched no descendants of <${tag}> — meta + aria attributes will not be applied. ` +
|
|
761
|
+
`Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Bail when attrs unchanged AND nothing structural moved — keeps repeated
|
|
765
|
+
// effect runs (driven by an unrelated tracked-signal flicker) cheap.
|
|
766
|
+
if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
previousAttrs = attrs;
|
|
770
|
+
for (const el of cachedTargets) {
|
|
771
|
+
const applied = appliedByElement.get(el) ?? new Set();
|
|
772
|
+
appliedByElement.set(el, applyMetaToElement(el, attrs, applied));
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
}
|
|
733
777
|
/**
|
|
734
778
|
* Sets up automatic meta attribute tracking and application for wrapped components.
|
|
735
779
|
*
|
|
@@ -768,42 +812,880 @@ function shouldShowErrors(field) {
|
|
|
768
812
|
* ```
|
|
769
813
|
*/
|
|
770
814
|
function setupMetaTracking(elementRef, meta, options) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
815
|
+
applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Marker-directive variant: writes parent meta + aria to resolved targets.
|
|
819
|
+
* Parent aria wins on key collision. Internal to NgForgeControl/HostControl.
|
|
820
|
+
* @internal
|
|
821
|
+
*/
|
|
822
|
+
function setupMarkerAttrTracking(elementRef, parent, selector) {
|
|
823
|
+
applyTrackedAttrsToTargets(elementRef, () => {
|
|
824
|
+
const ariaRequired = parent.ariaRequired();
|
|
825
|
+
const ariaDescribedBy = parent.ariaDescribedBy();
|
|
826
|
+
// Skip null aria-required/describedby so applyMetaToElement removes the
|
|
827
|
+
// attr (rather than serializing "null") on transitions.
|
|
828
|
+
return {
|
|
829
|
+
...(parent.meta() ?? {}),
|
|
830
|
+
'aria-invalid': parent.ariaInvalid(),
|
|
831
|
+
...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),
|
|
832
|
+
...(ariaDescribedBy === null ? {} : { 'aria-describedby': ariaDescribedBy }),
|
|
833
|
+
};
|
|
834
|
+
}, { selector });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Creates a signal that computes the aria-describedby value based on errors and hint state.
|
|
839
|
+
* Errors take precedence over hints - when errors are displayed, the hint is hidden.
|
|
840
|
+
* Only the first error is displayed (single error ID, not indexed).
|
|
841
|
+
*
|
|
842
|
+
* @param errorsToDisplay Signal containing the array of errors currently being displayed
|
|
843
|
+
* @param errorId Signal containing the ID for the error element (single error only)
|
|
844
|
+
* @param hintId Signal containing the ID for the hint element
|
|
845
|
+
* @param hasHint Function that returns true if a hint is configured
|
|
846
|
+
* @returns Signal containing the aria-describedby value or null
|
|
847
|
+
*/
|
|
848
|
+
function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
|
|
849
|
+
return computed(() => {
|
|
850
|
+
if (errorsToDisplay().length > 0) {
|
|
851
|
+
return errorId();
|
|
852
|
+
}
|
|
853
|
+
if (hasHint()) {
|
|
854
|
+
return hintId();
|
|
855
|
+
}
|
|
856
|
+
return null;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Error display utilities for field components
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Base directive composed into every ng-forge component via `hostDirectives`.
|
|
864
|
+
* Owns the universal identity inputs (`key`, `className`) and the host
|
|
865
|
+
* bindings that every field shape — value-bearing, action, future container —
|
|
866
|
+
* shares: `[id]`, `[attr.data-testid]`, `[class]`.
|
|
867
|
+
*
|
|
868
|
+
* Shape-specific add-ons (`NgForgeField` for value fields, `NgForgeAction`
|
|
869
|
+
* for actions) inject this directive and read `key()` / `className()` from
|
|
870
|
+
* it. Source-of-truth concerns like `hidden` / `disabled` live on the add-on
|
|
871
|
+
* because they vary by shape (value fields derive from `field()()`; actions
|
|
872
|
+
* receive direct inputs).
|
|
873
|
+
*
|
|
874
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
875
|
+
* `NG_FORGE_FIELD` / `NG_FORGE_ACTION` presets rather than composing Shell
|
|
876
|
+
* directly.
|
|
877
|
+
*/
|
|
878
|
+
class NgForgeFieldShell {
|
|
879
|
+
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
880
|
+
className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
881
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldShell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
882
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeFieldShell, isStandalone: true, inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "key()", "attr.data-testid": "key()", "class": "className()" } }, ngImport: i0 });
|
|
883
|
+
}
|
|
884
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldShell, decorators: [{
|
|
885
|
+
type: Directive,
|
|
886
|
+
args: [{
|
|
887
|
+
host: {
|
|
888
|
+
'[id]': 'key()',
|
|
889
|
+
'[attr.data-testid]': 'key()',
|
|
890
|
+
'[class]': 'className()',
|
|
891
|
+
},
|
|
892
|
+
}]
|
|
893
|
+
}], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }] } });
|
|
894
|
+
/** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */
|
|
895
|
+
const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'];
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via
|
|
899
|
+
* the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every
|
|
900
|
+
* value-bearing field needs.
|
|
901
|
+
*
|
|
902
|
+
* The Shell owns universal identity (`key`, `className`, `[id]`,
|
|
903
|
+
* `[attr.data-testid]`, `[class]`); this directive injects Shell and adds
|
|
904
|
+
* field-driven bindings (`[attr.hidden]`, `[attr.aria-disabled]`), the
|
|
905
|
+
* standard value-field inputs (`field`, `label`, `placeholder`, `tabIndex`,
|
|
906
|
+
* `props`, `meta`, `validationMessages`), the derived error/aria signals,
|
|
907
|
+
* and the meta-tracking claim contract.
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```ts
|
|
911
|
+
* \@Component({
|
|
912
|
+
* hostDirectives: NG_FORGE_FIELD,
|
|
913
|
+
* imports: [NgForgeControl, FormField],
|
|
914
|
+
* template: `
|
|
915
|
+
* <label [for]="ngf.key() + '-input'">{{ ngf.label() | dynamicText | async }}</label>
|
|
916
|
+
* <input ngForgeControl [id]="ngf.key() + '-input'" [formField]="ngf.field()" />
|
|
917
|
+
* @if (ngf.errorsToDisplay()[0]; as e) {
|
|
918
|
+
* <span [id]="ngf.errorId()">{{ e.message }}</span>
|
|
919
|
+
* }
|
|
920
|
+
* `,
|
|
921
|
+
* })
|
|
922
|
+
* export class MyInputField {
|
|
923
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
924
|
+
* }
|
|
925
|
+
* ```
|
|
926
|
+
*
|
|
927
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
928
|
+
* `NG_FORGE_FIELD` preset over composing this directive manually.
|
|
929
|
+
*/
|
|
930
|
+
class NgForgeField {
|
|
931
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
932
|
+
// Shell-owned identity — re-exported so component templates keep using
|
|
933
|
+
// `ngf.key()` / `ngf.className()` without reaching for the Shell directly.
|
|
934
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
935
|
+
shell = inject(NgForgeFieldShell);
|
|
936
|
+
/** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */
|
|
937
|
+
key = this.shell.key;
|
|
938
|
+
/** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */
|
|
939
|
+
className = this.shell.className;
|
|
940
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
941
|
+
// Forwarded inputs — the value-field-specific subset of mapper output.
|
|
942
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
943
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
944
|
+
label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
945
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
946
|
+
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
947
|
+
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
948
|
+
meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
949
|
+
validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : /* istanbul ignore next */ []));
|
|
950
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
951
|
+
// Wiring
|
|
952
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
953
|
+
defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
|
|
954
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
955
|
+
// Error display
|
|
956
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
957
|
+
errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
|
|
958
|
+
showErrors = shouldShowErrors(this.field);
|
|
959
|
+
errorsToDisplay = computed(() => (this.showErrors() ? this.errors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : /* istanbul ignore next */ []));
|
|
960
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
961
|
+
// Accessibility helpers
|
|
962
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
963
|
+
errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
964
|
+
hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
965
|
+
ariaInvalid = computed(() => {
|
|
966
|
+
const state = this.field()();
|
|
967
|
+
return state.invalid() && state.touched();
|
|
968
|
+
}, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : /* istanbul ignore next */ []));
|
|
969
|
+
ariaRequired = computed(() => {
|
|
970
|
+
return this.field()().required?.() === true ? true : null;
|
|
971
|
+
}, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : /* istanbul ignore next */ []));
|
|
972
|
+
ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => Boolean(this.props()?.hint));
|
|
973
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
974
|
+
// Claim tracking — markers and ambient sub-components flip this to true so
|
|
975
|
+
// dev-mode can warn when meta() is non-empty but nothing consumed it.
|
|
976
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
977
|
+
_claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
|
|
978
|
+
/** @internal Marker directives + ambient sub-components call this on construction. */
|
|
979
|
+
markClaimed() {
|
|
980
|
+
this._claimed.set(true);
|
|
981
|
+
}
|
|
982
|
+
logger = inject(DynamicFormLogger, { optional: true });
|
|
983
|
+
constructor() {
|
|
984
|
+
// Dev-mode safety net: a mapper or template-author binds meta() but no
|
|
985
|
+
// NgForgeControl / NgForgeHostControl / ambient sub-component is in
|
|
986
|
+
// place to consume it. Fires at most once per directive instance.
|
|
987
|
+
if (isDevMode()) {
|
|
988
|
+
let warned = false;
|
|
989
|
+
afterRenderEffect({
|
|
990
|
+
write: () => {
|
|
991
|
+
if (warned)
|
|
992
|
+
return;
|
|
993
|
+
const meta = this.meta();
|
|
994
|
+
if (!meta || Object.keys(meta).length === 0)
|
|
995
|
+
return;
|
|
996
|
+
if (untracked(this._claimed))
|
|
997
|
+
return;
|
|
998
|
+
warned = true;
|
|
999
|
+
// `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).
|
|
1000
|
+
const key = untracked(this.key);
|
|
1001
|
+
const message = `NgForgeField - meta() provided for field "${key}" but no NgForgeControl / NgForgeHostControl / ` +
|
|
1002
|
+
`ambient sub-component claimed it. Meta will not be applied. ` +
|
|
1003
|
+
`See https://ng-forge.dev/building-an-adapter#meta-forwarding`;
|
|
1004
|
+
if (this.logger) {
|
|
1005
|
+
this.logger.warn(message);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
console.warn(message);
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1015
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeField, isStandalone: true, inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.hidden": "field()().hidden() || null", "attr.aria-disabled": "field()().disabled() || null" } }, ngImport: i0 });
|
|
1016
|
+
}
|
|
1017
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeField, decorators: [{
|
|
1018
|
+
type: Directive,
|
|
1019
|
+
args: [{
|
|
1020
|
+
host: {
|
|
1021
|
+
'[attr.hidden]': 'field()().hidden() || null',
|
|
1022
|
+
'[attr.aria-disabled]': 'field()().disabled() || null',
|
|
1023
|
+
},
|
|
1024
|
+
}]
|
|
1025
|
+
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }] } });
|
|
1026
|
+
/**
|
|
1027
|
+
* Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes
|
|
1028
|
+
* `key` / `className` (forwarded to `NgForgeFieldShell` separately via
|
|
1029
|
+
* `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
|
|
1030
|
+
* directly — use the `NG_FORGE_FIELD` preset.
|
|
1031
|
+
*/
|
|
1032
|
+
const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'];
|
|
1033
|
+
// Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared
|
|
1034
|
+
// `input()` properties on NgForgeField. Drift in either direction fails the build.
|
|
1035
|
+
const _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP = true;
|
|
1036
|
+
void _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;
|
|
1037
|
+
/**
|
|
1038
|
+
* Typed wrapper around `inject(NgForgeField)`. The generic narrows the
|
|
1039
|
+
* `field` signal to `Signal<FieldTree<T>>` for the calling component's value
|
|
1040
|
+
* type. The cast is unchecked — the runtime contract is that mappers only
|
|
1041
|
+
* forward a `field` whose value type matches the field-type definition.
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```ts
|
|
1045
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
1046
|
+
* // ngf.field() is Signal<FieldTree<string>>; ngf.errors() etc. unchanged
|
|
1047
|
+
* ```
|
|
1048
|
+
*/
|
|
1049
|
+
function injectNgForgeField() {
|
|
1050
|
+
return inject(NgForgeField);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Action add-on directive — composed alongside `NgForgeFieldShell` via the
|
|
1055
|
+
* `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /
|
|
1056
|
+
* action component needs (label, disabled, event, eventArgs, …) plus the
|
|
1057
|
+
* `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own
|
|
1058
|
+
* inputs (no field tree).
|
|
1059
|
+
*
|
|
1060
|
+
* Components call `action.dispatch()` from their click handler; this
|
|
1061
|
+
* directive resolves any `eventArgs` tokens via the optional `ARRAY_CONTEXT`
|
|
1062
|
+
* and dispatches the configured `event` through the `EventBus`.
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```ts
|
|
1066
|
+
* \@Component({
|
|
1067
|
+
* hostDirectives: NG_FORGE_ACTION,
|
|
1068
|
+
* template: `<button (click)="action.dispatch()" [disabled]="action.disabled()">{{ action.label() }}</button>`,
|
|
1069
|
+
* })
|
|
1070
|
+
* export class MyButton {
|
|
1071
|
+
* protected readonly action = inject(NgForgeAction);
|
|
1072
|
+
* }
|
|
1073
|
+
* ```
|
|
1074
|
+
*
|
|
1075
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
1076
|
+
* `NG_FORGE_ACTION` preset over composing this directive manually.
|
|
1077
|
+
*/
|
|
1078
|
+
class NgForgeAction {
|
|
1079
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1080
|
+
// Shell-owned identity — re-exported as plain `Signal<T>` so they don't get
|
|
1081
|
+
// flagged by the lockstep assertion below.
|
|
1082
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1083
|
+
shell = inject(NgForgeFieldShell);
|
|
1084
|
+
key = this.shell.key;
|
|
1085
|
+
className = this.shell.className;
|
|
1086
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1087
|
+
// Forwarded inputs — the action-specific subset of mapper output.
|
|
1088
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1089
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1090
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1091
|
+
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1092
|
+
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
1093
|
+
event = input(...(ngDevMode ? [undefined, { debugName: "event" }] : /* istanbul ignore next */ []));
|
|
1094
|
+
eventArgs = input(...(ngDevMode ? [undefined, { debugName: "eventArgs" }] : /* istanbul ignore next */ []));
|
|
1095
|
+
eventContext = input(...(ngDevMode ? [undefined, { debugName: "eventContext" }] : /* istanbul ignore next */ []));
|
|
1096
|
+
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
1097
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1098
|
+
// Dispatch
|
|
1099
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1100
|
+
eventBus = inject(EventBus);
|
|
1101
|
+
arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
1102
|
+
/**
|
|
1103
|
+
* Dispatch the configured event through the EventBus. Resolves `eventArgs`
|
|
1104
|
+
* tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the
|
|
1105
|
+
* ambient array context if present, falling back to a static `eventContext`
|
|
1106
|
+
* input or the field's own `key` otherwise. No-op when `event` is unset
|
|
1107
|
+
* (e.g. submit buttons handled by the native form lifecycle).
|
|
1108
|
+
*/
|
|
1109
|
+
dispatch() {
|
|
1110
|
+
const event = this.event();
|
|
1111
|
+
if (!event)
|
|
1112
|
+
return;
|
|
1113
|
+
const args = this.eventArgs();
|
|
1114
|
+
if (!args || args.length === 0) {
|
|
1115
|
+
this.eventBus.dispatch(event);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const context = this.arrayContext
|
|
1119
|
+
? {
|
|
1120
|
+
key: this.key(),
|
|
1121
|
+
index: this.arrayContext.index(),
|
|
1122
|
+
arrayKey: this.arrayContext.arrayKey,
|
|
1123
|
+
formValue: this.arrayContext.formValue,
|
|
1124
|
+
}
|
|
1125
|
+
: (this.eventContext() ?? { key: this.key() });
|
|
1126
|
+
this.eventBus.dispatch(event, ...resolveTokens(args, context));
|
|
1127
|
+
}
|
|
1128
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1129
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAction, isStandalone: true, inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.hidden": "hidden() || null", "attr.aria-disabled": "disabled() || null" } }, ngImport: i0 });
|
|
1130
|
+
}
|
|
1131
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAction, decorators: [{
|
|
1132
|
+
type: Directive,
|
|
1133
|
+
args: [{
|
|
1134
|
+
host: {
|
|
1135
|
+
'[attr.hidden]': 'hidden() || null',
|
|
1136
|
+
'[attr.aria-disabled]': 'disabled() || null',
|
|
1137
|
+
},
|
|
1138
|
+
}]
|
|
1139
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], event: [{ type: i0.Input, args: [{ isSignal: true, alias: "event", required: false }] }], eventArgs: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventArgs", required: false }] }], eventContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventContext", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }] } });
|
|
1140
|
+
/**
|
|
1141
|
+
* Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes
|
|
1142
|
+
* `key` / `className` (forwarded to `NgForgeFieldShell` separately via
|
|
1143
|
+
* `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
|
|
1144
|
+
* directly — use the `NG_FORGE_ACTION` preset.
|
|
1145
|
+
*/
|
|
1146
|
+
const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'];
|
|
1147
|
+
// Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared
|
|
1148
|
+
// `input()` properties on NgForgeAction. Drift fails the build.
|
|
1149
|
+
const _NG_FORGE_ACTION_INPUTS_LOCKSTEP = true;
|
|
1150
|
+
void _NG_FORGE_ACTION_INPUTS_LOCKSTEP;
|
|
1151
|
+
/**
|
|
1152
|
+
* Typed wrapper around `inject(NgForgeAction)`. The generic narrows the
|
|
1153
|
+
* `event` input to a specific FormEvent subclass.
|
|
1154
|
+
*
|
|
1155
|
+
* @example
|
|
1156
|
+
* ```ts
|
|
1157
|
+
* protected readonly action = injectNgForgeAction<SubmitEvent>();
|
|
1158
|
+
* ```
|
|
1159
|
+
*/
|
|
1160
|
+
function injectNgForgeAction() {
|
|
1161
|
+
return inject(NgForgeAction);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Universal addon-array plumbing: signal-cached `hidden` resolution and
|
|
1166
|
+
* slot bucketing. Used internally by the `NgForgeAddons` host wrapper —
|
|
1167
|
+
* consumers should compose `NgForgeAddons` directly and inject the
|
|
1168
|
+
* directive instance via `injectNgForgeAddons<TAddon>()`.
|
|
1169
|
+
*/
|
|
1170
|
+
class NgForgeAddonsBase {
|
|
1171
|
+
/** Capture the host's injector so `toSignal` (inside `resolveDynamicValue`) ties to the host's `DestroyRef`. */
|
|
1172
|
+
hostInjector = inject(Injector);
|
|
1173
|
+
/** Addon array mapped from `FieldDef.addons` by `buildBaseInputs`. */
|
|
1174
|
+
addons = input(undefined, ...(ngDevMode ? [{ debugName: "addons" }] : /* istanbul ignore next */ []));
|
|
1175
|
+
/**
|
|
1176
|
+
* Per-addon `hidden` signal cache. Built via `explicitEffect` rather
|
|
1177
|
+
* than `computed` because `toSignal` (inside `resolveDynamicValue` for
|
|
1178
|
+
* Observable inputs) is disallowed in reactive contexts (NG0602).
|
|
1179
|
+
*/
|
|
1180
|
+
_hiddenSignalCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_hiddenSignalCache" }] : /* istanbul ignore next */ []));
|
|
1181
|
+
hiddenSignalCache = this._hiddenSignalCache.asReadonly();
|
|
1182
|
+
visibleAddons = computed(() => {
|
|
1183
|
+
const list = this.addons() ?? [];
|
|
1184
|
+
const cache = this.hiddenSignalCache();
|
|
1185
|
+
return list.filter((a) => !(cache.get(a)?.() ?? false));
|
|
1186
|
+
}, ...(ngDevMode ? [{ debugName: "visibleAddons" }] : /* istanbul ignore next */ []));
|
|
1187
|
+
hasAddons = computed(() => this.visibleAddons().length > 0, ...(ngDevMode ? [{ debugName: "hasAddons" }] : /* istanbul ignore next */ []));
|
|
1188
|
+
prefixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'), ...(ngDevMode ? [{ debugName: "prefixAddons" }] : /* istanbul ignore next */ []));
|
|
1189
|
+
suffixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'suffix'), ...(ngDevMode ? [{ debugName: "suffixAddons" }] : /* istanbul ignore next */ []));
|
|
1190
|
+
/**
|
|
1191
|
+
* Visible addons grouped by their `slot` string. Convenience view for
|
|
1192
|
+
* adapters with non-universal slots (e.g., Ionic `start` / `end`).
|
|
1193
|
+
*/
|
|
1194
|
+
addonsBySlot = computed(() => {
|
|
1195
|
+
const map = new Map();
|
|
1196
|
+
for (const a of this.visibleAddons()) {
|
|
1197
|
+
const bucket = map.get(a.slot) ?? [];
|
|
1198
|
+
bucket.push(a);
|
|
1199
|
+
map.set(a.slot, bucket);
|
|
1200
|
+
}
|
|
1201
|
+
return map;
|
|
1202
|
+
}, ...(ngDevMode ? [{ debugName: "addonsBySlot" }] : /* istanbul ignore next */ []));
|
|
1203
|
+
/** Secondary index keyed by `hidden` value identity. Catches the common case
|
|
1204
|
+
* where a FormConfig re-emit produces structurally-different-but-content-
|
|
1205
|
+
* identical addon objects — without this, every re-emit would spawn a new
|
|
1206
|
+
* `toSignal` subscription for each addon's hidden axis. */
|
|
1207
|
+
_hiddenByValue = new WeakMap();
|
|
1208
|
+
constructor() {
|
|
1209
|
+
explicitEffect([this.addons], ([list]) => {
|
|
1210
|
+
const prev = this._hiddenSignalCache();
|
|
1211
|
+
const next = new Map();
|
|
1212
|
+
for (const a of list ?? []) {
|
|
1213
|
+
// Tier 1: reference identity — the cheapest hit when the same addon
|
|
1214
|
+
// object is reused across emissions.
|
|
1215
|
+
let resolved = prev.get(a);
|
|
1216
|
+
if (!resolved) {
|
|
1217
|
+
// Tier 2: hidden-value identity — when `hidden` is an object
|
|
1218
|
+
// (Signal / Observable / function), the same instance can be passed
|
|
1219
|
+
// through multiple new addon objects on FormConfig re-emit.
|
|
1220
|
+
const h = a.hidden;
|
|
1221
|
+
const valueKey = typeof h === 'object' && h !== null ? h : null;
|
|
1222
|
+
resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
|
|
1223
|
+
if (!resolved) {
|
|
1224
|
+
resolved = resolveDynamicValue(a.hidden, false, this.hostInjector);
|
|
1225
|
+
if (valueKey)
|
|
1226
|
+
this._hiddenByValue.set(valueKey, resolved);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
next.set(a, resolved);
|
|
1230
|
+
}
|
|
1231
|
+
this._hiddenSignalCache.set(next);
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonsBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1235
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAddonsBase, isStandalone: true, inputs: { addons: { classPropertyName: "addons", publicName: "addons", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1236
|
+
}
|
|
1237
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonsBase, decorators: [{
|
|
1238
|
+
type: Directive,
|
|
1239
|
+
args: [{}]
|
|
1240
|
+
}], ctorParameters: () => [], propDecorators: { addons: [{ type: i0.Input, args: [{ isSignal: true, alias: "addons", required: false }] }] } });
|
|
1241
|
+
/** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */
|
|
1242
|
+
const NG_FORGE_ADDONS_INPUTS = ['addons'];
|
|
1243
|
+
// Compile-time lockstep — drift between the tuple and the declared inputs fails the build.
|
|
1244
|
+
const _NG_FORGE_ADDONS_INPUTS_LOCKSTEP = true;
|
|
1245
|
+
void _NG_FORGE_ADDONS_INPUTS_LOCKSTEP;
|
|
1246
|
+
/**
|
|
1247
|
+
* Host wrapper for `NgForgeAddonsBase`. Consumers compose this directly:
|
|
1248
|
+
*
|
|
1249
|
+
* ```ts
|
|
1250
|
+
* \@Component({
|
|
1251
|
+
* hostDirectives: [NgForgeFieldHost, NgForgeAddons],
|
|
1252
|
+
* template: `
|
|
1253
|
+
* @if (ngfa.hasAddons()) {
|
|
1254
|
+
* <p-inputgroup>
|
|
1255
|
+
* @for (a of ngfa.prefixAddons(); track $index) {
|
|
1256
|
+
* <df-addon-slot [addon]="a" [hidden]="ngfa.hiddenSignalCache().get(a)" />
|
|
1257
|
+
* }
|
|
1258
|
+
* <input ngForgeControl ... />
|
|
1259
|
+
* </p-inputgroup>
|
|
1260
|
+
* }
|
|
1261
|
+
* `,
|
|
1262
|
+
* })
|
|
1263
|
+
* export class MyInputField {
|
|
1264
|
+
* protected readonly ngfa = injectNgForgeAddons<MyInputAddon>();
|
|
1265
|
+
* }
|
|
1266
|
+
* ```
|
|
1267
|
+
*
|
|
1268
|
+
* The wrapper exists so `hostDirectives: [NgForgeAddons]` resolves
|
|
1269
|
+
* cross-package — same constraint that drives `NgForgeFieldHost`.
|
|
1270
|
+
*/
|
|
1271
|
+
class NgForgeAddons {
|
|
1272
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1273
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddons, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonsBase, inputs: ["addons", "addons"] }], ngImport: i0 });
|
|
1274
|
+
}
|
|
1275
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, decorators: [{
|
|
1276
|
+
type: Directive,
|
|
1277
|
+
args: [{
|
|
1278
|
+
hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],
|
|
1279
|
+
}]
|
|
1280
|
+
}] });
|
|
1281
|
+
/**
|
|
1282
|
+
* Typed inject helper. Always resolves to the underlying
|
|
1283
|
+
* `NgForgeAddonsBase` instance — the wrapper class has no instance
|
|
1284
|
+
* fields of its own.
|
|
1285
|
+
*/
|
|
1286
|
+
function injectNgForgeAddons() {
|
|
1287
|
+
return inject(NgForgeAddonsBase);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const ADDON_PRESET_HANDLER = new InjectionToken('ADDON_PRESET_HANDLER');
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Universal click-dispatch plumbing for action-style addons (icon buttons,
|
|
1294
|
+
* labelled buttons, etc.): `disabled` / `loading` resolution from
|
|
1295
|
+
* `DynamicValue<boolean>`, action context construction, and the
|
|
1296
|
+
* `preset / actionRef / action` precedence dispatcher.
|
|
1297
|
+
*
|
|
1298
|
+
* Adapter button kinds (e.g., `prime-button`) compose `NgForgeAddonAction`
|
|
1299
|
+
* and read state via `injectNgForgeAddonAction<TAddon>()`. Preset semantics
|
|
1300
|
+
* are adapter-specific — register an `ADDON_PRESET_HANDLER` at the host
|
|
1301
|
+
* field scope to receive preset dispatches.
|
|
1302
|
+
*/
|
|
1303
|
+
class NgForgeAddonActionBase {
|
|
1304
|
+
// The action registry is form-scoped. `optional: true` matches the
|
|
1305
|
+
// preset handler so addons rendered outside a form that called
|
|
1306
|
+
// `provideAddonActions(...)` degrade gracefully instead of throwing —
|
|
1307
|
+
// `actionRef` dispatches then log an actionable warning and no-op.
|
|
1308
|
+
actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });
|
|
1309
|
+
presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });
|
|
1310
|
+
logger = inject(DynamicFormLogger);
|
|
1311
|
+
hostInjector = inject(Injector);
|
|
1312
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
1313
|
+
fieldInputs = input(undefined, ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
1314
|
+
/**
|
|
1315
|
+
* Per-value caches for `disabled` / `loading` signal resolution. Keyed by
|
|
1316
|
+
* the `DynamicValue` identity so a single `toSignal` subscription is
|
|
1317
|
+
* shared across addon-reference swaps that pass the same Observable.
|
|
1318
|
+
* Mirrors `NgForgeAddonsBase._hiddenByValue` — `resolveDynamicValue`
|
|
1319
|
+
* cannot run inside `computed`/`linkedSignal.computation` because
|
|
1320
|
+
* `toSignal` throws in reactive contexts (NG0602), and re-running it on
|
|
1321
|
+
* every recomputation would leak subscriptions on each cycle.
|
|
1322
|
+
*/
|
|
1323
|
+
_disabledByValue = new WeakMap();
|
|
1324
|
+
_loadingByValue = new WeakMap();
|
|
1325
|
+
_resolvedDisabled = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedDisabled" }] : /* istanbul ignore next */ []));
|
|
1326
|
+
_resolvedLoading = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedLoading" }] : /* istanbul ignore next */ []));
|
|
1327
|
+
disabled = computed(() => this._resolvedDisabled()?.() ?? false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1328
|
+
loading = computed(() => this._resolvedLoading()?.() ?? false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1329
|
+
constructor() {
|
|
1330
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
1331
|
+
this._resolvedDisabled.set(this._resolveAxis(addon.disabled, this._disabledByValue));
|
|
1332
|
+
this._resolvedLoading.set(this._resolveAxis(addon.loading, this._loadingByValue));
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
_resolveAxis(value, cache) {
|
|
1336
|
+
const valueKey = typeof value === 'object' && value !== null ? value : null;
|
|
1337
|
+
let resolved = valueKey ? cache.get(valueKey) : undefined;
|
|
1338
|
+
if (!resolved) {
|
|
1339
|
+
resolved = resolveDynamicValue(value, false, this.hostInjector);
|
|
1340
|
+
if (valueKey)
|
|
1341
|
+
cache.set(valueKey, resolved);
|
|
1342
|
+
}
|
|
1343
|
+
return resolved;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Resolve the click handler from the addon's three-axis click variant and
|
|
1347
|
+
* invoke it. Precedence on multi-set: `preset > actionRef > action`. The
|
|
1348
|
+
* validator drops multi-set configs at config-init time; this is
|
|
1349
|
+
* defence-in-depth + a logged warning so the misconfiguration is loud.
|
|
1350
|
+
*/
|
|
1351
|
+
dispatch() {
|
|
1352
|
+
const addon = this.addon();
|
|
1353
|
+
const ctx = this.buildActionContext();
|
|
1354
|
+
const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;
|
|
1355
|
+
if (variantsSet > 1) {
|
|
1356
|
+
this.logger.warn(`Addon button: more than one of preset/actionRef/action configured — dispatching by precedence (preset > actionRef > action). Ship at most one to silence this.`);
|
|
1357
|
+
}
|
|
1358
|
+
if (addon.preset !== undefined) {
|
|
1359
|
+
if (!this.presetHandler) {
|
|
1360
|
+
this.logger.warn(`Addon button: preset '${addon.preset}' is configured but no ADDON_PRESET_HANDLER is registered in this scope. Did the adapter forget to provide one at the host field?`);
|
|
784
1361
|
return;
|
|
785
1362
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const applied = applyMetaToElement(el, currentMeta, appliedAttributes);
|
|
794
|
-
applied.forEach((attr) => allApplied.add(attr));
|
|
795
|
-
});
|
|
796
|
-
appliedAttributes = allApplied;
|
|
1363
|
+
void this.presetHandler.run(addon.preset, ctx);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (addon.actionRef !== undefined) {
|
|
1367
|
+
const handler = this.actionRegistry?.get(addon.actionRef);
|
|
1368
|
+
if (handler) {
|
|
1369
|
+
handler(ctx);
|
|
797
1370
|
}
|
|
798
1371
|
else {
|
|
799
|
-
|
|
800
|
-
|
|
1372
|
+
this.logger.warn(`Addon button: actionRef '${addon.actionRef}' is not registered. ` +
|
|
1373
|
+
(this.actionRegistry
|
|
1374
|
+
? `Did you call provideAddonActions({ ${addon.actionRef}: ... })?`
|
|
1375
|
+
: `No provideAddonActions(...) registry is reachable in this scope — was the addon rendered outside a form?`));
|
|
801
1376
|
}
|
|
802
|
-
|
|
803
|
-
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (addon.action !== undefined) {
|
|
1380
|
+
addon.action(ctx);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
// Decorative — no handler configured.
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Build an `AddonActionContext` for handlers from the wrapper-style
|
|
1387
|
+
* `fieldInputs` bag. Returns the field-bound variant when a host field
|
|
1388
|
+
* is reachable, or the orphan variant otherwise.
|
|
1389
|
+
*
|
|
1390
|
+
* Resolution order for `setValue`:
|
|
1391
|
+
* 1. `inputs.setValue` from the forwarded bag (preferred — populated by
|
|
1392
|
+
* `buildFieldInputs` in `DfFieldOutlet`).
|
|
1393
|
+
* 2. Fallback reconstructed from `inputs.field` itself: although
|
|
1394
|
+
* `ReadonlyFieldTree.value` is type-narrowed to `Signal<T>`, the
|
|
1395
|
+
* underlying runtime object is the same `WritableSignal<T>` created
|
|
1396
|
+
* by Signal Forms (the read-only wrapping is type-level only, see
|
|
1397
|
+
* `toReadonlyFieldTree`). Casting back at the writer boundary
|
|
1398
|
+
* produces a working setValue for ANY field depth — top-level OR
|
|
1399
|
+
* nested under groups. This avoids the previous FSC-key navigation
|
|
1400
|
+
* that broke for grouped fields, where `inputs.key` is DOM-mangled
|
|
1401
|
+
* (`'user_address_street'`) and `fsc.form[key]` returns undefined.
|
|
1402
|
+
*
|
|
1403
|
+
* The orphan variant fires only when the bag carries no field tree at
|
|
1404
|
+
* all — genuine "rendered outside a field" scenario.
|
|
1405
|
+
*/
|
|
1406
|
+
buildActionContext() {
|
|
1407
|
+
const inputs = this.fieldInputs();
|
|
1408
|
+
const key = inputs?.key ?? '';
|
|
1409
|
+
const type = inputs?.type ?? '';
|
|
1410
|
+
const tree = inputs?.field;
|
|
1411
|
+
if (tree) {
|
|
1412
|
+
// `writeToFieldValue` centralises the `Signal → WritableSignal` cast and
|
|
1413
|
+
// warns at runtime if Signal Forms ever stops storing a writable signal
|
|
1414
|
+
// here (the alternative — a silent no-op preset — is much worse).
|
|
1415
|
+
const setValue = inputs?.setValue ?? ((next) => writeToFieldValue(tree.value, next, this.logger));
|
|
1416
|
+
return {
|
|
1417
|
+
field: { key, type },
|
|
1418
|
+
form: tree,
|
|
1419
|
+
value: tree.value() ?? undefined,
|
|
1420
|
+
setValue,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
field: { key, type },
|
|
1425
|
+
form: null,
|
|
1426
|
+
value: undefined,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonActionBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1430
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAddonActionBase, isStandalone: true, 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 });
|
|
1431
|
+
}
|
|
1432
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonActionBase, decorators: [{
|
|
1433
|
+
type: Directive,
|
|
1434
|
+
args: [{}]
|
|
1435
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
1436
|
+
/** Forwarded onto `NgForgeAddonActionBase` via the `NgForgeAddonAction` wrapper. */
|
|
1437
|
+
const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'];
|
|
1438
|
+
const _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP = true;
|
|
1439
|
+
void _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP;
|
|
1440
|
+
/**
|
|
1441
|
+
* Host wrapper for `NgForgeAddonActionBase`. Consumers compose this directly:
|
|
1442
|
+
*
|
|
1443
|
+
* ```ts
|
|
1444
|
+
* \@Component({
|
|
1445
|
+
* hostDirectives: [NgForgeAddonAction],
|
|
1446
|
+
* template: `
|
|
1447
|
+
* <p-button [loading]="action.loading()" [disabled]="action.disabled()"
|
|
1448
|
+
* (onClick)="action.dispatch()" />
|
|
1449
|
+
* `,
|
|
1450
|
+
* })
|
|
1451
|
+
* export class MyButtonAddon {
|
|
1452
|
+
* protected readonly action = injectNgForgeAddonAction<MyButtonAddon>();
|
|
1453
|
+
* }
|
|
1454
|
+
* ```
|
|
1455
|
+
*/
|
|
1456
|
+
class NgForgeAddonAction {
|
|
1457
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1458
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddonAction, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: ["addon", "addon", "fieldInputs", "fieldInputs"] }], ngImport: i0 });
|
|
1459
|
+
}
|
|
1460
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, decorators: [{
|
|
1461
|
+
type: Directive,
|
|
1462
|
+
args: [{
|
|
1463
|
+
hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: [...NG_FORGE_ADDON_ACTION_INPUTS] }],
|
|
1464
|
+
}]
|
|
1465
|
+
}] });
|
|
1466
|
+
function injectNgForgeAddonAction() {
|
|
1467
|
+
return inject(NgForgeAddonActionBase);
|
|
804
1468
|
}
|
|
805
1469
|
|
|
806
|
-
|
|
1470
|
+
/**
|
|
1471
|
+
* Marker directive applied to the canonical control element of an
|
|
1472
|
+
* `NgForgeField`-using component. Forwards the field's `meta` AND
|
|
1473
|
+
* parent-derived aria signals (`aria-invalid`, `aria-required`,
|
|
1474
|
+
* `aria-describedby`) onto a target element.
|
|
1475
|
+
*
|
|
1476
|
+
* By default attributes land on the directive's own host. For wrapped
|
|
1477
|
+
* controls (e.g. `<mat-checkbox>`, `<p-toggleSwitch>`) where the canonical
|
|
1478
|
+
* native input is rendered as a descendant inside the wrapper, pass a CSS
|
|
1479
|
+
* selector through the input alias — the directive caches the resolved
|
|
1480
|
+
* descendant set and re-resolves only when the selector changes.
|
|
1481
|
+
*
|
|
1482
|
+
* @example Bare native control
|
|
1483
|
+
* ```html
|
|
1484
|
+
* <input ngForgeControl [formField]="ngf.field()" />
|
|
1485
|
+
* ```
|
|
1486
|
+
*
|
|
1487
|
+
* @example Wrapped control with inner native input
|
|
1488
|
+
* ```html
|
|
1489
|
+
* <mat-checkbox ngForgeControl="input[type='checkbox']" [formField]="ngf.field()">
|
|
1490
|
+
* {{ ngf.label() }}
|
|
1491
|
+
* </mat-checkbox>
|
|
1492
|
+
* ```
|
|
1493
|
+
*
|
|
1494
|
+
* @example Dynamic option list (radios rendered in @for)
|
|
1495
|
+
* ```html
|
|
1496
|
+
* @for (option of options(); track option.value) {
|
|
1497
|
+
* <input type="radio" ngForgeControl [value]="option.value" />
|
|
1498
|
+
* }
|
|
1499
|
+
* ```
|
|
1500
|
+
*
|
|
1501
|
+
* Each `@for` iteration creates its own directive instance; Angular's
|
|
1502
|
+
* structural lifecycle handles add/remove naturally.
|
|
1503
|
+
*/
|
|
1504
|
+
class NgForgeControl {
|
|
1505
|
+
target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'ngForgeControl' });
|
|
1506
|
+
constructor() {
|
|
1507
|
+
const parent = inject(NgForgeField);
|
|
1508
|
+
setupMarkerAttrTracking(inject(ElementRef), parent, () => this.target());
|
|
1509
|
+
parent.markClaimed();
|
|
1510
|
+
}
|
|
1511
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1512
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeControl, isStandalone: true, selector: "[ngForgeControl]", inputs: { target: { classPropertyName: "target", publicName: "ngForgeControl", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1513
|
+
}
|
|
1514
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeControl, decorators: [{
|
|
1515
|
+
type: Directive,
|
|
1516
|
+
args: [{ selector: '[ngForgeControl]' }]
|
|
1517
|
+
}], ctorParameters: () => [], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngForgeControl", required: false }] }] } });
|
|
1518
|
+
/**
|
|
1519
|
+
* Selectorless companion directive for shadow-DOM-encapsulated components
|
|
1520
|
+
* (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child
|
|
1521
|
+
* is the canonical control. Added to the component's `hostDirectives` so the
|
|
1522
|
+
* field's `meta` + parent aria land on the component's own host element.
|
|
1523
|
+
*
|
|
1524
|
+
* For split parent/control components (e.g. `df-prime-select` rendering
|
|
1525
|
+
* `df-prime-select-control` internally), place `NgForgeHostControl` on the
|
|
1526
|
+
* INNER control component's `hostDirectives` — the inner element is the
|
|
1527
|
+
* canonical control, and the marker's `inject(NgForgeField)` walks up the
|
|
1528
|
+
* element-injector tree to find the parent's directive instance.
|
|
1529
|
+
*
|
|
1530
|
+
* @example
|
|
1531
|
+
* ```ts
|
|
1532
|
+
* \@Component({
|
|
1533
|
+
* hostDirectives: [
|
|
1534
|
+
* { directive: NgForgeField, inputs: [...NG_FORGE_FIELD_INPUTS] },
|
|
1535
|
+
* NgForgeHostControl,
|
|
1536
|
+
* ],
|
|
1537
|
+
* template: `...`,
|
|
1538
|
+
* })
|
|
1539
|
+
* export class IonicInputField { ... }
|
|
1540
|
+
* ```
|
|
1541
|
+
*/
|
|
1542
|
+
// Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.
|
|
1543
|
+
class NgForgeHostControl {
|
|
1544
|
+
constructor() {
|
|
1545
|
+
const parent = inject(NgForgeField);
|
|
1546
|
+
setupMarkerAttrTracking(inject(ElementRef), parent, () => undefined);
|
|
1547
|
+
parent.markClaimed();
|
|
1548
|
+
}
|
|
1549
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeHostControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1550
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeHostControl, isStandalone: true, ngImport: i0 });
|
|
1551
|
+
}
|
|
1552
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeHostControl, decorators: [{
|
|
1553
|
+
type: Directive,
|
|
1554
|
+
args: [{}]
|
|
1555
|
+
}], ctorParameters: () => [] });
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal
|
|
1559
|
+
* identity) with `NgForgeField` (value + validation + aria + meta-tracking)
|
|
1560
|
+
* in a single host-directive entry, so consumer components compose just one
|
|
1561
|
+
* directive instead of two.
|
|
1562
|
+
*
|
|
1563
|
+
* Why a directive instead of a `const` preset: cross-package partial
|
|
1564
|
+
* compilation can't resolve a const reference inside `hostDirectives:`, so
|
|
1565
|
+
* an exported const array doesn't work for library consumers. A wrapper
|
|
1566
|
+
* directive's own `hostDirectives` IS a literal at compile time (in this
|
|
1567
|
+
* file), so it works everywhere.
|
|
1568
|
+
*
|
|
1569
|
+
* Authoring shape for adapter components:
|
|
1570
|
+
* ```ts
|
|
1571
|
+
* \@Component({
|
|
1572
|
+
* hostDirectives: [NgForgeFieldHost],
|
|
1573
|
+
* ...
|
|
1574
|
+
* })
|
|
1575
|
+
* export class MyInputField {
|
|
1576
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
1577
|
+
* }
|
|
1578
|
+
* ```
|
|
1579
|
+
*
|
|
1580
|
+
* Pattern reference: Taiga UI uses the same wrapper-directive technique
|
|
1581
|
+
* for the same reason. See https://www.angularspace.com/host-directives-decomposition-unleashed/
|
|
1582
|
+
*/
|
|
1583
|
+
class NgForgeFieldHost {
|
|
1584
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1585
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeFieldHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeField, inputs: ["field", "field", "label", "label", "placeholder", "placeholder", "tabIndex", "tabIndex", "props", "props", "meta", "meta", "validationMessages", "validationMessages"] }], ngImport: i0 });
|
|
1586
|
+
}
|
|
1587
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, decorators: [{
|
|
1588
|
+
type: Directive,
|
|
1589
|
+
args: [{
|
|
1590
|
+
hostDirectives: [
|
|
1591
|
+
{ directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
|
|
1592
|
+
{ directive: NgForgeField, inputs: [...NG_FORGE_VALUE_FIELD_INPUTS] },
|
|
1593
|
+
],
|
|
1594
|
+
}]
|
|
1595
|
+
}] });
|
|
1596
|
+
/**
|
|
1597
|
+
* Action / button host wrapper. Composes `NgForgeFieldShell` with
|
|
1598
|
+
* `NgForgeAction` (event dispatch + aria-disabled) for button-shaped
|
|
1599
|
+
* components.
|
|
1600
|
+
*
|
|
1601
|
+
* Authoring shape:
|
|
1602
|
+
* ```ts
|
|
1603
|
+
* \@Component({
|
|
1604
|
+
* hostDirectives: [NgForgeActionHost],
|
|
1605
|
+
* ...
|
|
1606
|
+
* })
|
|
1607
|
+
* export class MyButton {
|
|
1608
|
+
* protected readonly action = injectNgForgeAction();
|
|
1609
|
+
* }
|
|
1610
|
+
* ```
|
|
1611
|
+
*/
|
|
1612
|
+
class NgForgeActionHost {
|
|
1613
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1614
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeActionHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeAction, inputs: ["label", "label", "disabled", "disabled", "hidden", "hidden", "tabIndex", "tabIndex", "event", "event", "eventArgs", "eventArgs", "eventContext", "eventContext", "props", "props"] }], ngImport: i0 });
|
|
1615
|
+
}
|
|
1616
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, decorators: [{
|
|
1617
|
+
type: Directive,
|
|
1618
|
+
args: [{
|
|
1619
|
+
hostDirectives: [
|
|
1620
|
+
{ directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
|
|
1621
|
+
{ directive: NgForgeAction, inputs: [...NG_FORGE_ACTION_INPUTS] },
|
|
1622
|
+
],
|
|
1623
|
+
}]
|
|
1624
|
+
}] });
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Builds a `ComponentFixture` for an `NgForgeField`-composing component with
|
|
1628
|
+
* `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that
|
|
1629
|
+
* Shell's required `key` and Field's required `field` would otherwise throw.
|
|
1630
|
+
*
|
|
1631
|
+
* The caller drives `detectChanges()` themselves (so they can `setInput`
|
|
1632
|
+
* additional component-specific props in between).
|
|
1633
|
+
*/
|
|
1634
|
+
function createNgForgeFieldFixture(component, options) {
|
|
1635
|
+
TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });
|
|
1636
|
+
const key = options.key;
|
|
1637
|
+
const rootValue = signal({ [key]: options.value }, ...(ngDevMode ? [{ debugName: "rootValue" }] : /* istanbul ignore next */ []));
|
|
1638
|
+
const injector = TestBed.inject(EnvironmentInjector);
|
|
1639
|
+
const schemaCallback = options.schema;
|
|
1640
|
+
const root = runInInjectionContext(injector, () => {
|
|
1641
|
+
if (!schemaCallback)
|
|
1642
|
+
return form(rootValue);
|
|
1643
|
+
return form(rootValue, schema((path) => {
|
|
1644
|
+
schemaCallback(path[key]);
|
|
1645
|
+
}));
|
|
1646
|
+
});
|
|
1647
|
+
const field = root[key];
|
|
1648
|
+
if (options.touched)
|
|
1649
|
+
field().markAsTouched();
|
|
1650
|
+
const fixture = TestBed.createComponent(component);
|
|
1651
|
+
fixture.componentRef.setInput('field', field);
|
|
1652
|
+
fixture.componentRef.setInput('key', key);
|
|
1653
|
+
if (options.inputs) {
|
|
1654
|
+
for (const [name, value] of Object.entries(options.inputs)) {
|
|
1655
|
+
fixture.componentRef.setInput(name, value);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return { fixture, field, rootValue };
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Builds a `ComponentFixture` for an `NgForgeAction`-composing component with
|
|
1662
|
+
* `key` + `label` bound and `EventBus` provided. The caller calls
|
|
1663
|
+
* `detectChanges()`; the harness handles the NG0950-avoidance bindings.
|
|
1664
|
+
*/
|
|
1665
|
+
function createNgForgeActionFixture(component, options) {
|
|
1666
|
+
TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });
|
|
1667
|
+
const fixture = TestBed.createComponent(component);
|
|
1668
|
+
fixture.componentRef.setInput('key', options.key);
|
|
1669
|
+
fixture.componentRef.setInput('label', options.label);
|
|
1670
|
+
if (options.event)
|
|
1671
|
+
fixture.componentRef.setInput('event', options.event);
|
|
1672
|
+
if (options.eventArgs)
|
|
1673
|
+
fixture.componentRef.setInput('eventArgs', options.eventArgs);
|
|
1674
|
+
if (options.disabled !== undefined)
|
|
1675
|
+
fixture.componentRef.setInput('disabled', options.disabled);
|
|
1676
|
+
if (options.hidden !== undefined)
|
|
1677
|
+
fixture.componentRef.setInput('hidden', options.hidden);
|
|
1678
|
+
if (options.inputs) {
|
|
1679
|
+
for (const [name, value] of Object.entries(options.inputs)) {
|
|
1680
|
+
fixture.componentRef.setInput(name, value);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return { fixture, eventBus: TestBed.inject(EventBus) };
|
|
1684
|
+
}
|
|
1685
|
+
/** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */
|
|
1686
|
+
function provideTestValidationMessages(messages) {
|
|
1687
|
+
return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };
|
|
1688
|
+
}
|
|
807
1689
|
|
|
808
1690
|
/**
|
|
809
1691
|
* @ng-forge/dynamic-forms/integration
|
|
@@ -824,5 +1706,5 @@ function setupMetaTracking(elementRef, meta, options) {
|
|
|
824
1706
|
* Generated bundle index. Do not edit.
|
|
825
1707
|
*/
|
|
826
1708
|
|
|
827
|
-
export { addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
1709
|
+
export { ADDON_PRESET_HANDLER, NG_FORGE_ACTION_INPUTS, NG_FORGE_ADDONS_INPUTS, NG_FORGE_ADDON_ACTION_INPUTS, NG_FORGE_FIELD_SHELL_INPUTS, NG_FORGE_VALUE_FIELD_INPUTS, NgForgeAction, NgForgeActionHost, NgForgeAddonAction, NgForgeAddonActionBase, NgForgeAddons, NgForgeAddonsBase, NgForgeControl, NgForgeField, NgForgeFieldHost, NgForgeFieldShell, NgForgeHostControl, addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createAriaDescribedBySignal, createNgForgeActionFixture, createNgForgeFieldFixture, createResolvedErrorsSignal, datepickerFieldMapper, injectNgForgeAction, injectNgForgeAddonAction, injectNgForgeAddons, injectNgForgeField, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, provideTestValidationMessages, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
828
1710
|
//# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map
|