@ng-forge/dynamic-forms 0.10.0-next.1 → 0.10.0-next.3
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-GiaVHO9N.mjs → ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs} +99 -476
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs → ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs} +3 -14
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs +23 -0
- package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs → ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs} +2 -22
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs +17 -0
- package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs → ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs} +15 -63
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +72 -844
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs +10876 -0
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs → ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs} +10 -38
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs → ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs} +2 -18
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs +0 -73
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs.map → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +27 -462
- package/types/ng-forge-dynamic-forms-schema.d.ts +2 -117
- package/types/ng-forge-dynamic-forms.d.ts +286 -4650
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs +0 -48
- package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs +0 -29
- package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs +0 -18773
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +0 -1
|
@@ -9,47 +9,18 @@ import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
|
9
9
|
import { TestBed } from '@angular/core/testing';
|
|
10
10
|
import { form, schema } from '@angular/forms/signals';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
|
|
14
|
-
* Must be called within an injection context.
|
|
15
|
-
*
|
|
16
|
-
* With direct root form binding for array items, the FIELD_SIGNAL_CONTEXT.form
|
|
17
|
-
* uses a getter that evaluates reactively. This means:
|
|
18
|
-
* - For regular fields: direct access to the form's FieldTree
|
|
19
|
-
* - For array items: the getter resolves rootForm['arrayKey'][index] dynamically
|
|
20
|
-
*
|
|
21
|
-
* The returned context provides a getFieldTree function that should be called
|
|
22
|
-
* inside a computed to establish proper reactive dependencies.
|
|
23
|
-
*
|
|
24
|
-
* @returns The value field context with reactive field tree accessor
|
|
25
|
-
*/
|
|
26
12
|
function resolveValueFieldContext() {
|
|
27
13
|
const context = injectFieldSignalContext();
|
|
28
14
|
return {
|
|
29
15
|
getFieldTree: (fieldKey) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const formRoot = context.form;
|
|
33
|
-
// Handle case where form is undefined (e.g., newly added array items before root form updates)
|
|
34
|
-
if (!formRoot) {
|
|
16
|
+
const formRoot = context.form;
|
|
17
|
+
if (!formRoot) {
|
|
35
18
|
return undefined;
|
|
36
19
|
}
|
|
37
20
|
return formRoot[fieldKey];
|
|
38
21
|
},
|
|
39
22
|
};
|
|
40
23
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Builds the base inputs for a value field.
|
|
43
|
-
* This is a helper function used by valueFieldMapper and specialized mappers.
|
|
44
|
-
*
|
|
45
|
-
* Note: This function should be called inside a computed signal to ensure
|
|
46
|
-
* proper reactive dependency tracking for the field tree resolution.
|
|
47
|
-
*
|
|
48
|
-
* @param fieldDef The value field definition
|
|
49
|
-
* @param ctx The resolved value field context
|
|
50
|
-
* @param defaultProps Default props from the form configuration (may be undefined if not configured)
|
|
51
|
-
* @returns Record of input names to values
|
|
52
|
-
*/
|
|
53
24
|
function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
|
|
54
25
|
const omittedFields = omit(fieldDef, ['value']);
|
|
55
26
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps);
|
|
@@ -60,56 +31,20 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
|
|
|
60
31
|
if (fieldDef.placeholder !== undefined) {
|
|
61
32
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
62
33
|
}
|
|
63
|
-
|
|
64
|
-
// so it establishes the reactive dependency correctly
|
|
65
|
-
const fieldTree = ctx.getFieldTree(fieldDef.key);
|
|
34
|
+
const fieldTree = ctx.getFieldTree(fieldDef.key);
|
|
66
35
|
if (fieldTree !== undefined) {
|
|
67
36
|
inputs['field'] = fieldTree;
|
|
68
37
|
}
|
|
69
38
|
return inputs;
|
|
70
39
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Maps a value field definition to component inputs.
|
|
73
|
-
*
|
|
74
|
-
* Value fields are input fields that contribute to the form's value (text, number, etc.).
|
|
75
|
-
* This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
|
|
76
|
-
*
|
|
77
|
-
* For array items, the FIELD_SIGNAL_CONTEXT.form uses a reactive getter that resolves
|
|
78
|
-
* rootForm['arrayKey'][index] dynamically. This means:
|
|
79
|
-
* - Zod/StandardSchema validation errors are automatically available
|
|
80
|
-
* - The field tree updates reactively when the root form structure changes
|
|
81
|
-
* - Newly added array items will get their field tree once the form value updates
|
|
82
|
-
*
|
|
83
|
-
* For fields with specific properties, use the specialized mappers:
|
|
84
|
-
* - optionsFieldMapper: for fields with options (select, radio, multi-checkbox)
|
|
85
|
-
* - datepickerFieldMapper: for fields with minDate, maxDate, startAt
|
|
86
|
-
* - checkboxFieldMapper: for boolean fields (checkbox, toggle)
|
|
87
|
-
*
|
|
88
|
-
* @param fieldDef The value field definition
|
|
89
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
90
|
-
*/
|
|
91
40
|
function valueFieldMapper(fieldDef) {
|
|
92
|
-
|
|
93
|
-
const ctx = resolveValueFieldContext();
|
|
41
|
+
const ctx = resolveValueFieldContext();
|
|
94
42
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
95
|
-
|
|
96
|
-
// 1. Accesses context.form (triggering the getter for array items)
|
|
97
|
-
// 2. Returns the field tree if available, or undefined if not yet
|
|
98
|
-
// 3. Re-runs when the form structure updates (reactive dependency)
|
|
99
|
-
return computed(() => {
|
|
43
|
+
return computed(() => {
|
|
100
44
|
return buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
101
45
|
});
|
|
102
46
|
}
|
|
103
47
|
|
|
104
|
-
/**
|
|
105
|
-
* Maps a checkbox/toggle field definition to component inputs.
|
|
106
|
-
*
|
|
107
|
-
* Checkbox fields are checked fields that contribute to the form's value as boolean.
|
|
108
|
-
* This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field tree.
|
|
109
|
-
*
|
|
110
|
-
* @param fieldDef The checkbox field definition
|
|
111
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
112
|
-
*/
|
|
113
48
|
function checkboxFieldMapper(fieldDef) {
|
|
114
49
|
const context = injectFieldSignalContext();
|
|
115
50
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
@@ -123,9 +58,7 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
123
58
|
if (fieldDef.placeholder !== undefined) {
|
|
124
59
|
inputs['placeholder'] = fieldDef.placeholder;
|
|
125
60
|
}
|
|
126
|
-
|
|
127
|
-
// form may not be immediately available (e.g., during array item initialization)
|
|
128
|
-
const formRoot = context.form;
|
|
61
|
+
const formRoot = context.form;
|
|
129
62
|
const fieldTree = formRoot?.[fieldDef.key];
|
|
130
63
|
if (fieldTree !== undefined) {
|
|
131
64
|
inputs['field'] = fieldTree;
|
|
@@ -134,10 +67,6 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
134
67
|
});
|
|
135
68
|
}
|
|
136
69
|
|
|
137
|
-
/**
|
|
138
|
-
* Converts a date value (string, Date, or null) to a Date object or null.
|
|
139
|
-
* This ensures UI libraries that expect Date objects receive the correct type.
|
|
140
|
-
*/
|
|
141
70
|
function toDate(value) {
|
|
142
71
|
if (value === null || value === undefined) {
|
|
143
72
|
return null;
|
|
@@ -145,31 +74,15 @@ function toDate(value) {
|
|
|
145
74
|
if (value instanceof Date) {
|
|
146
75
|
return value;
|
|
147
76
|
}
|
|
148
|
-
|
|
149
|
-
const date = new Date(value);
|
|
77
|
+
const date = new Date(value);
|
|
150
78
|
return isNaN(date.getTime()) ? null : date;
|
|
151
79
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Maps a datepicker field to component inputs.
|
|
154
|
-
*
|
|
155
|
-
* Extends the base value field mapper by adding datepicker-specific properties:
|
|
156
|
-
* - minDate: Minimum selectable date
|
|
157
|
-
* - maxDate: Maximum selectable date
|
|
158
|
-
* - startAt: Initial date to display when opening the picker
|
|
159
|
-
*
|
|
160
|
-
* Date values are automatically converted from strings to Date objects
|
|
161
|
-
* since UI libraries (Material, PrimeNG, etc.) expect Date objects.
|
|
162
|
-
*
|
|
163
|
-
* @param fieldDef The datepicker field definition
|
|
164
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
165
|
-
*/
|
|
166
80
|
function datepickerFieldMapper(fieldDef) {
|
|
167
81
|
const ctx = resolveValueFieldContext();
|
|
168
82
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
169
83
|
return computed(() => {
|
|
170
84
|
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
171
|
-
|
|
172
|
-
if (fieldDef.minDate !== undefined) {
|
|
85
|
+
if (fieldDef.minDate !== undefined) {
|
|
173
86
|
inputs['minDate'] = toDate(fieldDef.minDate);
|
|
174
87
|
}
|
|
175
88
|
if (fieldDef.maxDate !== undefined) {
|
|
@@ -182,44 +95,22 @@ function datepickerFieldMapper(fieldDef) {
|
|
|
182
95
|
});
|
|
183
96
|
}
|
|
184
97
|
|
|
185
|
-
/**
|
|
186
|
-
* Maps a field with options (select, radio, multi-checkbox) to component inputs.
|
|
187
|
-
*
|
|
188
|
-
* Extends the base value field mapper by adding the options property.
|
|
189
|
-
*
|
|
190
|
-
* @param fieldDef The field definition with options
|
|
191
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
192
|
-
*/
|
|
193
98
|
function optionsFieldMapper(fieldDef) {
|
|
194
99
|
const ctx = resolveValueFieldContext();
|
|
195
100
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
196
101
|
return computed(() => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Add options property
|
|
200
|
-
inputs['options'] = fieldDef.options;
|
|
102
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
103
|
+
inputs['options'] = fieldDef.options;
|
|
201
104
|
return inputs;
|
|
202
105
|
});
|
|
203
106
|
}
|
|
204
107
|
|
|
205
|
-
/**
|
|
206
|
-
* Maps a slider field to component inputs.
|
|
207
|
-
*
|
|
208
|
-
* Extends the base value field mapper by adding slider-specific properties:
|
|
209
|
-
* - minValue: Minimum slider value
|
|
210
|
-
* - maxValue: Maximum slider value
|
|
211
|
-
* - step: Step increment for the slider
|
|
212
|
-
*
|
|
213
|
-
* @param fieldDef The slider field definition
|
|
214
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
215
|
-
*/
|
|
216
108
|
function sliderFieldMapper(fieldDef) {
|
|
217
109
|
const ctx = resolveValueFieldContext();
|
|
218
110
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
219
111
|
return computed(() => {
|
|
220
112
|
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
|
|
221
|
-
|
|
222
|
-
if (fieldDef.minValue !== undefined) {
|
|
113
|
+
if (fieldDef.minValue !== undefined) {
|
|
223
114
|
inputs['minValue'] = fieldDef.minValue;
|
|
224
115
|
}
|
|
225
116
|
if (fieldDef.maxValue !== undefined) {
|
|
@@ -232,10 +123,6 @@ function sliderFieldMapper(fieldDef) {
|
|
|
232
123
|
});
|
|
233
124
|
}
|
|
234
125
|
|
|
235
|
-
/**
|
|
236
|
-
* Applies hidden and disabled logic to a non-form-bound field.
|
|
237
|
-
* Must be called inside a computed() context (reads form signals).
|
|
238
|
-
*/
|
|
239
126
|
function applyNonFieldLogic(rootFormRegistry, fieldDef) {
|
|
240
127
|
const result = {};
|
|
241
128
|
const rootForm = rootFormRegistry.rootForm();
|
|
@@ -248,10 +135,8 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
|
|
|
248
135
|
});
|
|
249
136
|
result.hidden = hiddenSignal();
|
|
250
137
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// Create the signal and READ IT to establish reactive dependencies
|
|
254
|
-
const disabledSignal = resolveNonFieldDisabled({
|
|
138
|
+
if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {
|
|
139
|
+
const disabledSignal = resolveNonFieldDisabled({
|
|
255
140
|
form: rootForm,
|
|
256
141
|
fieldLogic: fieldDef.logic,
|
|
257
142
|
explicitValue: fieldDef.disabled,
|
|
@@ -260,8 +145,7 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
|
|
|
260
145
|
}
|
|
261
146
|
}
|
|
262
147
|
else {
|
|
263
|
-
|
|
264
|
-
if (fieldDef.hidden !== undefined) {
|
|
148
|
+
if (fieldDef.hidden !== undefined) {
|
|
265
149
|
result.hidden = fieldDef.hidden;
|
|
266
150
|
}
|
|
267
151
|
if (fieldDef.disabled !== undefined) {
|
|
@@ -270,19 +154,6 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
|
|
|
270
154
|
}
|
|
271
155
|
return result;
|
|
272
156
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Applies hidden logic to a non-form-bound field.
|
|
275
|
-
*
|
|
276
|
-
* This is a focused utility for fields that only need hidden state resolution.
|
|
277
|
-
* Use this when you need hidden logic but handle disabled separately
|
|
278
|
-
* (e.g., navigation buttons with custom disabled logic).
|
|
279
|
-
*
|
|
280
|
-
* NOTE: This function must be called inside a computed() context.
|
|
281
|
-
*
|
|
282
|
-
* @param rootForm The root form FieldTree (can be undefined)
|
|
283
|
-
* @param fieldDef The field definition with optional hidden/logic
|
|
284
|
-
* @returns The resolved hidden boolean value, or undefined if no hidden logic
|
|
285
|
-
*/
|
|
286
157
|
function resolveHiddenValue(rootForm, fieldDef) {
|
|
287
158
|
if (!rootForm) {
|
|
288
159
|
return fieldDef.hidden;
|
|
@@ -292,71 +163,36 @@ function resolveHiddenValue(rootForm, fieldDef) {
|
|
|
292
163
|
form: rootForm,
|
|
293
164
|
fieldLogic: fieldDef.logic,
|
|
294
165
|
explicitValue: fieldDef.hidden,
|
|
295
|
-
|
|
296
|
-
})();
|
|
166
|
+
})();
|
|
297
167
|
}
|
|
298
168
|
return undefined;
|
|
299
169
|
}
|
|
300
170
|
|
|
301
|
-
/**
|
|
302
|
-
* Maps a button field to component inputs.
|
|
303
|
-
*
|
|
304
|
-
* Unlike value field mappers, button fields do not participate in form values
|
|
305
|
-
* and do not need field tree resolution or validation messages.
|
|
306
|
-
*
|
|
307
|
-
* Button-specific properties:
|
|
308
|
-
* - event: The FormEvent constructor to emit when clicked
|
|
309
|
-
* - eventArgs: Optional arguments to pass to the event constructor
|
|
310
|
-
*
|
|
311
|
-
* Hidden and disabled states are resolved using non-field logic resolvers which consider:
|
|
312
|
-
* 1. Explicit `hidden: true` / `disabled: true` on the field definition
|
|
313
|
-
* 2. Field-level `logic` array with `type: 'hidden'` or `type: 'disabled'` conditions
|
|
314
|
-
*
|
|
315
|
-
* @param fieldDef The button field definition
|
|
316
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
317
|
-
*/
|
|
318
171
|
function buttonFieldMapper(fieldDef) {
|
|
319
172
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
320
173
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
321
174
|
return computed(() => {
|
|
322
175
|
const inputs = buildBaseInputs(fieldDef, defaultProps());
|
|
323
|
-
|
|
324
|
-
inputs['event'] = fieldDef.event;
|
|
176
|
+
inputs['event'] = fieldDef.event;
|
|
325
177
|
if (fieldDef.eventArgs !== undefined) {
|
|
326
178
|
inputs['eventArgs'] = fieldDef.eventArgs;
|
|
327
179
|
}
|
|
328
|
-
|
|
329
|
-
return {
|
|
180
|
+
return {
|
|
330
181
|
...inputs,
|
|
331
182
|
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
332
183
|
};
|
|
333
184
|
});
|
|
334
185
|
}
|
|
335
186
|
|
|
336
|
-
/**
|
|
337
|
-
* Resolves array button context for button mappers.
|
|
338
|
-
*
|
|
339
|
-
* Handles both inside-array (context from ARRAY_CONTEXT) and
|
|
340
|
-
* outside-array (explicit arrayKey) placements.
|
|
341
|
-
*
|
|
342
|
-
* Must be called in injection context.
|
|
343
|
-
*
|
|
344
|
-
* @param fieldKey The button field key (for logging)
|
|
345
|
-
* @param buttonType The button type name (for logging)
|
|
346
|
-
* @param explicitArrayKey Optional explicit arrayKey from field definition
|
|
347
|
-
* @returns The array button context with reactive index signal
|
|
348
|
-
*/
|
|
349
187
|
function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
|
|
350
188
|
const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
351
189
|
const logger = inject(DynamicFormLogger);
|
|
352
|
-
|
|
353
|
-
const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
|
|
190
|
+
const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
|
|
354
191
|
if (!targetArrayKey) {
|
|
355
192
|
logger.warn(`${buttonType} button "${fieldKey}" has no array context. ` +
|
|
356
193
|
'Either place it inside an array field, or provide an explicit arrayKey property.');
|
|
357
194
|
}
|
|
358
|
-
|
|
359
|
-
const indexSignal = computed(() => {
|
|
195
|
+
const indexSignal = computed(() => {
|
|
360
196
|
if (!arrayContext)
|
|
361
197
|
return -1;
|
|
362
198
|
return arrayContext.index();
|
|
@@ -368,21 +204,10 @@ function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
|
|
|
368
204
|
isInsideArray: arrayContext !== null,
|
|
369
205
|
};
|
|
370
206
|
}
|
|
371
|
-
/**
|
|
372
|
-
* Builds the event context passed to the button component.
|
|
373
|
-
*
|
|
374
|
-
* Should be called inside a computed to track index signal changes.
|
|
375
|
-
*
|
|
376
|
-
* @param fieldKey The button field key
|
|
377
|
-
* @param ctx The resolved array button context
|
|
378
|
-
* @param template Optional template for add operations (single field or array of fields)
|
|
379
|
-
* @returns The event context for the button component
|
|
380
|
-
*/
|
|
381
207
|
function buildArrayButtonEventContext(fieldKey, ctx, template) {
|
|
382
208
|
const eventContext = {
|
|
383
209
|
key: fieldKey,
|
|
384
|
-
index: ctx.indexSignal(),
|
|
385
|
-
arrayKey: ctx.targetArrayKey ?? '',
|
|
210
|
+
index: ctx.indexSignal(), arrayKey: ctx.targetArrayKey ?? '',
|
|
386
211
|
formValue: ctx.formValue,
|
|
387
212
|
};
|
|
388
213
|
if (template) {
|
|
@@ -390,24 +215,10 @@ function buildArrayButtonEventContext(fieldKey, ctx, template) {
|
|
|
390
215
|
}
|
|
391
216
|
return eventContext;
|
|
392
217
|
}
|
|
393
|
-
/**
|
|
394
|
-
* Resolves eventArgs with defaults for array buttons.
|
|
395
|
-
*
|
|
396
|
-
* @param fieldEventArgs The eventArgs from field definition (may be undefined)
|
|
397
|
-
* @param defaultEventArgs The default eventArgs to use if not specified
|
|
398
|
-
* @returns The resolved eventArgs array
|
|
399
|
-
*/
|
|
400
218
|
function resolveArrayButtonEventArgs(fieldEventArgs, defaultEventArgs = ['$arrayKey', '$template']) {
|
|
401
219
|
return fieldEventArgs ?? defaultEventArgs;
|
|
402
220
|
}
|
|
403
221
|
|
|
404
|
-
// =============================================================================
|
|
405
|
-
// Add Array Item Mappers
|
|
406
|
-
// =============================================================================
|
|
407
|
-
/**
|
|
408
|
-
* Mapper for add array item button — appends new item to end of array.
|
|
409
|
-
* Template is REQUIRED to define the field structure for new items.
|
|
410
|
-
*/
|
|
411
222
|
function addArrayItemButtonMapper(fieldDef) {
|
|
412
223
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
413
224
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -423,10 +234,6 @@ function addArrayItemButtonMapper(fieldDef) {
|
|
|
423
234
|
};
|
|
424
235
|
});
|
|
425
236
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Mapper for prepend array item button — inserts new item at beginning of array.
|
|
428
|
-
* Template is REQUIRED to define the field structure for new items.
|
|
429
|
-
*/
|
|
430
237
|
function prependArrayItemButtonMapper(fieldDef) {
|
|
431
238
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
432
239
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -442,10 +249,6 @@ function prependArrayItemButtonMapper(fieldDef) {
|
|
|
442
249
|
};
|
|
443
250
|
});
|
|
444
251
|
}
|
|
445
|
-
/**
|
|
446
|
-
* Mapper for insert array item button — inserts new item at specific index.
|
|
447
|
-
* Template and index are REQUIRED.
|
|
448
|
-
*/
|
|
449
252
|
function insertArrayItemButtonMapper(fieldDef) {
|
|
450
253
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
451
254
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -461,21 +264,11 @@ function insertArrayItemButtonMapper(fieldDef) {
|
|
|
461
264
|
};
|
|
462
265
|
});
|
|
463
266
|
}
|
|
464
|
-
// =============================================================================
|
|
465
|
-
// Remove Array Item Mappers
|
|
466
|
-
// =============================================================================
|
|
467
|
-
/**
|
|
468
|
-
* Mapper for remove array item button.
|
|
469
|
-
* Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.
|
|
470
|
-
*/
|
|
471
267
|
function removeArrayItemButtonMapper(fieldDef) {
|
|
472
268
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
473
269
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
474
270
|
const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);
|
|
475
|
-
|
|
476
|
-
// - Inside array: RemoveAtIndexEvent (remove specific item)
|
|
477
|
-
// - Outside array: PopArrayItemEvent (remove last item)
|
|
478
|
-
const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
|
|
271
|
+
const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
|
|
479
272
|
const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];
|
|
480
273
|
return computed(() => {
|
|
481
274
|
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
@@ -488,7 +281,6 @@ function removeArrayItemButtonMapper(fieldDef) {
|
|
|
488
281
|
};
|
|
489
282
|
});
|
|
490
283
|
}
|
|
491
|
-
/** Mapper for pop array item button — removes the last item from array. */
|
|
492
284
|
function popArrayItemButtonMapper(fieldDef) {
|
|
493
285
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
494
286
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -504,7 +296,6 @@ function popArrayItemButtonMapper(fieldDef) {
|
|
|
504
296
|
};
|
|
505
297
|
});
|
|
506
298
|
}
|
|
507
|
-
/** Mapper for shift array item button — removes the first item from array. */
|
|
508
299
|
function shiftArrayItemButtonMapper(fieldDef) {
|
|
509
300
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
510
301
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -521,13 +312,6 @@ function shiftArrayItemButtonMapper(fieldDef) {
|
|
|
521
312
|
});
|
|
522
313
|
}
|
|
523
314
|
|
|
524
|
-
// =============================================================================
|
|
525
|
-
// Submit Button Mapper
|
|
526
|
-
// =============================================================================
|
|
527
|
-
/**
|
|
528
|
-
* Mapper for submit button — configures native form submission via type="submit".
|
|
529
|
-
* Triggers native form submission rather than dispatching events directly.
|
|
530
|
-
*/
|
|
531
315
|
function submitButtonFieldMapper(fieldDef) {
|
|
532
316
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
533
317
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
@@ -536,8 +320,7 @@ function submitButtonFieldMapper(fieldDef) {
|
|
|
536
320
|
return computed(() => {
|
|
537
321
|
const baseInputs = buildBaseInputs(fieldDef, defaultProps());
|
|
538
322
|
const rootForm = rootFormRegistry.rootForm();
|
|
539
|
-
|
|
540
|
-
const disabled = resolveSubmitButtonDisabled({
|
|
323
|
+
const disabled = resolveSubmitButtonDisabled({
|
|
541
324
|
form: rootForm,
|
|
542
325
|
formOptions: formOptions(),
|
|
543
326
|
fieldLogic: fieldWithLogic.logic,
|
|
@@ -545,8 +328,7 @@ function submitButtonFieldMapper(fieldDef) {
|
|
|
545
328
|
})();
|
|
546
329
|
const inputs = {
|
|
547
330
|
...baseInputs,
|
|
548
|
-
|
|
549
|
-
props: { ...fieldDef.props, type: 'submit' },
|
|
331
|
+
props: { ...fieldDef.props, type: 'submit' },
|
|
550
332
|
disabled,
|
|
551
333
|
};
|
|
552
334
|
const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
|
|
@@ -556,10 +338,6 @@ function submitButtonFieldMapper(fieldDef) {
|
|
|
556
338
|
return inputs;
|
|
557
339
|
});
|
|
558
340
|
}
|
|
559
|
-
// =============================================================================
|
|
560
|
-
// Next Page Button Mapper
|
|
561
|
-
// =============================================================================
|
|
562
|
-
/** Mapper for next page button — preconfigures NextPageEvent. */
|
|
563
341
|
function nextButtonFieldMapper(fieldDef) {
|
|
564
342
|
const fieldSignalContext = injectFieldSignalContext();
|
|
565
343
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -587,10 +365,6 @@ function nextButtonFieldMapper(fieldDef) {
|
|
|
587
365
|
return inputs;
|
|
588
366
|
});
|
|
589
367
|
}
|
|
590
|
-
// =============================================================================
|
|
591
|
-
// Previous Page Button Mapper
|
|
592
|
-
// =============================================================================
|
|
593
|
-
/** Mapper for previous page button — preconfigures PreviousPageEvent. */
|
|
594
368
|
function previousButtonFieldMapper(fieldDef) {
|
|
595
369
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
596
370
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
@@ -602,8 +376,7 @@ function previousButtonFieldMapper(fieldDef) {
|
|
|
602
376
|
...baseInputs,
|
|
603
377
|
event: PreviousPageEvent,
|
|
604
378
|
};
|
|
605
|
-
|
|
606
|
-
if (fieldDef.disabled !== undefined) {
|
|
379
|
+
if (fieldDef.disabled !== undefined) {
|
|
607
380
|
inputs['disabled'] = fieldDef.disabled;
|
|
608
381
|
}
|
|
609
382
|
const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
|
|
@@ -614,105 +387,48 @@ function previousButtonFieldMapper(fieldDef) {
|
|
|
614
387
|
});
|
|
615
388
|
}
|
|
616
389
|
|
|
617
|
-
// Button field mapper (generic)
|
|
618
390
|
|
|
619
|
-
// UI-specific field mappers
|
|
620
391
|
|
|
621
|
-
/**
|
|
622
|
-
* Factory function that creates a signal of resolved validation errors
|
|
623
|
-
* Handles async resolution of DynamicText validation messages
|
|
624
|
-
*
|
|
625
|
-
* @param field - Signal containing FieldTree
|
|
626
|
-
* @param validationMessages - Signal containing custom field-level validation messages
|
|
627
|
-
* @param defaultValidationMessages - Signal containing default validation messages (fallback)
|
|
628
|
-
* @param injector - Optional injector for DynamicText resolution
|
|
629
|
-
* @returns Signal<ResolvedError[]> - Reactively resolved error messages
|
|
630
|
-
*
|
|
631
|
-
* @example
|
|
632
|
-
* ```typescript
|
|
633
|
-
* readonly resolvedErrors = createResolvedErrorsSignal(
|
|
634
|
-
* this.field,
|
|
635
|
-
* this.validationMessages,
|
|
636
|
-
* this.defaultValidationMessages
|
|
637
|
-
* );
|
|
638
|
-
* ```
|
|
639
|
-
*/
|
|
640
392
|
function createResolvedErrorsSignal(field, validationMessages, defaultValidationMessages = signal(undefined), injector = inject(Injector)) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
// Ensure validationMessages is never undefined (mappers pass {} if not defined)
|
|
644
|
-
const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
|
|
393
|
+
const logger = injector.get(DynamicFormLogger);
|
|
394
|
+
const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
|
|
645
395
|
const defaultMessages = computed(() => defaultValidationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "defaultMessages" }] : /* istanbul ignore next */ []));
|
|
646
|
-
|
|
647
|
-
// This ensures the signal tracks changes to field().errors(), not just the field reference
|
|
648
|
-
const errors = computed(() => {
|
|
396
|
+
const errors = computed(() => {
|
|
649
397
|
const control = field()();
|
|
650
398
|
return control.errors();
|
|
651
399
|
}, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
652
|
-
|
|
653
|
-
const errors$ = toObservable(errors, { injector });
|
|
400
|
+
const errors$ = toObservable(errors, { injector });
|
|
654
401
|
const messages$ = toObservable(messages, { injector });
|
|
655
402
|
const defaultMessages$ = toObservable(defaultMessages, { injector });
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
// No errors - return empty array
|
|
659
|
-
if (!currentErrors || currentErrors.length === 0) {
|
|
403
|
+
const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap(([currentErrors, msgs, defaultMsgs]) => {
|
|
404
|
+
if (!currentErrors || currentErrors.length === 0) {
|
|
660
405
|
return of([]);
|
|
661
406
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
// Combine all error message observables into single array emission, filtering out nulls
|
|
665
|
-
return errorResolvers.length > 0
|
|
407
|
+
const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector, logger));
|
|
408
|
+
return errorResolvers.length > 0
|
|
666
409
|
? combineLatest(errorResolvers).pipe(map((errors) => errors.filter((e) => e !== null)))
|
|
667
410
|
: of([]);
|
|
668
411
|
}));
|
|
669
|
-
|
|
670
|
-
// toSignal properly handles the injection context and manages subscriptions
|
|
671
|
-
return toSignal(resolvedErrors$, { initialValue: [], injector });
|
|
412
|
+
return toSignal(resolvedErrors$, { initialValue: [], injector });
|
|
672
413
|
}
|
|
673
|
-
/**
|
|
674
|
-
* Resolves a single error message from DynamicText sources with fallback logic
|
|
675
|
-
* Priority: field-level message → default message → error.message → no message (logs warning)
|
|
676
|
-
*
|
|
677
|
-
* The error.message fallback enables schema validation libraries (Zod, Valibot, etc.)
|
|
678
|
-
* to provide their own messages without requiring explicit validationMessages configuration.
|
|
679
|
-
* Users who want translations can still override by configuring validationMessages.
|
|
680
|
-
*
|
|
681
|
-
* @internal
|
|
682
|
-
*/
|
|
683
414
|
function resolveErrorMessage(error, fieldMessages, defaultMessages, injector, logger) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// Determine which message to use: field-level → default → error.message (from schema validators)
|
|
689
|
-
const messageToUse = fieldMessage ?? defaultMessage ?? error.message;
|
|
690
|
-
// If no message found, log warning and return null (will be filtered out)
|
|
691
|
-
if (!messageToUse) {
|
|
415
|
+
const fieldMessage = fieldMessages[error.kind];
|
|
416
|
+
const defaultMessage = defaultMessages[error.kind];
|
|
417
|
+
const messageToUse = fieldMessage ?? defaultMessage ?? error.message;
|
|
418
|
+
if (!messageToUse) {
|
|
692
419
|
logger.warn(`No validation message found for error kind "${error.kind}". ` +
|
|
693
420
|
`Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`);
|
|
694
421
|
return of(null);
|
|
695
422
|
}
|
|
696
|
-
|
|
697
|
-
// error.message is always a plain string, not DynamicText
|
|
698
|
-
const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;
|
|
423
|
+
const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;
|
|
699
424
|
const messageObservable = isSchemaMessage
|
|
700
|
-
? of(messageToUse)
|
|
701
|
-
|
|
702
|
-
// Apply parameter interpolation to support {{param}} syntax
|
|
703
|
-
return messageObservable.pipe(map((msg) => ({
|
|
425
|
+
? of(messageToUse) : dynamicTextToObservable(messageToUse, injector);
|
|
426
|
+
return messageObservable.pipe(map((msg) => ({
|
|
704
427
|
kind: error.kind,
|
|
705
428
|
message: interpolateParams(msg, error),
|
|
706
429
|
})));
|
|
707
430
|
}
|
|
708
431
|
|
|
709
|
-
/**
|
|
710
|
-
* Computed signal that determines if errors should be displayed.
|
|
711
|
-
* Based on field's invalid, touched, and error count.
|
|
712
|
-
*
|
|
713
|
-
* @param field - Signal containing FieldTree
|
|
714
|
-
* @returns Signal<boolean> - True if errors should be displayed
|
|
715
|
-
*/
|
|
716
432
|
function shouldShowErrors(field) {
|
|
717
433
|
return computed(() => {
|
|
718
434
|
const fieldTree = field();
|
|
@@ -721,13 +437,6 @@ function shouldShowErrors(field) {
|
|
|
721
437
|
});
|
|
722
438
|
}
|
|
723
439
|
|
|
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
440
|
function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
732
441
|
const appliedByElement = new WeakMap();
|
|
733
442
|
let cachedSelector;
|
|
@@ -737,8 +446,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
|
737
446
|
const warnedSelectors = isDevMode() ? new Set() : undefined;
|
|
738
447
|
afterRenderEffect({
|
|
739
448
|
write: () => {
|
|
740
|
-
|
|
741
|
-
const currentDeps = options.dependents?.map((d) => d());
|
|
449
|
+
const currentDeps = options.dependents?.map((d) => d());
|
|
742
450
|
const attrs = computeAttrs();
|
|
743
451
|
const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;
|
|
744
452
|
const selectorChanged = selector !== cachedSelector;
|
|
@@ -749,11 +457,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
|
749
457
|
cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];
|
|
750
458
|
}
|
|
751
459
|
previousDeps = currentDeps;
|
|
752
|
-
|
|
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) {
|
|
460
|
+
if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
|
|
757
461
|
if (!warnedSelectors.has(selector)) {
|
|
758
462
|
warnedSelectors.add(selector);
|
|
759
463
|
const tag = elementRef.nativeElement.tagName.toLowerCase();
|
|
@@ -761,9 +465,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
|
761
465
|
`Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`);
|
|
762
466
|
}
|
|
763
467
|
}
|
|
764
|
-
|
|
765
|
-
// effect runs (driven by an unrelated tracked-signal flicker) cheap.
|
|
766
|
-
if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
|
|
468
|
+
if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
|
|
767
469
|
return;
|
|
768
470
|
}
|
|
769
471
|
previousAttrs = attrs;
|
|
@@ -774,58 +476,14 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
|
|
|
774
476
|
},
|
|
775
477
|
});
|
|
776
478
|
}
|
|
777
|
-
/**
|
|
778
|
-
* Sets up automatic meta attribute tracking and application for wrapped components.
|
|
779
|
-
*
|
|
780
|
-
* This utility creates an `afterRenderEffect` that applies meta attributes to DOM elements
|
|
781
|
-
* after Angular's render cycle. It efficiently tracks signal dependencies and only re-applies
|
|
782
|
-
* when the meta signal or any dependent signals change.
|
|
783
|
-
*
|
|
784
|
-
* Use this instead of MutationObserver for better performance and integration with Angular's
|
|
785
|
-
* change detection.
|
|
786
|
-
*
|
|
787
|
-
* @param elementRef - Reference to the host element
|
|
788
|
-
* @param meta - Signal containing meta attributes to apply
|
|
789
|
-
* @param options - Configuration options
|
|
790
|
-
*
|
|
791
|
-
* @example
|
|
792
|
-
* ```typescript
|
|
793
|
-
* // For a radio group with dynamic options
|
|
794
|
-
* constructor() {
|
|
795
|
-
* setupMetaTracking(inject(ElementRef), this.meta, {
|
|
796
|
-
* selector: 'input[type="radio"]',
|
|
797
|
-
* dependents: [this.options]
|
|
798
|
-
* });
|
|
799
|
-
* }
|
|
800
|
-
*
|
|
801
|
-
* // For a simple checkbox
|
|
802
|
-
* constructor() {
|
|
803
|
-
* setupMetaTracking(inject(ElementRef), this.meta, {
|
|
804
|
-
* selector: 'input[type="checkbox"]'
|
|
805
|
-
* });
|
|
806
|
-
* }
|
|
807
|
-
*
|
|
808
|
-
* // For Shadow DOM components (apply to host)
|
|
809
|
-
* constructor() {
|
|
810
|
-
* setupMetaTracking(inject(ElementRef), this.meta);
|
|
811
|
-
* }
|
|
812
|
-
* ```
|
|
813
|
-
*/
|
|
814
479
|
function setupMetaTracking(elementRef, meta, options) {
|
|
815
480
|
applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });
|
|
816
481
|
}
|
|
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
482
|
function setupMarkerAttrTracking(elementRef, parent, selector) {
|
|
823
483
|
applyTrackedAttrsToTargets(elementRef, () => {
|
|
824
484
|
const ariaRequired = parent.ariaRequired();
|
|
825
485
|
const ariaDescribedBy = parent.ariaDescribedBy();
|
|
826
|
-
|
|
827
|
-
// attr (rather than serializing "null") on transitions.
|
|
828
|
-
return {
|
|
486
|
+
return {
|
|
829
487
|
...(parent.meta() ?? {}),
|
|
830
488
|
'aria-invalid': parent.ariaInvalid(),
|
|
831
489
|
...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),
|
|
@@ -834,17 +492,6 @@ function setupMarkerAttrTracking(elementRef, parent, selector) {
|
|
|
834
492
|
}, { selector });
|
|
835
493
|
}
|
|
836
494
|
|
|
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
495
|
function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
|
|
849
496
|
return computed(() => {
|
|
850
497
|
if (errorsToDisplay().length > 0) {
|
|
@@ -857,24 +504,7 @@ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint)
|
|
|
857
504
|
});
|
|
858
505
|
}
|
|
859
506
|
|
|
860
|
-
// Error display utilities for field components
|
|
861
507
|
|
|
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
508
|
class NgForgeFieldShell {
|
|
879
509
|
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
880
510
|
className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
@@ -891,76 +521,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
891
521
|
},
|
|
892
522
|
}]
|
|
893
523
|
}], 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
524
|
const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'];
|
|
896
525
|
|
|
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
526
|
class NgForgeField {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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 */ []));
|
|
527
|
+
shell = inject(NgForgeFieldShell);
|
|
528
|
+
key = this.shell.key;
|
|
529
|
+
className = this.shell.className;
|
|
530
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
944
531
|
label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
945
532
|
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
946
533
|
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
947
534
|
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
948
535
|
meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
949
536
|
validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : /* istanbul ignore next */ []));
|
|
950
|
-
|
|
951
|
-
|
|
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);
|
|
537
|
+
defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
|
|
538
|
+
errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
|
|
958
539
|
showErrors = shouldShowErrors(this.field);
|
|
959
540
|
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 */ []));
|
|
541
|
+
errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
|
|
964
542
|
hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
|
|
965
543
|
ariaInvalid = computed(() => {
|
|
966
544
|
const state = this.field()();
|
|
@@ -970,21 +548,13 @@ class NgForgeField {
|
|
|
970
548
|
return this.field()().required?.() === true ? true : null;
|
|
971
549
|
}, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : /* istanbul ignore next */ []));
|
|
972
550
|
ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => Boolean(this.props()?.hint));
|
|
973
|
-
|
|
974
|
-
|
|
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() {
|
|
551
|
+
_claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
|
|
552
|
+
markClaimed() {
|
|
980
553
|
this._claimed.set(true);
|
|
981
554
|
}
|
|
982
555
|
logger = inject(DynamicFormLogger, { optional: true });
|
|
983
556
|
constructor() {
|
|
984
|
-
|
|
985
|
-
// NgForgeControl / NgForgeHostControl / ambient sub-component is in
|
|
986
|
-
// place to consume it. Fires at most once per directive instance.
|
|
987
|
-
if (isDevMode()) {
|
|
557
|
+
if (isDevMode()) {
|
|
988
558
|
let warned = false;
|
|
989
559
|
afterRenderEffect({
|
|
990
560
|
write: () => {
|
|
@@ -996,8 +566,7 @@ class NgForgeField {
|
|
|
996
566
|
if (untracked(this._claimed))
|
|
997
567
|
return;
|
|
998
568
|
warned = true;
|
|
999
|
-
|
|
1000
|
-
const key = untracked(this.key);
|
|
569
|
+
const key = untracked(this.key);
|
|
1001
570
|
const message = `NgForgeField - meta() provided for field "${key}" but no NgForgeControl / NgForgeHostControl / ` +
|
|
1002
571
|
`ambient sub-component claimed it. Meta will not be applied. ` +
|
|
1003
572
|
`See https://ng-forge.dev/building-an-adapter#meta-forwarding`;
|
|
@@ -1023,70 +592,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1023
592
|
},
|
|
1024
593
|
}]
|
|
1025
594
|
}], 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
595
|
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
596
|
const _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP = true;
|
|
1036
597
|
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
598
|
function injectNgForgeField() {
|
|
1050
599
|
return inject(NgForgeField);
|
|
1051
600
|
}
|
|
1052
601
|
|
|
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
602
|
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);
|
|
603
|
+
shell = inject(NgForgeFieldShell);
|
|
1084
604
|
key = this.shell.key;
|
|
1085
605
|
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 */ []));
|
|
606
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1090
607
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1091
608
|
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1092
609
|
tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
@@ -1094,19 +611,9 @@ class NgForgeAction {
|
|
|
1094
611
|
eventArgs = input(...(ngDevMode ? [undefined, { debugName: "eventArgs" }] : /* istanbul ignore next */ []));
|
|
1095
612
|
eventContext = input(...(ngDevMode ? [undefined, { debugName: "eventContext" }] : /* istanbul ignore next */ []));
|
|
1096
613
|
props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
|
|
1097
|
-
|
|
1098
|
-
// Dispatch
|
|
1099
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
1100
|
-
eventBus = inject(EventBus);
|
|
614
|
+
eventBus = inject(EventBus);
|
|
1101
615
|
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() {
|
|
616
|
+
dispatch() {
|
|
1110
617
|
const event = this.event();
|
|
1111
618
|
if (!event)
|
|
1112
619
|
return;
|
|
@@ -1137,47 +644,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1137
644
|
},
|
|
1138
645
|
}]
|
|
1139
646
|
}], 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
647
|
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
648
|
const _NG_FORGE_ACTION_INPUTS_LOCKSTEP = true;
|
|
1150
649
|
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
650
|
function injectNgForgeAction() {
|
|
1161
651
|
return inject(NgForgeAction);
|
|
1162
652
|
}
|
|
1163
653
|
|
|
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
654
|
class NgForgeAddonsBase {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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 */ []));
|
|
655
|
+
hostInjector = inject(Injector);
|
|
656
|
+
addons = input(undefined, ...(ngDevMode ? [{ debugName: "addons" }] : /* istanbul ignore next */ []));
|
|
657
|
+
_hiddenSignalCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_hiddenSignalCache" }] : /* istanbul ignore next */ []));
|
|
1181
658
|
hiddenSignalCache = this._hiddenSignalCache.asReadonly();
|
|
1182
659
|
visibleAddons = computed(() => {
|
|
1183
660
|
const list = this.addons() ?? [];
|
|
@@ -1187,11 +664,7 @@ class NgForgeAddonsBase {
|
|
|
1187
664
|
hasAddons = computed(() => this.visibleAddons().length > 0, ...(ngDevMode ? [{ debugName: "hasAddons" }] : /* istanbul ignore next */ []));
|
|
1188
665
|
prefixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'), ...(ngDevMode ? [{ debugName: "prefixAddons" }] : /* istanbul ignore next */ []));
|
|
1189
666
|
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(() => {
|
|
667
|
+
addonsBySlot = computed(() => {
|
|
1195
668
|
const map = new Map();
|
|
1196
669
|
for (const a of this.visibleAddons()) {
|
|
1197
670
|
const bucket = map.get(a.slot) ?? [];
|
|
@@ -1200,24 +673,15 @@ class NgForgeAddonsBase {
|
|
|
1200
673
|
}
|
|
1201
674
|
return map;
|
|
1202
675
|
}, ...(ngDevMode ? [{ debugName: "addonsBySlot" }] : /* istanbul ignore next */ []));
|
|
1203
|
-
|
|
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();
|
|
676
|
+
_hiddenByValue = new WeakMap();
|
|
1208
677
|
constructor() {
|
|
1209
678
|
explicitEffect([this.addons], ([list]) => {
|
|
1210
679
|
const prev = this._hiddenSignalCache();
|
|
1211
680
|
const next = new Map();
|
|
1212
681
|
for (const a of list ?? []) {
|
|
1213
|
-
|
|
1214
|
-
// object is reused across emissions.
|
|
1215
|
-
let resolved = prev.get(a);
|
|
682
|
+
let resolved = prev.get(a);
|
|
1216
683
|
if (!resolved) {
|
|
1217
|
-
|
|
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;
|
|
684
|
+
const h = a.hidden;
|
|
1221
685
|
const valueKey = typeof h === 'object' && h !== null ? h : null;
|
|
1222
686
|
resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
|
|
1223
687
|
if (!resolved) {
|
|
@@ -1238,36 +702,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1238
702
|
type: Directive,
|
|
1239
703
|
args: [{}]
|
|
1240
704
|
}], ctorParameters: () => [], propDecorators: { addons: [{ type: i0.Input, args: [{ isSignal: true, alias: "addons", required: false }] }] } });
|
|
1241
|
-
/** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */
|
|
1242
705
|
const NG_FORGE_ADDONS_INPUTS = ['addons'];
|
|
1243
|
-
// Compile-time lockstep — drift between the tuple and the declared inputs fails the build.
|
|
1244
706
|
const _NG_FORGE_ADDONS_INPUTS_LOCKSTEP = true;
|
|
1245
707
|
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
708
|
class NgForgeAddons {
|
|
1272
709
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1273
710
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddons, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonsBase, inputs: ["addons", "addons"] }], ngImport: i0 });
|
|
@@ -1278,49 +715,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1278
715
|
hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],
|
|
1279
716
|
}]
|
|
1280
717
|
}] });
|
|
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
718
|
function injectNgForgeAddons() {
|
|
1287
719
|
return inject(NgForgeAddonsBase);
|
|
1288
720
|
}
|
|
1289
721
|
|
|
1290
722
|
const ADDON_PRESET_HANDLER = new InjectionToken('ADDON_PRESET_HANDLER');
|
|
1291
723
|
|
|
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
724
|
class NgForgeAddonActionBase {
|
|
1304
|
-
|
|
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 });
|
|
725
|
+
actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });
|
|
1309
726
|
presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });
|
|
1310
727
|
logger = inject(DynamicFormLogger);
|
|
1311
728
|
hostInjector = inject(Injector);
|
|
1312
729
|
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
1313
730
|
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();
|
|
731
|
+
_disabledByValue = new WeakMap();
|
|
1324
732
|
_loadingByValue = new WeakMap();
|
|
1325
733
|
_resolvedDisabled = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedDisabled" }] : /* istanbul ignore next */ []));
|
|
1326
734
|
_resolvedLoading = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedLoading" }] : /* istanbul ignore next */ []));
|
|
@@ -1342,13 +750,7 @@ class NgForgeAddonActionBase {
|
|
|
1342
750
|
}
|
|
1343
751
|
return resolved;
|
|
1344
752
|
}
|
|
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() {
|
|
753
|
+
dispatch() {
|
|
1352
754
|
const addon = this.addon();
|
|
1353
755
|
const ctx = this.buildActionContext();
|
|
1354
756
|
const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;
|
|
@@ -1380,39 +782,14 @@ class NgForgeAddonActionBase {
|
|
|
1380
782
|
addon.action(ctx);
|
|
1381
783
|
return;
|
|
1382
784
|
}
|
|
1383
|
-
|
|
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() {
|
|
785
|
+
}
|
|
786
|
+
buildActionContext() {
|
|
1407
787
|
const inputs = this.fieldInputs();
|
|
1408
788
|
const key = inputs?.key ?? '';
|
|
1409
789
|
const type = inputs?.type ?? '';
|
|
1410
790
|
const tree = inputs?.field;
|
|
1411
791
|
if (tree) {
|
|
1412
|
-
|
|
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));
|
|
792
|
+
const setValue = inputs?.setValue ?? ((next) => writeToFieldValue(tree.value, next, this.logger));
|
|
1416
793
|
return {
|
|
1417
794
|
field: { key, type },
|
|
1418
795
|
form: tree,
|
|
@@ -1433,26 +810,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1433
810
|
type: Directive,
|
|
1434
811
|
args: [{}]
|
|
1435
812
|
}], 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
813
|
const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'];
|
|
1438
814
|
const _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP = true;
|
|
1439
815
|
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
816
|
class NgForgeAddonAction {
|
|
1457
817
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1458
818
|
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 });
|
|
@@ -1467,40 +827,6 @@ function injectNgForgeAddonAction() {
|
|
|
1467
827
|
return inject(NgForgeAddonActionBase);
|
|
1468
828
|
}
|
|
1469
829
|
|
|
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
830
|
class NgForgeControl {
|
|
1505
831
|
target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'ngForgeControl' });
|
|
1506
832
|
constructor() {
|
|
@@ -1515,31 +841,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1515
841
|
type: Directive,
|
|
1516
842
|
args: [{ selector: '[ngForgeControl]' }]
|
|
1517
843
|
}], 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
844
|
class NgForgeHostControl {
|
|
1544
845
|
constructor() {
|
|
1545
846
|
const parent = inject(NgForgeField);
|
|
@@ -1554,32 +855,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1554
855
|
args: [{}]
|
|
1555
856
|
}], ctorParameters: () => [] });
|
|
1556
857
|
|
|
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
858
|
class NgForgeFieldHost {
|
|
1584
859
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1585
860
|
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 });
|
|
@@ -1593,22 +868,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1593
868
|
],
|
|
1594
869
|
}]
|
|
1595
870
|
}] });
|
|
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
871
|
class NgForgeActionHost {
|
|
1613
872
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1614
873
|
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 });
|
|
@@ -1623,14 +882,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
1623
882
|
}]
|
|
1624
883
|
}] });
|
|
1625
884
|
|
|
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
885
|
function createNgForgeFieldFixture(component, options) {
|
|
1635
886
|
TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });
|
|
1636
887
|
const key = options.key;
|
|
@@ -1657,11 +908,6 @@ function createNgForgeFieldFixture(component, options) {
|
|
|
1657
908
|
}
|
|
1658
909
|
return { fixture, field, rootValue };
|
|
1659
910
|
}
|
|
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
911
|
function createNgForgeActionFixture(component, options) {
|
|
1666
912
|
TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });
|
|
1667
913
|
const fixture = TestBed.createComponent(component);
|
|
@@ -1682,29 +928,11 @@ function createNgForgeActionFixture(component, options) {
|
|
|
1682
928
|
}
|
|
1683
929
|
return { fixture, eventBus: TestBed.inject(EventBus) };
|
|
1684
930
|
}
|
|
1685
|
-
/** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */
|
|
1686
931
|
function provideTestValidationMessages(messages) {
|
|
1687
932
|
return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };
|
|
1688
933
|
}
|
|
1689
934
|
|
|
1690
|
-
/**
|
|
1691
|
-
* @ng-forge/dynamic-forms/integration
|
|
1692
|
-
*
|
|
1693
|
-
* Integration API for UI library authors building field implementations
|
|
1694
|
-
* (Material, Bootstrap, PrimeNG, Ionic, etc.)
|
|
1695
|
-
*
|
|
1696
|
-
* This entrypoint provides:
|
|
1697
|
-
* - Specific field type definitions (InputField, SelectField, etc.)
|
|
1698
|
-
* - Field mappers for creating FieldTypeDefinition
|
|
1699
|
-
* - Error display utilities for field components
|
|
1700
|
-
*/
|
|
1701
|
-
// =============================================================================
|
|
1702
|
-
// Field Mappers
|
|
1703
|
-
// =============================================================================
|
|
1704
935
|
|
|
1705
|
-
/**
|
|
1706
|
-
* Generated bundle index. Do not edit.
|
|
1707
|
-
*/
|
|
1708
936
|
|
|
1709
937
|
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 };
|
|
1710
938
|
//# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map
|