@ng-forge/dynamic-forms 0.5.0 → 0.5.2
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-integration.mjs +534 -27
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-9JM7tFkl.mjs} +9633 -7027
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-9JM7tFkl.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs → ng-forge-dynamic-forms-page-field.component-Bm1KS0M6.mjs} +9 -20
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-Bm1KS0M6.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs → ng-forge-dynamic-forms-text-field.component-DmJwXtzI.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs.map → ng-forge-dynamic-forms-text-field.component-DmJwXtzI.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 +237 -17
- package/types/ng-forge-dynamic-forms.d.ts +1458 -637
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs.map +0 -1
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
import { inject, computed, signal, Injector, afterRenderEffect } from '@angular/core';
|
|
2
|
-
import { FIELD_SIGNAL_CONTEXT, omit, buildBaseInputs, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DynamicFormLogger, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement } from '@ng-forge/dynamic-forms';
|
|
3
|
-
export { isEqual } from '@ng-forge/dynamic-forms';
|
|
2
|
+
import { FIELD_SIGNAL_CONTEXT, omit, buildBaseInputs, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, resolveNonFieldHidden, resolveNonFieldDisabled, RootFormRegistryService, ARRAY_CONTEXT, DynamicFormLogger, AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, FORM_OPTIONS, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement } from '@ng-forge/dynamic-forms';
|
|
3
|
+
export { isEqual, resolveNonFieldDisabled, resolveNonFieldHidden } from '@ng-forge/dynamic-forms';
|
|
4
4
|
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { combineLatest, of } from 'rxjs';
|
|
6
6
|
import { switchMap, map } from 'rxjs/operators';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
|
|
10
10
|
* Must be called within an injection context.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* With direct root form binding for array items, the FIELD_SIGNAL_CONTEXT.form
|
|
13
|
+
* uses a getter that evaluates reactively. This means:
|
|
14
|
+
* - For regular fields: direct access to the form's FieldTree
|
|
15
|
+
* - For array items: the getter resolves rootForm['arrayKey'][index] dynamically
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* The returned context provides a getFieldTree function that should be called
|
|
18
|
+
* inside a computed to establish proper reactive dependencies.
|
|
19
|
+
*
|
|
20
|
+
* @returns The value field context with reactive field tree accessor
|
|
16
21
|
*/
|
|
17
|
-
function resolveValueFieldContext(
|
|
22
|
+
function resolveValueFieldContext() {
|
|
18
23
|
const context = inject(FIELD_SIGNAL_CONTEXT);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
return {
|
|
25
|
+
getFieldTree: (fieldKey) => {
|
|
26
|
+
// Access context.form here (inside a function that will be called from a computed)
|
|
27
|
+
// This allows the getter to be evaluated reactively
|
|
28
|
+
const formRoot = context.form;
|
|
29
|
+
// Handle case where form is undefined (e.g., newly added array items before root form updates)
|
|
30
|
+
if (!formRoot) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return formRoot[fieldKey];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
24
38
|
* Builds the base inputs for a value field.
|
|
25
39
|
* This is a helper function used by valueFieldMapper and specialized mappers.
|
|
26
40
|
*
|
|
41
|
+
* Note: This function should be called inside a computed signal to ensure
|
|
42
|
+
* proper reactive dependency tracking for the field tree resolution.
|
|
43
|
+
*
|
|
27
44
|
* @param fieldDef The value field definition
|
|
28
45
|
* @param ctx The resolved value field context
|
|
29
46
|
* @param defaultProps Default props from the form configuration (may be undefined if not configured)
|
|
@@ -43,8 +60,11 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
|
|
|
43
60
|
if (defaultValidationMessages !== undefined) {
|
|
44
61
|
inputs['defaultValidationMessages'] = defaultValidationMessages;
|
|
45
62
|
}
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
// Resolve field tree reactively - this call is inside a computed,
|
|
64
|
+
// so it establishes the reactive dependency correctly
|
|
65
|
+
const fieldTree = ctx.getFieldTree(fieldDef.key);
|
|
66
|
+
if (fieldTree !== undefined) {
|
|
67
|
+
inputs['field'] = fieldTree;
|
|
48
68
|
}
|
|
49
69
|
return inputs;
|
|
50
70
|
}
|
|
@@ -54,6 +74,12 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
|
|
|
54
74
|
* Value fields are input fields that contribute to the form's value (text, number, etc.).
|
|
55
75
|
* This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
|
|
56
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
|
+
*
|
|
57
83
|
* For fields with specific properties (select, datepicker, textarea, slider), use the specialized mappers:
|
|
58
84
|
* - selectFieldMapper: for fields with options (select, radio, multi-checkbox)
|
|
59
85
|
* - datepickerFieldMapper: for fields with minDate, maxDate, startAt
|
|
@@ -64,10 +90,17 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
|
|
|
64
90
|
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
65
91
|
*/
|
|
66
92
|
function valueFieldMapper(fieldDef) {
|
|
67
|
-
|
|
93
|
+
// Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
|
|
94
|
+
const ctx = resolveValueFieldContext();
|
|
68
95
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
69
96
|
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
70
|
-
|
|
97
|
+
// The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
|
|
98
|
+
// 1. Accesses context.form (triggering the getter for array items)
|
|
99
|
+
// 2. Returns the field tree if available, or undefined if not yet
|
|
100
|
+
// 3. Re-runs when the form structure updates (reactive dependency)
|
|
101
|
+
return computed(() => {
|
|
102
|
+
return buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
|
|
103
|
+
});
|
|
71
104
|
}
|
|
72
105
|
|
|
73
106
|
/**
|
|
@@ -83,8 +116,6 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
83
116
|
const context = inject(FIELD_SIGNAL_CONTEXT);
|
|
84
117
|
const defaultProps = inject(DEFAULT_PROPS);
|
|
85
118
|
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
86
|
-
const formRoot = context.form;
|
|
87
|
-
const fieldTree = formRoot[fieldDef.key];
|
|
88
119
|
return computed(() => {
|
|
89
120
|
const omittedFields = omit(fieldDef, ['value']);
|
|
90
121
|
const baseInputs = buildBaseInputs(omittedFields, defaultProps());
|
|
@@ -99,6 +130,10 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
99
130
|
if (validationMessages !== undefined) {
|
|
100
131
|
inputs['defaultValidationMessages'] = validationMessages;
|
|
101
132
|
}
|
|
133
|
+
// Access form inside computed for reactivity and to handle cases where
|
|
134
|
+
// form may not be immediately available (e.g., during array item initialization)
|
|
135
|
+
const formRoot = context.form;
|
|
136
|
+
const fieldTree = formRoot?.[fieldDef.key];
|
|
102
137
|
if (fieldTree !== undefined) {
|
|
103
138
|
inputs['field'] = fieldTree;
|
|
104
139
|
}
|
|
@@ -106,6 +141,21 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
106
141
|
});
|
|
107
142
|
}
|
|
108
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Converts a date value (string, Date, or null) to a Date object or null.
|
|
146
|
+
* This ensures UI libraries that expect Date objects receive the correct type.
|
|
147
|
+
*/
|
|
148
|
+
function toDate(value) {
|
|
149
|
+
if (value === null || value === undefined) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (value instanceof Date) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
// Parse string to Date
|
|
156
|
+
const date = new Date(value);
|
|
157
|
+
return isNaN(date.getTime()) ? null : date;
|
|
158
|
+
}
|
|
109
159
|
/**
|
|
110
160
|
* Maps a datepicker field to component inputs.
|
|
111
161
|
*
|
|
@@ -114,22 +164,27 @@ function checkboxFieldMapper(fieldDef) {
|
|
|
114
164
|
* - maxDate: Maximum selectable date
|
|
115
165
|
* - startAt: Initial date to display when opening the picker
|
|
116
166
|
*
|
|
167
|
+
* Date values are automatically converted from strings to Date objects
|
|
168
|
+
* since UI libraries (Material, PrimeNG, etc.) expect Date objects.
|
|
169
|
+
*
|
|
117
170
|
* @param fieldDef The datepicker field definition
|
|
118
171
|
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
119
172
|
*/
|
|
120
173
|
function datepickerFieldMapper(fieldDef) {
|
|
121
|
-
const ctx = resolveValueFieldContext(
|
|
174
|
+
const ctx = resolveValueFieldContext();
|
|
175
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
176
|
+
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
122
177
|
return computed(() => {
|
|
123
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx);
|
|
124
|
-
// Add datepicker-specific properties
|
|
178
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
|
|
179
|
+
// Add datepicker-specific properties, converting strings to Date objects
|
|
125
180
|
if (fieldDef.minDate !== undefined) {
|
|
126
|
-
inputs['minDate'] = fieldDef.minDate;
|
|
181
|
+
inputs['minDate'] = toDate(fieldDef.minDate);
|
|
127
182
|
}
|
|
128
183
|
if (fieldDef.maxDate !== undefined) {
|
|
129
|
-
inputs['maxDate'] = fieldDef.maxDate;
|
|
184
|
+
inputs['maxDate'] = toDate(fieldDef.maxDate);
|
|
130
185
|
}
|
|
131
186
|
if (fieldDef.startAt !== undefined) {
|
|
132
|
-
inputs['startAt'] = fieldDef.startAt;
|
|
187
|
+
inputs['startAt'] = toDate(fieldDef.startAt);
|
|
133
188
|
}
|
|
134
189
|
return inputs;
|
|
135
190
|
});
|
|
@@ -144,16 +199,468 @@ function datepickerFieldMapper(fieldDef) {
|
|
|
144
199
|
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
145
200
|
*/
|
|
146
201
|
function optionsFieldMapper(fieldDef) {
|
|
147
|
-
const ctx = resolveValueFieldContext(
|
|
202
|
+
const ctx = resolveValueFieldContext();
|
|
203
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
204
|
+
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
148
205
|
return computed(() => {
|
|
149
206
|
// Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
|
|
150
|
-
const inputs = buildValueFieldInputs(fieldDef, ctx);
|
|
207
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
|
|
151
208
|
// Add options property
|
|
152
209
|
inputs['options'] = fieldDef.options;
|
|
153
210
|
return inputs;
|
|
154
211
|
});
|
|
155
212
|
}
|
|
156
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Maps a textarea field to component inputs.
|
|
216
|
+
*
|
|
217
|
+
* Extends the base value field mapper by adding textarea-specific properties:
|
|
218
|
+
* - rows: Number of visible text rows
|
|
219
|
+
* - cols: Number of visible text columns
|
|
220
|
+
* - maxLength: Maximum character length (also used for validation)
|
|
221
|
+
*
|
|
222
|
+
* @param fieldDef The textarea field definition
|
|
223
|
+
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
224
|
+
*/
|
|
225
|
+
function textareaFieldMapper(fieldDef) {
|
|
226
|
+
const ctx = resolveValueFieldContext();
|
|
227
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
228
|
+
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
229
|
+
return computed(() => {
|
|
230
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
|
|
231
|
+
// Add textarea-specific properties from props
|
|
232
|
+
const props = fieldDef.props;
|
|
233
|
+
if (props && typeof props === 'object') {
|
|
234
|
+
if (props['rows'] !== undefined) {
|
|
235
|
+
inputs['rows'] = props['rows'];
|
|
236
|
+
}
|
|
237
|
+
if (props['cols'] !== undefined) {
|
|
238
|
+
inputs['cols'] = props['cols'];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// maxLength is at field level for validation
|
|
242
|
+
if (fieldDef.maxLength !== undefined) {
|
|
243
|
+
inputs['maxLength'] = fieldDef.maxLength;
|
|
244
|
+
}
|
|
245
|
+
return inputs;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Maps a slider field to component inputs.
|
|
251
|
+
*
|
|
252
|
+
* Extends the base value field mapper by adding slider-specific properties:
|
|
253
|
+
* - minValue: Minimum slider value
|
|
254
|
+
* - maxValue: Maximum slider value
|
|
255
|
+
* - step: Step increment for the slider
|
|
256
|
+
*
|
|
257
|
+
* @param fieldDef The slider field definition
|
|
258
|
+
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
259
|
+
*/
|
|
260
|
+
function sliderFieldMapper(fieldDef) {
|
|
261
|
+
const ctx = resolveValueFieldContext();
|
|
262
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
263
|
+
const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
264
|
+
return computed(() => {
|
|
265
|
+
const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
|
|
266
|
+
// Add slider-specific properties (these are at field level, not in props)
|
|
267
|
+
if (fieldDef.minValue !== undefined) {
|
|
268
|
+
inputs['minValue'] = fieldDef.minValue;
|
|
269
|
+
}
|
|
270
|
+
if (fieldDef.maxValue !== undefined) {
|
|
271
|
+
inputs['maxValue'] = fieldDef.maxValue;
|
|
272
|
+
}
|
|
273
|
+
if (fieldDef.step !== undefined) {
|
|
274
|
+
inputs['step'] = fieldDef.step;
|
|
275
|
+
}
|
|
276
|
+
return inputs;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Applies hidden and disabled logic to a non-form-bound field.
|
|
282
|
+
* Must be called inside a computed() context (reads form signals).
|
|
283
|
+
*/
|
|
284
|
+
function applyNonFieldLogic(rootFormRegistry, fieldDef) {
|
|
285
|
+
const result = {};
|
|
286
|
+
const rootForm = rootFormRegistry.rootForm();
|
|
287
|
+
if (rootForm) {
|
|
288
|
+
if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {
|
|
289
|
+
const hiddenSignal = resolveNonFieldHidden({
|
|
290
|
+
form: rootForm,
|
|
291
|
+
fieldLogic: fieldDef.logic,
|
|
292
|
+
explicitValue: fieldDef.hidden,
|
|
293
|
+
});
|
|
294
|
+
result.hidden = hiddenSignal();
|
|
295
|
+
}
|
|
296
|
+
// Resolve disabled state if explicitly set or has logic
|
|
297
|
+
if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {
|
|
298
|
+
// Create the signal and READ IT to establish reactive dependencies
|
|
299
|
+
const disabledSignal = resolveNonFieldDisabled({
|
|
300
|
+
form: rootForm,
|
|
301
|
+
fieldLogic: fieldDef.logic,
|
|
302
|
+
explicitValue: fieldDef.disabled,
|
|
303
|
+
});
|
|
304
|
+
result.disabled = disabledSignal();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Fallback to static values when rootForm is not available
|
|
309
|
+
if (fieldDef.hidden !== undefined) {
|
|
310
|
+
result.hidden = fieldDef.hidden;
|
|
311
|
+
}
|
|
312
|
+
if (fieldDef.disabled !== undefined) {
|
|
313
|
+
result.disabled = fieldDef.disabled;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Applies hidden logic to a non-form-bound field.
|
|
320
|
+
*
|
|
321
|
+
* This is a focused utility for fields that only need hidden state resolution.
|
|
322
|
+
* Use this when you need hidden logic but handle disabled separately
|
|
323
|
+
* (e.g., navigation buttons with custom disabled logic).
|
|
324
|
+
*
|
|
325
|
+
* NOTE: This function must be called inside a computed() context.
|
|
326
|
+
*
|
|
327
|
+
* @param rootForm The root form FieldTree (can be undefined)
|
|
328
|
+
* @param fieldDef The field definition with optional hidden/logic
|
|
329
|
+
* @returns The resolved hidden boolean value, or undefined if no hidden logic
|
|
330
|
+
*/
|
|
331
|
+
function resolveHiddenValue(rootForm, fieldDef) {
|
|
332
|
+
if (!rootForm) {
|
|
333
|
+
return fieldDef.hidden;
|
|
334
|
+
}
|
|
335
|
+
if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {
|
|
336
|
+
return resolveNonFieldHidden({
|
|
337
|
+
form: rootForm,
|
|
338
|
+
fieldLogic: fieldDef.logic,
|
|
339
|
+
explicitValue: fieldDef.hidden,
|
|
340
|
+
// formValue omitted intentionally - resolver will read form.value() reactively
|
|
341
|
+
})();
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Maps a button field to component inputs.
|
|
348
|
+
*
|
|
349
|
+
* Unlike value field mappers, button fields do not participate in form values
|
|
350
|
+
* and do not need field tree resolution or validation messages.
|
|
351
|
+
*
|
|
352
|
+
* Button-specific properties:
|
|
353
|
+
* - event: The FormEvent constructor to emit when clicked
|
|
354
|
+
* - eventArgs: Optional arguments to pass to the event constructor
|
|
355
|
+
*
|
|
356
|
+
* Hidden and disabled states are resolved using non-field logic resolvers which consider:
|
|
357
|
+
* 1. Explicit `hidden: true` / `disabled: true` on the field definition
|
|
358
|
+
* 2. Field-level `logic` array with `type: 'hidden'` or `type: 'disabled'` conditions
|
|
359
|
+
*
|
|
360
|
+
* @param fieldDef The button field definition
|
|
361
|
+
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
362
|
+
*/
|
|
363
|
+
function buttonFieldMapper(fieldDef) {
|
|
364
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
365
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
366
|
+
return computed(() => {
|
|
367
|
+
const inputs = buildBaseInputs(fieldDef, defaultProps());
|
|
368
|
+
// Add button-specific properties
|
|
369
|
+
inputs['event'] = fieldDef.event;
|
|
370
|
+
if (fieldDef.eventArgs !== undefined) {
|
|
371
|
+
inputs['eventArgs'] = fieldDef.eventArgs;
|
|
372
|
+
}
|
|
373
|
+
// Apply hidden/disabled logic
|
|
374
|
+
return {
|
|
375
|
+
...inputs,
|
|
376
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolves array button context for button mappers.
|
|
383
|
+
*
|
|
384
|
+
* Handles both inside-array (context from ARRAY_CONTEXT) and
|
|
385
|
+
* outside-array (explicit arrayKey) placements.
|
|
386
|
+
*
|
|
387
|
+
* Must be called in injection context.
|
|
388
|
+
*
|
|
389
|
+
* @param fieldKey The button field key (for logging)
|
|
390
|
+
* @param buttonType The button type name (for logging)
|
|
391
|
+
* @param explicitArrayKey Optional explicit arrayKey from field definition
|
|
392
|
+
* @returns The array button context with reactive index signal
|
|
393
|
+
*/
|
|
394
|
+
function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
|
|
395
|
+
const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
396
|
+
const logger = inject(DynamicFormLogger);
|
|
397
|
+
// Priority: explicit arrayKey from fieldDef > arrayKey from context
|
|
398
|
+
const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
|
|
399
|
+
if (!targetArrayKey) {
|
|
400
|
+
logger.warn(`${buttonType} button "${fieldKey}" has no array context. ` +
|
|
401
|
+
'Either place it inside an array field, or provide an explicit arrayKey property.');
|
|
402
|
+
}
|
|
403
|
+
// Create index signal once at injection time - computed to track arrayContext.index changes
|
|
404
|
+
const indexSignal = computed(() => {
|
|
405
|
+
if (!arrayContext)
|
|
406
|
+
return -1;
|
|
407
|
+
return arrayContext.index();
|
|
408
|
+
}, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
|
|
409
|
+
return {
|
|
410
|
+
targetArrayKey,
|
|
411
|
+
indexSignal,
|
|
412
|
+
formValue: (arrayContext?.formValue ?? {}),
|
|
413
|
+
isInsideArray: arrayContext !== null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Builds the event context passed to the button component.
|
|
418
|
+
*
|
|
419
|
+
* Should be called inside a computed to track index signal changes.
|
|
420
|
+
*
|
|
421
|
+
* @param fieldKey The button field key
|
|
422
|
+
* @param ctx The resolved array button context
|
|
423
|
+
* @param template Optional template for add operations (single field or array of fields)
|
|
424
|
+
* @returns The event context for the button component
|
|
425
|
+
*/
|
|
426
|
+
function buildArrayButtonEventContext(fieldKey, ctx, template) {
|
|
427
|
+
const eventContext = {
|
|
428
|
+
key: fieldKey,
|
|
429
|
+
index: ctx.indexSignal(), // Evaluated inside computed
|
|
430
|
+
arrayKey: ctx.targetArrayKey ?? '',
|
|
431
|
+
formValue: ctx.formValue,
|
|
432
|
+
};
|
|
433
|
+
if (template) {
|
|
434
|
+
eventContext.template = template;
|
|
435
|
+
}
|
|
436
|
+
return eventContext;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolves eventArgs with defaults for array buttons.
|
|
440
|
+
*
|
|
441
|
+
* @param fieldEventArgs The eventArgs from field definition (may be undefined)
|
|
442
|
+
* @param defaultEventArgs The default eventArgs to use if not specified
|
|
443
|
+
* @returns The resolved eventArgs array
|
|
444
|
+
*/
|
|
445
|
+
function resolveArrayButtonEventArgs(fieldEventArgs, defaultEventArgs = ['$arrayKey', '$template']) {
|
|
446
|
+
return fieldEventArgs ?? defaultEventArgs;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// =============================================================================
|
|
450
|
+
// Add Array Item Mappers
|
|
451
|
+
// =============================================================================
|
|
452
|
+
/**
|
|
453
|
+
* Mapper for add array item button — appends new item to end of array.
|
|
454
|
+
* Template is REQUIRED to define the field structure for new items.
|
|
455
|
+
*/
|
|
456
|
+
function addArrayItemButtonMapper(fieldDef) {
|
|
457
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
458
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
459
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'addArrayItem', fieldDef.arrayKey);
|
|
460
|
+
return computed(() => {
|
|
461
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
462
|
+
return {
|
|
463
|
+
...baseInputs,
|
|
464
|
+
event: AppendArrayItemEvent,
|
|
465
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),
|
|
466
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
|
|
467
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Mapper for prepend array item button — inserts new item at beginning of array.
|
|
473
|
+
* Template is REQUIRED to define the field structure for new items.
|
|
474
|
+
*/
|
|
475
|
+
function prependArrayItemButtonMapper(fieldDef) {
|
|
476
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
477
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
478
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'prependArrayItem', fieldDef.arrayKey);
|
|
479
|
+
return computed(() => {
|
|
480
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
481
|
+
return {
|
|
482
|
+
...baseInputs,
|
|
483
|
+
event: PrependArrayItemEvent,
|
|
484
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),
|
|
485
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
|
|
486
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Mapper for insert array item button — inserts new item at specific index.
|
|
492
|
+
* Template and index are REQUIRED.
|
|
493
|
+
*/
|
|
494
|
+
function insertArrayItemButtonMapper(fieldDef) {
|
|
495
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
496
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
497
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'insertArrayItem', fieldDef.arrayKey);
|
|
498
|
+
return computed(() => {
|
|
499
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
500
|
+
return {
|
|
501
|
+
...baseInputs,
|
|
502
|
+
event: InsertArrayItemEvent,
|
|
503
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', fieldDef.index, '$template']),
|
|
504
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
|
|
505
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// =============================================================================
|
|
510
|
+
// Remove Array Item Mappers
|
|
511
|
+
// =============================================================================
|
|
512
|
+
/**
|
|
513
|
+
* Mapper for remove array item button.
|
|
514
|
+
* Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.
|
|
515
|
+
*/
|
|
516
|
+
function removeArrayItemButtonMapper(fieldDef) {
|
|
517
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
518
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
519
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);
|
|
520
|
+
// Choose event type based on context:
|
|
521
|
+
// - Inside array: RemoveAtIndexEvent (remove specific item)
|
|
522
|
+
// - Outside array: PopArrayItemEvent (remove last item)
|
|
523
|
+
const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
|
|
524
|
+
const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];
|
|
525
|
+
return computed(() => {
|
|
526
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
527
|
+
return {
|
|
528
|
+
...baseInputs,
|
|
529
|
+
event: eventType,
|
|
530
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, defaultEventArgs),
|
|
531
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
|
|
532
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/** Mapper for pop array item button — removes the last item from array. */
|
|
537
|
+
function popArrayItemButtonMapper(fieldDef) {
|
|
538
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
539
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
540
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'popArrayItem', fieldDef.arrayKey);
|
|
541
|
+
return computed(() => {
|
|
542
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
543
|
+
return {
|
|
544
|
+
...baseInputs,
|
|
545
|
+
event: PopArrayItemEvent,
|
|
546
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),
|
|
547
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
|
|
548
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/** Mapper for shift array item button — removes the first item from array. */
|
|
553
|
+
function shiftArrayItemButtonMapper(fieldDef) {
|
|
554
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
555
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
556
|
+
const ctx = resolveArrayButtonContext(fieldDef.key, 'shiftArrayItem', fieldDef.arrayKey);
|
|
557
|
+
return computed(() => {
|
|
558
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
|
|
559
|
+
return {
|
|
560
|
+
...baseInputs,
|
|
561
|
+
event: ShiftArrayItemEvent,
|
|
562
|
+
eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),
|
|
563
|
+
eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
|
|
564
|
+
...applyNonFieldLogic(rootFormRegistry, fieldDef),
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// =============================================================================
|
|
570
|
+
// Submit Button Mapper
|
|
571
|
+
// =============================================================================
|
|
572
|
+
/**
|
|
573
|
+
* Mapper for submit button — configures native form submission via type="submit".
|
|
574
|
+
* Triggers native form submission rather than dispatching events directly.
|
|
575
|
+
*/
|
|
576
|
+
function submitButtonFieldMapper(fieldDef) {
|
|
577
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
578
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
579
|
+
const formOptions = inject(FORM_OPTIONS);
|
|
580
|
+
const fieldWithLogic = fieldDef;
|
|
581
|
+
return computed(() => {
|
|
582
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps());
|
|
583
|
+
const rootForm = rootFormRegistry.rootForm();
|
|
584
|
+
// Use rootForm (not fieldSignalContext.form) — submit needs root form validity (#157)
|
|
585
|
+
const disabled = resolveSubmitButtonDisabled({
|
|
586
|
+
form: rootForm,
|
|
587
|
+
formOptions: formOptions(),
|
|
588
|
+
fieldLogic: fieldWithLogic.logic,
|
|
589
|
+
explicitlyDisabled: fieldDef.disabled,
|
|
590
|
+
})();
|
|
591
|
+
const inputs = {
|
|
592
|
+
...baseInputs,
|
|
593
|
+
// type="submit" triggers native form submission (no event dispatch needed)
|
|
594
|
+
props: { ...fieldDef.props, type: 'submit' },
|
|
595
|
+
disabled,
|
|
596
|
+
};
|
|
597
|
+
const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
|
|
598
|
+
if (hidden !== undefined) {
|
|
599
|
+
inputs['hidden'] = hidden;
|
|
600
|
+
}
|
|
601
|
+
return inputs;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
// =============================================================================
|
|
605
|
+
// Next Page Button Mapper
|
|
606
|
+
// =============================================================================
|
|
607
|
+
/** Mapper for next page button — preconfigures NextPageEvent. */
|
|
608
|
+
function nextButtonFieldMapper(fieldDef) {
|
|
609
|
+
const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
|
|
610
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
611
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
612
|
+
const formOptions = inject(FORM_OPTIONS);
|
|
613
|
+
const fieldWithLogic = fieldDef;
|
|
614
|
+
return computed(() => {
|
|
615
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps());
|
|
616
|
+
const rootForm = rootFormRegistry.rootForm();
|
|
617
|
+
const disabled = resolveNextButtonDisabled({
|
|
618
|
+
form: fieldSignalContext.form,
|
|
619
|
+
formOptions: formOptions(),
|
|
620
|
+
fieldLogic: fieldWithLogic.logic,
|
|
621
|
+
currentPageValid: fieldSignalContext.currentPageValid,
|
|
622
|
+
})();
|
|
623
|
+
const inputs = {
|
|
624
|
+
...baseInputs,
|
|
625
|
+
event: NextPageEvent,
|
|
626
|
+
disabled,
|
|
627
|
+
};
|
|
628
|
+
const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
|
|
629
|
+
if (hidden !== undefined) {
|
|
630
|
+
inputs['hidden'] = hidden;
|
|
631
|
+
}
|
|
632
|
+
return inputs;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
// =============================================================================
|
|
636
|
+
// Previous Page Button Mapper
|
|
637
|
+
// =============================================================================
|
|
638
|
+
/** Mapper for previous page button — preconfigures PreviousPageEvent. */
|
|
639
|
+
function previousButtonFieldMapper(fieldDef) {
|
|
640
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
641
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
642
|
+
const fieldWithLogic = fieldDef;
|
|
643
|
+
return computed(() => {
|
|
644
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps());
|
|
645
|
+
const rootForm = rootFormRegistry.rootForm();
|
|
646
|
+
const inputs = {
|
|
647
|
+
...baseInputs,
|
|
648
|
+
event: PreviousPageEvent,
|
|
649
|
+
};
|
|
650
|
+
// Add disabled binding only if explicitly set by user
|
|
651
|
+
if (fieldDef.disabled !== undefined) {
|
|
652
|
+
inputs['disabled'] = fieldDef.disabled;
|
|
653
|
+
}
|
|
654
|
+
const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
|
|
655
|
+
if (hidden !== undefined) {
|
|
656
|
+
inputs['hidden'] = hidden;
|
|
657
|
+
}
|
|
658
|
+
return inputs;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Button field mapper (generic)
|
|
663
|
+
|
|
157
664
|
// UI-specific field mappers
|
|
158
665
|
|
|
159
666
|
/**
|
|
@@ -245,8 +752,8 @@ function resolveErrorMessage(error, fieldMessages, defaultMessages, injector, lo
|
|
|
245
752
|
}
|
|
246
753
|
|
|
247
754
|
/**
|
|
248
|
-
* Computed signal that determines if errors should be displayed
|
|
249
|
-
* Based on field's invalid, touched, and error count
|
|
755
|
+
* Computed signal that determines if errors should be displayed.
|
|
756
|
+
* Based on field's invalid, touched, and error count.
|
|
250
757
|
*
|
|
251
758
|
* @param field - Signal containing FieldTree
|
|
252
759
|
* @returns Signal<boolean> - True if errors should be displayed
|
|
@@ -353,5 +860,5 @@ function setupMetaTracking(elementRef, meta, options) {
|
|
|
353
860
|
* Generated bundle index. Do not edit.
|
|
354
861
|
*/
|
|
355
862
|
|
|
356
|
-
export { buildValueFieldInputs, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, optionsFieldMapper, resolveValueFieldContext, setupMetaTracking, shouldShowErrors, valueFieldMapper };
|
|
863
|
+
export { addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
357
864
|
//# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map
|