@ng-forge/dynamic-forms 0.8.0 → 0.9.0-next.10
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-CWxCWVdz.mjs → ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CWxCWVdz.mjs.map → ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs → ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs.map → ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs → ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs.map → ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs → ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs} +2 -2
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs → ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs.map → ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs → ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs.map → ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +629 -54
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs} +662 -110
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs → ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs.map → ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs → ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs.map → ng-forge-dynamic-forms-text-field.component-B4LnTnIA.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 +380 -8
- package/types/ng-forge-dynamic-forms.d.ts +106 -8
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs.map +0 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, computed, signal, Injector, isDevMode, afterRenderEffect, input, Directive, untracked, ElementRef, EnvironmentInjector, runInInjectionContext } from '@angular/core';
|
|
3
|
+
import { FIELD_SIGNAL_CONTEXT, 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 } 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 { TestBed } from '@angular/core/testing';
|
|
9
|
+
import { form, schema } from '@angular/forms/signals';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
|
|
@@ -44,10 +47,9 @@ function resolveValueFieldContext() {
|
|
|
44
47
|
* @param fieldDef The value field definition
|
|
45
48
|
* @param ctx The resolved value field context
|
|
46
49
|
* @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
50
|
* @returns Record of input names to values
|
|
49
51
|
*/
|
|
50
|
-
function buildValueFieldInputs(fieldDef, ctx, defaultProps
|
|
52
|
+
function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
|
|
51
53
|
const omittedFields = omit(fieldDef, ['value']);
|
|
52
54
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps);
|
|
53
55
|
const inputs = {
|
|
@@ -57,9 +59,6 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
|
|
|
57
59
|
if (fieldDef.placeholder !== undefined) {
|
|
58
60
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
59
61
|
}
|
|
60
|
-
if (defaultValidationMessages !== undefined) {
|
|
61
|
-
inputs['defaultValidationMessages'] = defaultValidationMessages;
|
|
62
|
-
}
|
|
63
62
|
// Resolve field tree reactively - this call is inside a computed,
|
|
64
63
|
// so it establishes the reactive dependency correctly
|
|
65
64
|
const fieldTree = ctx.getFieldTree(fieldDef.key);
|
|
@@ -92,13 +91,12 @@ function valueFieldMapper(fieldDef) {
|
|
|
92
91
|
// Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
|
|
93
92
|
const ctx = resolveValueFieldContext();
|
|
94
93
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
95
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
96
94
|
// The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
|
|
97
95
|
// 1. Accesses context.form (triggering the getter for array items)
|
|
98
96
|
// 2. Returns the field tree if available, or undefined if not yet
|
|
99
97
|
// 3. Re-runs when the form structure updates (reactive dependency)
|
|
100
98
|
return computed(() => {
|
|
101
|
-
return buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
99
|
+
return buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
102
100
|
});
|
|
103
101
|
}
|
|
104
102
|
|
|
@@ -114,11 +112,9 @@ function valueFieldMapper(fieldDef) {
|
|
|
114
112
|
function checkboxFieldMapper(fieldDef) {
|
|
115
113
|
const context = inject(FIELD_SIGNAL_CONTEXT);
|
|
116
114
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
117
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
118
115
|
return computed(() => {
|
|
119
116
|
const omittedFields = omit(fieldDef, ['value']);
|
|
120
117
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps());
|
|
121
|
-
const validationMessages = defaultValidationMessages();
|
|
122
118
|
const inputs = {
|
|
123
119
|
...baseInputs,
|
|
124
120
|
validationMessages: fieldDef.validationMessages ?? {},
|
|
@@ -126,9 +122,6 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
126
122
|
if (fieldDef.placeholder !== undefined) {
|
|
127
123
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
128
124
|
}
|
|
129
|
-
if (validationMessages !== undefined) {
|
|
130
|
-
inputs['defaultValidationMessages'] = validationMessages;
|
|
131
|
-
}
|
|
132
125
|
// Access form inside computed for reactivity and to handle cases where
|
|
133
126
|
// form may not be immediately available (e.g., during array item initialization)
|
|
134
127
|
const formRoot = context.form;
|
|
@@ -172,9 +165,8 @@ function toDate(value) {
|
|
|
172
165
|
function datepickerFieldMapper(fieldDef) {
|
|
173
166
|
const ctx = resolveValueFieldContext();
|
|
174
167
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
175
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
176
168
|
return computed(() => {
|
|
177
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
169
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
178
170
|
// Add datepicker-specific properties, converting strings to Date objects
|
|
179
171
|
if (fieldDef.minDate !== undefined) {
|
|
180
172
|
inputs['minDate'] = toDate(fieldDef.minDate);
|
|
@@ -200,10 +192,9 @@ function datepickerFieldMapper(fieldDef) {
|
|
|
200
192
|
function optionsFieldMapper(fieldDef) {
|
|
201
193
|
const ctx = resolveValueFieldContext();
|
|
202
194
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
203
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
204
195
|
return computed(() => {
|
|
205
196
|
// Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
|
|
206
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
197
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
207
198
|
// Add options property
|
|
208
199
|
inputs['options'] = fieldDef.options;
|
|
209
200
|
return inputs;
|
|
@@ -224,9 +215,8 @@ function optionsFieldMapper(fieldDef) {
|
|
|
224
215
|
function sliderFieldMapper(fieldDef) {
|
|
225
216
|
const ctx = resolveValueFieldContext();
|
|
226
217
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
227
|
-
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
228
218
|
return computed(() => {
|
|
229
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps()
|
|
219
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
230
220
|
// Add slider-specific properties (these are at field level, not in props)
|
|
231
221
|
if (fieldDef.minValue !== undefined) {
|
|
232
222
|
inputs['minValue'] = fieldDef.minValue;
|
|
@@ -730,6 +720,59 @@ function shouldShowErrors(field) {
|
|
|
730
720
|
});
|
|
731
721
|
}
|
|
732
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Resolve target set, compute combined attrs, apply via applyMetaToElement.
|
|
725
|
+
* Target resolution is cached by selector identity — re-resolves only on
|
|
726
|
+
* selector change or `dependents` change. afterRenderEffect is signal-tracked,
|
|
727
|
+
* so the callback re-runs only when computeAttrs / dependents differ.
|
|
728
|
+
* @internal
|
|
729
|
+
*/
|
|
730
|
+
function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
731
|
+
const appliedByElement = new WeakMap();
|
|
732
|
+
let cachedSelector;
|
|
733
|
+
let cachedTargets = [];
|
|
734
|
+
let previousAttrs;
|
|
735
|
+
let previousDeps;
|
|
736
|
+
const warnedSelectors = isDevMode() ? new Set() : undefined;
|
|
737
|
+
afterRenderEffect({
|
|
738
|
+
write: () => {
|
|
739
|
+
// Read dependents first so afterRenderEffect tracks them.
|
|
740
|
+
const currentDeps = options.dependents?.map((d) => d());
|
|
741
|
+
const attrs = computeAttrs();
|
|
742
|
+
const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;
|
|
743
|
+
const selectorChanged = selector !== cachedSelector;
|
|
744
|
+
const depsChanged = !!currentDeps &&
|
|
745
|
+
(!previousDeps || currentDeps.length !== previousDeps.length || currentDeps.some((d, i) => d !== previousDeps[i]));
|
|
746
|
+
if (selectorChanged || depsChanged || cachedTargets.length === 0) {
|
|
747
|
+
cachedSelector = selector;
|
|
748
|
+
cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];
|
|
749
|
+
}
|
|
750
|
+
previousDeps = currentDeps;
|
|
751
|
+
// Dev-mode safety net: selector typo or wrong descendant query leaves
|
|
752
|
+
// meta + aria unapplied. Warn the first time we'd apply non-empty attrs
|
|
753
|
+
// to zero matches for a given selector. Re-warns only when the selector
|
|
754
|
+
// string changes (so authors fixing one typo see the next).
|
|
755
|
+
if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
|
|
756
|
+
if (!warnedSelectors.has(selector)) {
|
|
757
|
+
warnedSelectors.add(selector);
|
|
758
|
+
const tag = elementRef.nativeElement.tagName.toLowerCase();
|
|
759
|
+
console.warn(`[Dynamic Forms] ngForgeControl selector "${selector}" matched no descendants of <${tag}> — meta + aria attributes will not be applied. ` +
|
|
760
|
+
`Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Bail when attrs unchanged AND nothing structural moved — keeps repeated
|
|
764
|
+
// effect runs (driven by an unrelated tracked-signal flicker) cheap.
|
|
765
|
+
if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
previousAttrs = attrs;
|
|
769
|
+
for (const el of cachedTargets) {
|
|
770
|
+
const applied = appliedByElement.get(el) ?? new Set();
|
|
771
|
+
appliedByElement.set(el, applyMetaToElement(el, attrs, applied));
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
}
|
|
733
776
|
/**
|
|
734
777
|
* Sets up automatic meta attribute tracking and application for wrapped components.
|
|
735
778
|
*
|
|
@@ -768,43 +811,575 @@ function shouldShowErrors(field) {
|
|
|
768
811
|
* ```
|
|
769
812
|
*/
|
|
770
813
|
function setupMetaTracking(elementRef, meta, options) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
814
|
+
applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Marker-directive variant: writes parent meta + aria to resolved targets.
|
|
818
|
+
* Parent aria wins on key collision. Internal to NgForgeControl/HostControl.
|
|
819
|
+
* @internal
|
|
820
|
+
*/
|
|
821
|
+
function setupMarkerAttrTracking(elementRef, parent, selector) {
|
|
822
|
+
applyTrackedAttrsToTargets(elementRef, () => {
|
|
823
|
+
const ariaRequired = parent.ariaRequired();
|
|
824
|
+
const ariaDescribedBy = parent.ariaDescribedBy();
|
|
825
|
+
// Skip null aria-required/describedby so applyMetaToElement removes the
|
|
826
|
+
// attr (rather than serializing "null") on transitions.
|
|
827
|
+
return {
|
|
828
|
+
...(parent.meta() ?? {}),
|
|
829
|
+
'aria-invalid': parent.ariaInvalid(),
|
|
830
|
+
...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),
|
|
831
|
+
...(ariaDescribedBy === null ? {} : { 'aria-describedby': ariaDescribedBy }),
|
|
832
|
+
};
|
|
833
|
+
}, { selector });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Creates a signal that computes the aria-describedby value based on errors and hint state.
|
|
838
|
+
* Errors take precedence over hints - when errors are displayed, the hint is hidden.
|
|
839
|
+
* Only the first error is displayed (single error ID, not indexed).
|
|
840
|
+
*
|
|
841
|
+
* @param errorsToDisplay Signal containing the array of errors currently being displayed
|
|
842
|
+
* @param errorId Signal containing the ID for the error element (single error only)
|
|
843
|
+
* @param hintId Signal containing the ID for the hint element
|
|
844
|
+
* @param hasHint Function that returns true if a hint is configured
|
|
845
|
+
* @returns Signal containing the aria-describedby value or null
|
|
846
|
+
*/
|
|
847
|
+
function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
|
|
848
|
+
return computed(() => {
|
|
849
|
+
if (errorsToDisplay().length > 0) {
|
|
850
|
+
return errorId();
|
|
851
|
+
}
|
|
852
|
+
if (hasHint()) {
|
|
853
|
+
return hintId();
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
803
856
|
});
|
|
804
857
|
}
|
|
805
858
|
|
|
806
859
|
// Error display utilities for field components
|
|
807
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Base directive composed into every ng-forge component via `hostDirectives`.
|
|
863
|
+
* Owns the universal identity inputs (`key`, `className`) and the host
|
|
864
|
+
* bindings that every field shape — value-bearing, action, future container —
|
|
865
|
+
* shares: `[id]`, `[attr.data-testid]`, `[class]`.
|
|
866
|
+
*
|
|
867
|
+
* Shape-specific add-ons (`NgForgeField` for value fields, `NgForgeAction`
|
|
868
|
+
* for actions) inject this directive and read `key()` / `className()` from
|
|
869
|
+
* it. Source-of-truth concerns like `hidden` / `disabled` live on the add-on
|
|
870
|
+
* because they vary by shape (value fields derive from `field()()`; actions
|
|
871
|
+
* receive direct inputs).
|
|
872
|
+
*
|
|
873
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
874
|
+
* `NG_FORGE_FIELD` / `NG_FORGE_ACTION` presets rather than composing Shell
|
|
875
|
+
* directly.
|
|
876
|
+
*/
|
|
877
|
+
class NgForgeFieldShell {
|
|
878
|
+
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
879
|
+
className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
880
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeFieldShell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
881
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", 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 });
|
|
882
|
+
}
|
|
883
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeFieldShell, decorators: [{
|
|
884
|
+
type: Directive,
|
|
885
|
+
args: [{
|
|
886
|
+
host: {
|
|
887
|
+
'[id]': 'key()',
|
|
888
|
+
'[attr.data-testid]': 'key()',
|
|
889
|
+
'[class]': 'className()',
|
|
890
|
+
},
|
|
891
|
+
}]
|
|
892
|
+
}], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }] } });
|
|
893
|
+
/** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */
|
|
894
|
+
const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'];
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via
|
|
898
|
+
* the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every
|
|
899
|
+
* value-bearing field needs.
|
|
900
|
+
*
|
|
901
|
+
* The Shell owns universal identity (`key`, `className`, `[id]`,
|
|
902
|
+
* `[attr.data-testid]`, `[class]`); this directive injects Shell and adds
|
|
903
|
+
* field-driven bindings (`[attr.hidden]`, `[attr.aria-disabled]`), the
|
|
904
|
+
* standard value-field inputs (`field`, `label`, `placeholder`, `tabIndex`,
|
|
905
|
+
* `props`, `meta`, `validationMessages`), the derived error/aria signals,
|
|
906
|
+
* and the meta-tracking claim contract.
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* ```ts
|
|
910
|
+
* \@Component({
|
|
911
|
+
* hostDirectives: NG_FORGE_FIELD,
|
|
912
|
+
* imports: [NgForgeControl, FormField],
|
|
913
|
+
* template: `
|
|
914
|
+
* <label [for]="ngf.key() + '-input'">{{ ngf.label() | dynamicText | async }}</label>
|
|
915
|
+
* <input ngForgeControl [id]="ngf.key() + '-input'" [formField]="ngf.field()" />
|
|
916
|
+
* @if (ngf.errorsToDisplay()[0]; as e) {
|
|
917
|
+
* <span [id]="ngf.errorId()">{{ e.message }}</span>
|
|
918
|
+
* }
|
|
919
|
+
* `,
|
|
920
|
+
* })
|
|
921
|
+
* export class MyInputField {
|
|
922
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
923
|
+
* }
|
|
924
|
+
* ```
|
|
925
|
+
*
|
|
926
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
927
|
+
* `NG_FORGE_FIELD` preset over composing this directive manually.
|
|
928
|
+
*/
|
|
929
|
+
class NgForgeField {
|
|
930
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
931
|
+
// Shell-owned identity — re-exported so component templates keep using
|
|
932
|
+
// `ngf.key()` / `ngf.className()` without reaching for the Shell directly.
|
|
933
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
934
|
+
shell = inject(NgForgeFieldShell);
|
|
935
|
+
/** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */
|
|
936
|
+
key = this.shell.key;
|
|
937
|
+
/** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */
|
|
938
|
+
className = this.shell.className;
|
|
939
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
940
|
+
// Forwarded inputs — the value-field-specific subset of mapper output.
|
|
941
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
942
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
943
|
+
label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
944
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
945
|
+
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
946
|
+
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
947
|
+
meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
948
|
+
validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : /* istanbul ignore next */ []));
|
|
949
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
950
|
+
// Wiring
|
|
951
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
952
|
+
defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
|
|
953
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
954
|
+
// Error display
|
|
955
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
956
|
+
errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
|
|
957
|
+
showErrors = shouldShowErrors(this.field);
|
|
958
|
+
errorsToDisplay = computed(() => (this.showErrors() ? this.errors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : /* istanbul ignore next */ []));
|
|
959
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
960
|
+
// Accessibility helpers
|
|
961
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
962
|
+
errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
963
|
+
hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
964
|
+
ariaInvalid = computed(() => {
|
|
965
|
+
const state = this.field()();
|
|
966
|
+
return state.invalid() && state.touched();
|
|
967
|
+
}, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : /* istanbul ignore next */ []));
|
|
968
|
+
ariaRequired = computed(() => {
|
|
969
|
+
return this.field()().required?.() === true ? true : null;
|
|
970
|
+
}, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : /* istanbul ignore next */ []));
|
|
971
|
+
ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => Boolean(this.props()?.hint));
|
|
972
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
973
|
+
// Claim tracking — markers and ambient sub-components flip this to true so
|
|
974
|
+
// dev-mode can warn when meta() is non-empty but nothing consumed it.
|
|
975
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
976
|
+
_claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
|
|
977
|
+
/** @internal Marker directives + ambient sub-components call this on construction. */
|
|
978
|
+
markClaimed() {
|
|
979
|
+
this._claimed.set(true);
|
|
980
|
+
}
|
|
981
|
+
logger = inject(DynamicFormLogger, { optional: true });
|
|
982
|
+
constructor() {
|
|
983
|
+
// Dev-mode safety net: a mapper or template-author binds meta() but no
|
|
984
|
+
// NgForgeControl / NgForgeHostControl / ambient sub-component is in
|
|
985
|
+
// place to consume it. Fires at most once per directive instance.
|
|
986
|
+
if (isDevMode()) {
|
|
987
|
+
let warned = false;
|
|
988
|
+
afterRenderEffect({
|
|
989
|
+
write: () => {
|
|
990
|
+
if (warned)
|
|
991
|
+
return;
|
|
992
|
+
const meta = this.meta();
|
|
993
|
+
if (!meta || Object.keys(meta).length === 0)
|
|
994
|
+
return;
|
|
995
|
+
if (untracked(this._claimed))
|
|
996
|
+
return;
|
|
997
|
+
warned = true;
|
|
998
|
+
// `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).
|
|
999
|
+
const key = untracked(this.key);
|
|
1000
|
+
const message = `NgForgeField - meta() provided for field "${key}" but no NgForgeControl / NgForgeHostControl / ` +
|
|
1001
|
+
`ambient sub-component claimed it. Meta will not be applied. ` +
|
|
1002
|
+
`See https://ng-forge.dev/building-an-adapter#meta-forwarding`;
|
|
1003
|
+
if (this.logger) {
|
|
1004
|
+
this.logger.warn(message);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
console.warn(message);
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1014
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", 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 });
|
|
1015
|
+
}
|
|
1016
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeField, decorators: [{
|
|
1017
|
+
type: Directive,
|
|
1018
|
+
args: [{
|
|
1019
|
+
host: {
|
|
1020
|
+
'[attr.hidden]': 'field()().hidden() || null',
|
|
1021
|
+
'[attr.aria-disabled]': 'field()().disabled() || null',
|
|
1022
|
+
},
|
|
1023
|
+
}]
|
|
1024
|
+
}], 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 }] }] } });
|
|
1025
|
+
/**
|
|
1026
|
+
* Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes
|
|
1027
|
+
* `key` / `className` (forwarded to `NgForgeFieldShell` separately via
|
|
1028
|
+
* `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
|
|
1029
|
+
* directly — use the `NG_FORGE_FIELD` preset.
|
|
1030
|
+
*/
|
|
1031
|
+
const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'];
|
|
1032
|
+
// Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared
|
|
1033
|
+
// `input()` properties on NgForgeField. Drift in either direction fails the build.
|
|
1034
|
+
const _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP = true;
|
|
1035
|
+
void _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;
|
|
1036
|
+
/**
|
|
1037
|
+
* Typed wrapper around `inject(NgForgeField)`. The generic narrows the
|
|
1038
|
+
* `field` signal to `Signal<FieldTree<T>>` for the calling component's value
|
|
1039
|
+
* type. The cast is unchecked — the runtime contract is that mappers only
|
|
1040
|
+
* forward a `field` whose value type matches the field-type definition.
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* ```ts
|
|
1044
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
1045
|
+
* // ngf.field() is Signal<FieldTree<string>>; ngf.errors() etc. unchanged
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
function injectNgForgeField() {
|
|
1049
|
+
return inject(NgForgeField);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Action add-on directive — composed alongside `NgForgeFieldShell` via the
|
|
1054
|
+
* `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /
|
|
1055
|
+
* action component needs (label, disabled, event, eventArgs, …) plus the
|
|
1056
|
+
* `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own
|
|
1057
|
+
* inputs (no field tree).
|
|
1058
|
+
*
|
|
1059
|
+
* Components call `action.dispatch()` from their click handler; this
|
|
1060
|
+
* directive resolves any `eventArgs` tokens via the optional `ARRAY_CONTEXT`
|
|
1061
|
+
* and dispatches the configured `event` through the `EventBus`.
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```ts
|
|
1065
|
+
* \@Component({
|
|
1066
|
+
* hostDirectives: NG_FORGE_ACTION,
|
|
1067
|
+
* template: `<button (click)="action.dispatch()" [disabled]="action.disabled()">{{ action.label() }}</button>`,
|
|
1068
|
+
* })
|
|
1069
|
+
* export class MyButton {
|
|
1070
|
+
* protected readonly action = inject(NgForgeAction);
|
|
1071
|
+
* }
|
|
1072
|
+
* ```
|
|
1073
|
+
*
|
|
1074
|
+
* Selectorless — usage is exclusively via `hostDirectives`. Prefer the
|
|
1075
|
+
* `NG_FORGE_ACTION` preset over composing this directive manually.
|
|
1076
|
+
*/
|
|
1077
|
+
class NgForgeAction {
|
|
1078
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1079
|
+
// Shell-owned identity — re-exported as plain `Signal<T>` so they don't get
|
|
1080
|
+
// flagged by the lockstep assertion below.
|
|
1081
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1082
|
+
shell = inject(NgForgeFieldShell);
|
|
1083
|
+
key = this.shell.key;
|
|
1084
|
+
className = this.shell.className;
|
|
1085
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1086
|
+
// Forwarded inputs — the action-specific subset of mapper output.
|
|
1087
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1088
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1089
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1090
|
+
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1091
|
+
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
1092
|
+
event = input(...(ngDevMode ? [undefined, { debugName: "event" }] : /* istanbul ignore next */ []));
|
|
1093
|
+
eventArgs = input(...(ngDevMode ? [undefined, { debugName: "eventArgs" }] : /* istanbul ignore next */ []));
|
|
1094
|
+
eventContext = input(...(ngDevMode ? [undefined, { debugName: "eventContext" }] : /* istanbul ignore next */ []));
|
|
1095
|
+
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
1096
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1097
|
+
// Dispatch
|
|
1098
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1099
|
+
eventBus = inject(EventBus);
|
|
1100
|
+
arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
1101
|
+
/**
|
|
1102
|
+
* Dispatch the configured event through the EventBus. Resolves `eventArgs`
|
|
1103
|
+
* tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the
|
|
1104
|
+
* ambient array context if present, falling back to a static `eventContext`
|
|
1105
|
+
* input or the field's own `key` otherwise. No-op when `event` is unset
|
|
1106
|
+
* (e.g. submit buttons handled by the native form lifecycle).
|
|
1107
|
+
*/
|
|
1108
|
+
dispatch() {
|
|
1109
|
+
const event = this.event();
|
|
1110
|
+
if (!event)
|
|
1111
|
+
return;
|
|
1112
|
+
const args = this.eventArgs();
|
|
1113
|
+
if (!args || args.length === 0) {
|
|
1114
|
+
this.eventBus.dispatch(event);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const context = this.arrayContext
|
|
1118
|
+
? {
|
|
1119
|
+
key: this.key(),
|
|
1120
|
+
index: this.arrayContext.index(),
|
|
1121
|
+
arrayKey: this.arrayContext.arrayKey,
|
|
1122
|
+
formValue: this.arrayContext.formValue,
|
|
1123
|
+
}
|
|
1124
|
+
: (this.eventContext() ?? { key: this.key() });
|
|
1125
|
+
this.eventBus.dispatch(event, ...resolveTokens(args, context));
|
|
1126
|
+
}
|
|
1127
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1128
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", 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 });
|
|
1129
|
+
}
|
|
1130
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeAction, decorators: [{
|
|
1131
|
+
type: Directive,
|
|
1132
|
+
args: [{
|
|
1133
|
+
host: {
|
|
1134
|
+
'[attr.hidden]': 'hidden() || null',
|
|
1135
|
+
'[attr.aria-disabled]': 'disabled() || null',
|
|
1136
|
+
},
|
|
1137
|
+
}]
|
|
1138
|
+
}], 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 }] }] } });
|
|
1139
|
+
/**
|
|
1140
|
+
* Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes
|
|
1141
|
+
* `key` / `className` (forwarded to `NgForgeFieldShell` separately via
|
|
1142
|
+
* `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
|
|
1143
|
+
* directly — use the `NG_FORGE_ACTION` preset.
|
|
1144
|
+
*/
|
|
1145
|
+
const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'];
|
|
1146
|
+
// Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared
|
|
1147
|
+
// `input()` properties on NgForgeAction. Drift fails the build.
|
|
1148
|
+
const _NG_FORGE_ACTION_INPUTS_LOCKSTEP = true;
|
|
1149
|
+
void _NG_FORGE_ACTION_INPUTS_LOCKSTEP;
|
|
1150
|
+
/**
|
|
1151
|
+
* Typed wrapper around `inject(NgForgeAction)`. The generic narrows the
|
|
1152
|
+
* `event` input to a specific FormEvent subclass.
|
|
1153
|
+
*
|
|
1154
|
+
* @example
|
|
1155
|
+
* ```ts
|
|
1156
|
+
* protected readonly action = injectNgForgeAction<SubmitEvent>();
|
|
1157
|
+
* ```
|
|
1158
|
+
*/
|
|
1159
|
+
function injectNgForgeAction() {
|
|
1160
|
+
return inject(NgForgeAction);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Marker directive applied to the canonical control element of an
|
|
1165
|
+
* `NgForgeField`-using component. Forwards the field's `meta` AND
|
|
1166
|
+
* parent-derived aria signals (`aria-invalid`, `aria-required`,
|
|
1167
|
+
* `aria-describedby`) onto a target element.
|
|
1168
|
+
*
|
|
1169
|
+
* By default attributes land on the directive's own host. For wrapped
|
|
1170
|
+
* controls (e.g. `<mat-checkbox>`, `<p-toggleSwitch>`) where the canonical
|
|
1171
|
+
* native input is rendered as a descendant inside the wrapper, pass a CSS
|
|
1172
|
+
* selector through the input alias — the directive caches the resolved
|
|
1173
|
+
* descendant set and re-resolves only when the selector changes.
|
|
1174
|
+
*
|
|
1175
|
+
* @example Bare native control
|
|
1176
|
+
* ```html
|
|
1177
|
+
* <input ngForgeControl [formField]="ngf.field()" />
|
|
1178
|
+
* ```
|
|
1179
|
+
*
|
|
1180
|
+
* @example Wrapped control with inner native input
|
|
1181
|
+
* ```html
|
|
1182
|
+
* <mat-checkbox ngForgeControl="input[type='checkbox']" [formField]="ngf.field()">
|
|
1183
|
+
* {{ ngf.label() }}
|
|
1184
|
+
* </mat-checkbox>
|
|
1185
|
+
* ```
|
|
1186
|
+
*
|
|
1187
|
+
* @example Dynamic option list (radios rendered in @for)
|
|
1188
|
+
* ```html
|
|
1189
|
+
* @for (option of options(); track option.value) {
|
|
1190
|
+
* <input type="radio" ngForgeControl [value]="option.value" />
|
|
1191
|
+
* }
|
|
1192
|
+
* ```
|
|
1193
|
+
*
|
|
1194
|
+
* Each `@for` iteration creates its own directive instance; Angular's
|
|
1195
|
+
* structural lifecycle handles add/remove naturally.
|
|
1196
|
+
*/
|
|
1197
|
+
class NgForgeControl {
|
|
1198
|
+
target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'ngForgeControl' });
|
|
1199
|
+
constructor() {
|
|
1200
|
+
const parent = inject(NgForgeField);
|
|
1201
|
+
setupMarkerAttrTracking(inject(ElementRef), parent, () => this.target());
|
|
1202
|
+
parent.markClaimed();
|
|
1203
|
+
}
|
|
1204
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1205
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", type: NgForgeControl, isStandalone: true, selector: "[ngForgeControl]", inputs: { target: { classPropertyName: "target", publicName: "ngForgeControl", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1206
|
+
}
|
|
1207
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeControl, decorators: [{
|
|
1208
|
+
type: Directive,
|
|
1209
|
+
args: [{ selector: '[ngForgeControl]' }]
|
|
1210
|
+
}], ctorParameters: () => [], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngForgeControl", required: false }] }] } });
|
|
1211
|
+
/**
|
|
1212
|
+
* Selectorless companion directive for shadow-DOM-encapsulated components
|
|
1213
|
+
* (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child
|
|
1214
|
+
* is the canonical control. Added to the component's `hostDirectives` so the
|
|
1215
|
+
* field's `meta` + parent aria land on the component's own host element.
|
|
1216
|
+
*
|
|
1217
|
+
* For split parent/control components (e.g. `df-prime-select` rendering
|
|
1218
|
+
* `df-prime-select-control` internally), place `NgForgeHostControl` on the
|
|
1219
|
+
* INNER control component's `hostDirectives` — the inner element is the
|
|
1220
|
+
* canonical control, and the marker's `inject(NgForgeField)` walks up the
|
|
1221
|
+
* element-injector tree to find the parent's directive instance.
|
|
1222
|
+
*
|
|
1223
|
+
* @example
|
|
1224
|
+
* ```ts
|
|
1225
|
+
* \@Component({
|
|
1226
|
+
* hostDirectives: [
|
|
1227
|
+
* { directive: NgForgeField, inputs: [...NG_FORGE_FIELD_INPUTS] },
|
|
1228
|
+
* NgForgeHostControl,
|
|
1229
|
+
* ],
|
|
1230
|
+
* template: `...`,
|
|
1231
|
+
* })
|
|
1232
|
+
* export class IonicInputField { ... }
|
|
1233
|
+
* ```
|
|
1234
|
+
*/
|
|
1235
|
+
// Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.
|
|
1236
|
+
class NgForgeHostControl {
|
|
1237
|
+
constructor() {
|
|
1238
|
+
const parent = inject(NgForgeField);
|
|
1239
|
+
setupMarkerAttrTracking(inject(ElementRef), parent, () => undefined);
|
|
1240
|
+
parent.markClaimed();
|
|
1241
|
+
}
|
|
1242
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeHostControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1243
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", type: NgForgeHostControl, isStandalone: true, ngImport: i0 });
|
|
1244
|
+
}
|
|
1245
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeHostControl, decorators: [{
|
|
1246
|
+
type: Directive,
|
|
1247
|
+
args: [{}]
|
|
1248
|
+
}], ctorParameters: () => [] });
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal
|
|
1252
|
+
* identity) with `NgForgeField` (value + validation + aria + meta-tracking)
|
|
1253
|
+
* in a single host-directive entry, so consumer components compose just one
|
|
1254
|
+
* directive instead of two.
|
|
1255
|
+
*
|
|
1256
|
+
* Why a directive instead of a `const` preset: cross-package partial
|
|
1257
|
+
* compilation can't resolve a const reference inside `hostDirectives:`, so
|
|
1258
|
+
* an exported const array doesn't work for library consumers. A wrapper
|
|
1259
|
+
* directive's own `hostDirectives` IS a literal at compile time (in this
|
|
1260
|
+
* file), so it works everywhere.
|
|
1261
|
+
*
|
|
1262
|
+
* Authoring shape for adapter components:
|
|
1263
|
+
* ```ts
|
|
1264
|
+
* \@Component({
|
|
1265
|
+
* hostDirectives: [NgForgeFieldHost],
|
|
1266
|
+
* ...
|
|
1267
|
+
* })
|
|
1268
|
+
* export class MyInputField {
|
|
1269
|
+
* protected readonly ngf = injectNgForgeField<string>();
|
|
1270
|
+
* }
|
|
1271
|
+
* ```
|
|
1272
|
+
*
|
|
1273
|
+
* Pattern reference: Taiga UI uses the same wrapper-directive technique
|
|
1274
|
+
* for the same reason. See https://www.angularspace.com/host-directives-decomposition-unleashed/
|
|
1275
|
+
*/
|
|
1276
|
+
class NgForgeFieldHost {
|
|
1277
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1278
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", 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 });
|
|
1279
|
+
}
|
|
1280
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeFieldHost, decorators: [{
|
|
1281
|
+
type: Directive,
|
|
1282
|
+
args: [{
|
|
1283
|
+
hostDirectives: [
|
|
1284
|
+
{ directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
|
|
1285
|
+
{ directive: NgForgeField, inputs: [...NG_FORGE_VALUE_FIELD_INPUTS] },
|
|
1286
|
+
],
|
|
1287
|
+
}]
|
|
1288
|
+
}] });
|
|
1289
|
+
/**
|
|
1290
|
+
* Action / button host wrapper. Composes `NgForgeFieldShell` with
|
|
1291
|
+
* `NgForgeAction` (event dispatch + aria-disabled) for button-shaped
|
|
1292
|
+
* components.
|
|
1293
|
+
*
|
|
1294
|
+
* Authoring shape:
|
|
1295
|
+
* ```ts
|
|
1296
|
+
* \@Component({
|
|
1297
|
+
* hostDirectives: [NgForgeActionHost],
|
|
1298
|
+
* ...
|
|
1299
|
+
* })
|
|
1300
|
+
* export class MyButton {
|
|
1301
|
+
* protected readonly action = injectNgForgeAction();
|
|
1302
|
+
* }
|
|
1303
|
+
* ```
|
|
1304
|
+
*/
|
|
1305
|
+
class NgForgeActionHost {
|
|
1306
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeActionHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1307
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", 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 });
|
|
1308
|
+
}
|
|
1309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NgForgeActionHost, decorators: [{
|
|
1310
|
+
type: Directive,
|
|
1311
|
+
args: [{
|
|
1312
|
+
hostDirectives: [
|
|
1313
|
+
{ directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
|
|
1314
|
+
{ directive: NgForgeAction, inputs: [...NG_FORGE_ACTION_INPUTS] },
|
|
1315
|
+
],
|
|
1316
|
+
}]
|
|
1317
|
+
}] });
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Builds a `ComponentFixture` for an `NgForgeField`-composing component with
|
|
1321
|
+
* `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that
|
|
1322
|
+
* Shell's required `key` and Field's required `field` would otherwise throw.
|
|
1323
|
+
*
|
|
1324
|
+
* The caller drives `detectChanges()` themselves (so they can `setInput`
|
|
1325
|
+
* additional component-specific props in between).
|
|
1326
|
+
*/
|
|
1327
|
+
function createNgForgeFieldFixture(component, options) {
|
|
1328
|
+
TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });
|
|
1329
|
+
const key = options.key;
|
|
1330
|
+
const rootValue = signal({ [key]: options.value }, ...(ngDevMode ? [{ debugName: "rootValue" }] : /* istanbul ignore next */ []));
|
|
1331
|
+
const injector = TestBed.inject(EnvironmentInjector);
|
|
1332
|
+
const schemaCallback = options.schema;
|
|
1333
|
+
const root = runInInjectionContext(injector, () => {
|
|
1334
|
+
if (!schemaCallback)
|
|
1335
|
+
return form(rootValue);
|
|
1336
|
+
return form(rootValue, schema((path) => {
|
|
1337
|
+
schemaCallback(path[key]);
|
|
1338
|
+
}));
|
|
1339
|
+
});
|
|
1340
|
+
const field = root[key];
|
|
1341
|
+
if (options.touched)
|
|
1342
|
+
field().markAsTouched();
|
|
1343
|
+
const fixture = TestBed.createComponent(component);
|
|
1344
|
+
fixture.componentRef.setInput('field', field);
|
|
1345
|
+
fixture.componentRef.setInput('key', key);
|
|
1346
|
+
if (options.inputs) {
|
|
1347
|
+
for (const [name, value] of Object.entries(options.inputs)) {
|
|
1348
|
+
fixture.componentRef.setInput(name, value);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return { fixture, field, rootValue };
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Builds a `ComponentFixture` for an `NgForgeAction`-composing component with
|
|
1355
|
+
* `key` + `label` bound and `EventBus` provided. The caller calls
|
|
1356
|
+
* `detectChanges()`; the harness handles the NG0950-avoidance bindings.
|
|
1357
|
+
*/
|
|
1358
|
+
function createNgForgeActionFixture(component, options) {
|
|
1359
|
+
TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });
|
|
1360
|
+
const fixture = TestBed.createComponent(component);
|
|
1361
|
+
fixture.componentRef.setInput('key', options.key);
|
|
1362
|
+
fixture.componentRef.setInput('label', options.label);
|
|
1363
|
+
if (options.event)
|
|
1364
|
+
fixture.componentRef.setInput('event', options.event);
|
|
1365
|
+
if (options.eventArgs)
|
|
1366
|
+
fixture.componentRef.setInput('eventArgs', options.eventArgs);
|
|
1367
|
+
if (options.disabled !== undefined)
|
|
1368
|
+
fixture.componentRef.setInput('disabled', options.disabled);
|
|
1369
|
+
if (options.hidden !== undefined)
|
|
1370
|
+
fixture.componentRef.setInput('hidden', options.hidden);
|
|
1371
|
+
if (options.inputs) {
|
|
1372
|
+
for (const [name, value] of Object.entries(options.inputs)) {
|
|
1373
|
+
fixture.componentRef.setInput(name, value);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return { fixture, eventBus: TestBed.inject(EventBus) };
|
|
1377
|
+
}
|
|
1378
|
+
/** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */
|
|
1379
|
+
function provideTestValidationMessages(messages) {
|
|
1380
|
+
return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };
|
|
1381
|
+
}
|
|
1382
|
+
|
|
808
1383
|
/**
|
|
809
1384
|
* @ng-forge/dynamic-forms/integration
|
|
810
1385
|
*
|
|
@@ -824,5 +1399,5 @@ function setupMetaTracking(elementRef, meta, options) {
|
|
|
824
1399
|
* Generated bundle index. Do not edit.
|
|
825
1400
|
*/
|
|
826
1401
|
|
|
827
|
-
export { addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
1402
|
+
export { NG_FORGE_ACTION_INPUTS, NG_FORGE_FIELD_SHELL_INPUTS, NG_FORGE_VALUE_FIELD_INPUTS, NgForgeAction, NgForgeActionHost, NgForgeControl, NgForgeField, NgForgeFieldHost, NgForgeFieldShell, NgForgeHostControl, addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createAriaDescribedBySignal, createNgForgeActionFixture, createNgForgeFieldFixture, createResolvedErrorsSignal, datepickerFieldMapper, injectNgForgeAction, injectNgForgeField, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, provideTestValidationMessages, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
828
1403
|
//# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map
|