@ng-forge/dynamic-forms 0.9.0-next.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-us7mQtVQ.mjs → ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs} +171 -73
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-BCIQ6_Mm.mjs → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs} +12 -8
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-CzZlFNA0.mjs → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-CzZlFNA0.mjs.map → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-Dhlf6UxN.mjs → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs} +5 -5
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DqQ8BA4W.mjs → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DqQ8BA4W.mjs.map → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DKcoco0G.mjs → ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs} +36 -12
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +936 -54
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-CfbmSRM_.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs} +2230 -445
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs +114 -0
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs +34 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-8C1-oyU_.mjs → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-8C1-oyU_.mjs.map → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +2 -2
- package/types/ng-forge-dynamic-forms-integration.d.ts +587 -11
- package/types/ng-forge-dynamic-forms.d.ts +3841 -2428
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-us7mQtVQ.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-BCIQ6_Mm.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Dhlf6UxN.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DKcoco0G.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CfbmSRM_.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DqWAcwhG.mjs +0 -110
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DqWAcwhG.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +0 -34
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, reflectComponentType, Injector,
|
|
2
|
+
import { InjectionToken, inject, reflectComponentType, Injector, EnvironmentInjector, DestroyRef, computed, signal, input, ViewContainerRef, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, TemplateRef, model, contentChildren, makeEnvironmentProviders, Pipe } from '@angular/core';
|
|
3
3
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
4
|
-
import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
|
|
5
4
|
import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
|
|
6
6
|
import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateStandardSchema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
|
|
7
7
|
import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
|
|
8
8
|
import { HttpClient } from '@angular/common/http';
|
|
9
9
|
import { isStandardSchemaMarker } from '@ng-forge/dynamic-forms/schema';
|
|
10
10
|
import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
|
|
11
|
+
import { NgComponentOutlet, AsyncPipe, NgTemplateOutlet } from '@angular/common';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Type guard for WrapperTypeDefinition.
|
|
@@ -97,28 +98,39 @@ const DEFAULT_WRAPPERS = new InjectionToken('DEFAULT_WRAPPERS');
|
|
|
97
98
|
*
|
|
98
99
|
* @example
|
|
99
100
|
* ```typescript
|
|
100
|
-
* // In a mapper function
|
|
101
|
+
* // In a mapper function — prefer the helper for required access (descriptive error).
|
|
101
102
|
* export function mapValueField(fieldDef: BaseValueField<any, any>): Binding[] {
|
|
102
|
-
* const context =
|
|
103
|
+
* const context = injectFieldSignalContext();
|
|
103
104
|
* const form = context.form();
|
|
104
105
|
* // ... use context
|
|
105
106
|
* }
|
|
106
107
|
*
|
|
107
|
-
* //
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* // Optional access — token directly, returns null if not provided.
|
|
109
|
+
* const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
|
|
110
|
+
* if (ctx) {
|
|
111
|
+
* // ...
|
|
110
112
|
* }
|
|
111
113
|
* ```
|
|
112
114
|
*/
|
|
113
|
-
const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT');
|
|
116
|
+
/**
|
|
117
|
+
* Throwing accessor for {@link FIELD_SIGNAL_CONTEXT}.
|
|
118
|
+
*
|
|
119
|
+
* Use when the consumer requires the context — group/array containers and
|
|
120
|
+
* mappers that depend on form structure. Throws a descriptive
|
|
121
|
+
* `DynamicFormError` instead of Angular's generic `NullInjectorError` when
|
|
122
|
+
* the token isn't provided.
|
|
123
|
+
*
|
|
124
|
+
* Consumers that may legitimately run outside a `DynamicForm` should keep
|
|
125
|
+
* using `inject(FIELD_SIGNAL_CONTEXT, { optional: true })`.
|
|
126
|
+
*/
|
|
127
|
+
function injectFieldSignalContext() {
|
|
128
|
+
const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
|
|
129
|
+
if (!ctx) {
|
|
130
|
+
throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');
|
|
131
|
+
}
|
|
132
|
+
return ctx;
|
|
133
|
+
}
|
|
122
134
|
/**
|
|
123
135
|
* Injection token for providing array context metadata to mappers and components.
|
|
124
136
|
*
|
|
@@ -139,6 +151,32 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
|
|
|
139
151
|
* ```
|
|
140
152
|
*/
|
|
141
153
|
const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
|
|
154
|
+
/**
|
|
155
|
+
* Injection token for providing group ancestry to mappers and components.
|
|
156
|
+
*
|
|
157
|
+
* Re-provided in two places:
|
|
158
|
+
* 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
|
|
159
|
+
* parent path extended by its own key (e.g. `'address'` or `'user.address'`).
|
|
160
|
+
* 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
|
|
161
|
+
* resetting the chain at array boundaries — descendants of an array item
|
|
162
|
+
* that re-enter a group compose paths INSIDE the array item, matching the
|
|
163
|
+
* property-derivation collector's keying.
|
|
164
|
+
*
|
|
165
|
+
* `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
|
|
166
|
+
* the same leaf key can appear in different groups without colliding (#401).
|
|
167
|
+
*
|
|
168
|
+
* Inject with `{ optional: true }` because top-level fields with no group
|
|
169
|
+
* ancestor (and no enclosing array) won't have the token provided. The token
|
|
170
|
+
* IS present for fields inside an array even when no inner group exists — the
|
|
171
|
+
* array-boundary sentinel makes injection succeed with an empty `groupPath`.
|
|
172
|
+
*/
|
|
173
|
+
// No factory: GROUP_CONTEXT is optional-by-design — `GroupFieldComponent` and
|
|
174
|
+
// `createArrayItemInjector` re-provide it where it makes sense, and consumers
|
|
175
|
+
// inject with `{ optional: true }` to handle the top-level case. Mirrors the
|
|
176
|
+
// ARRAY_CONTEXT pattern. Adding a throwing factory here fires even when
|
|
177
|
+
// injected optionally (Angular still evaluates the tree-shakable factory
|
|
178
|
+
// before honoring `optional`), so non-group fields would crash on every render.
|
|
179
|
+
const GROUP_CONTEXT = new InjectionToken('GROUP_CONTEXT');
|
|
142
180
|
/**
|
|
143
181
|
* Injection token for form-level default props.
|
|
144
182
|
*
|
|
@@ -226,65 +264,57 @@ const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
|
|
|
226
264
|
* and inherited by all children via Angular's hierarchical injector.
|
|
227
265
|
*/
|
|
228
266
|
const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
|
|
267
|
+
|
|
229
268
|
/**
|
|
230
|
-
* Injection token for
|
|
231
|
-
*
|
|
232
|
-
* This registry tracks which template was used to create each array item,
|
|
233
|
-
* keyed by the item's unique ID. This is essential for "recreate" operations
|
|
234
|
-
* (e.g., after removing items from the middle) where we need to re-resolve
|
|
235
|
-
* items using their original templates, not a fallback.
|
|
236
|
-
*
|
|
237
|
-
* The token is provided by ArrayFieldComponent at its level and shared
|
|
238
|
-
* across all items in that array. Each nested array gets its own registry.
|
|
239
|
-
*
|
|
240
|
-
* @example
|
|
241
|
-
* ```typescript
|
|
242
|
-
* // In ArrayFieldComponent
|
|
243
|
-
* providers: [
|
|
244
|
-
* { provide: ARRAY_TEMPLATE_REGISTRY, useValue: new Map() }
|
|
245
|
-
* ]
|
|
246
|
-
*
|
|
247
|
-
* // When adding items
|
|
248
|
-
* const registry = inject(ARRAY_TEMPLATE_REGISTRY);
|
|
249
|
-
* registry.set(itemId, templates);
|
|
269
|
+
* Injection token for the dynamic forms logger.
|
|
250
270
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* ```
|
|
271
|
+
* Provided by provideDynamicForm() with ConsoleLogger as default.
|
|
272
|
+
* Override with withLoggerConfig(false) to disable logging.
|
|
254
273
|
*/
|
|
255
|
-
const
|
|
274
|
+
const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
|
|
275
|
+
|
|
256
276
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* Provides a function that generates unique IDs for array items. Each array
|
|
260
|
-
* component instance gets its own generator (via useFactory), ensuring:
|
|
261
|
-
* - SSR hydration compatibility (server and client generate same IDs for same array)
|
|
262
|
-
* - No global state pollution between form instances
|
|
263
|
-
* - Deterministic IDs within each array's lifecycle
|
|
264
|
-
*
|
|
265
|
-
* The token is provided by ArrayFieldComponent at its level with a factory
|
|
266
|
-
* that creates a fresh counter for each array instance.
|
|
267
|
-
*
|
|
268
|
-
* @example
|
|
269
|
-
* ```typescript
|
|
270
|
-
* // In ArrayFieldComponent
|
|
271
|
-
* providers: [
|
|
272
|
-
* { provide: ARRAY_ITEM_ID_GENERATOR, useFactory: createArrayItemIdGenerator }
|
|
273
|
-
* ]
|
|
274
|
-
*
|
|
275
|
-
* // Usage
|
|
276
|
-
* const generateId = inject(ARRAY_ITEM_ID_GENERATOR);
|
|
277
|
-
* const itemId = generateId(); // 'item-0', 'item-1', etc.
|
|
278
|
-
* ```
|
|
277
|
+
* Shared empty chain — returned when no wrappers apply so callers get a
|
|
278
|
+
* stable reference. Freezing makes accidental mutation observable.
|
|
279
279
|
*/
|
|
280
|
-
const
|
|
280
|
+
const EMPTY_WRAPPERS = Object.freeze([]);
|
|
281
281
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
282
|
+
* Element-wise identity (`===`) comparator for wrapper chains. Used as the
|
|
283
|
+
* `equal` option on signal memoisation so reconciled `FieldDef`s with the
|
|
284
|
+
* same chain don't churn downstream — relies on `WrapperConfig` objects
|
|
285
|
+
* being stable across ticks, which holds for configs declared in
|
|
286
|
+
* `FormConfig` / `createWrappers(...)`.
|
|
284
287
|
*/
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
function isSameWrapperChain(a, b) {
|
|
289
|
+
if (a === b)
|
|
290
|
+
return true;
|
|
291
|
+
if (a.length !== b.length)
|
|
292
|
+
return false;
|
|
293
|
+
for (let i = 0; i < a.length; i++) {
|
|
294
|
+
if (a[i] !== b[i])
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolves the wrapper chain for a field.
|
|
301
|
+
*
|
|
302
|
+
* Merge order (outermost → innermost): auto-associations, form defaults,
|
|
303
|
+
* field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
|
|
304
|
+
* `skipDefaultWrappers` on the field opt out of individual layers. Fresh
|
|
305
|
+
* array per call — downstream memoisation is on the consuming computed.
|
|
306
|
+
*/
|
|
307
|
+
function resolveWrappers(field, defaultWrappers, autoAssociations) {
|
|
308
|
+
if (field.wrappers === null) {
|
|
309
|
+
return EMPTY_WRAPPERS;
|
|
310
|
+
}
|
|
311
|
+
const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
|
|
312
|
+
const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
|
|
313
|
+
const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
|
|
314
|
+
if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
|
|
315
|
+
return EMPTY_WRAPPERS;
|
|
316
|
+
}
|
|
317
|
+
return [...autoWrappers, ...defaults, ...fieldLevel];
|
|
288
318
|
}
|
|
289
319
|
|
|
290
320
|
/**
|
|
@@ -324,8 +354,7 @@ function getDeclaredInputs(componentType) {
|
|
|
324
354
|
* avoiding the reflection scan on each call is meaningful under heavy typing.
|
|
325
355
|
*/
|
|
326
356
|
function setInputIfDeclared(ref, inputName, value) {
|
|
327
|
-
|
|
328
|
-
if (declared.has(inputName)) {
|
|
357
|
+
if (getDeclaredInputs(ref.componentType).has(inputName)) {
|
|
329
358
|
ref.setInput(inputName, value);
|
|
330
359
|
}
|
|
331
360
|
}
|
|
@@ -482,58 +511,6 @@ class WrapperAwareInjector extends Injector {
|
|
|
482
511
|
}
|
|
483
512
|
}
|
|
484
513
|
|
|
485
|
-
/**
|
|
486
|
-
* Injection token for the dynamic forms logger.
|
|
487
|
-
*
|
|
488
|
-
* Provided by provideDynamicForm() with ConsoleLogger as default.
|
|
489
|
-
* Override with withLoggerConfig(false) to disable logging.
|
|
490
|
-
*/
|
|
491
|
-
const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Shared empty chain — returned when no wrappers apply so callers get a
|
|
495
|
-
* stable reference. Freezing makes accidental mutation observable.
|
|
496
|
-
*/
|
|
497
|
-
const EMPTY_WRAPPERS = Object.freeze([]);
|
|
498
|
-
/**
|
|
499
|
-
* Element-wise identity (`===`) comparator for wrapper chains. Used as the
|
|
500
|
-
* `equal` option on signal memoisation so reconciled `FieldDef`s with the
|
|
501
|
-
* same chain don't churn downstream — relies on `WrapperConfig` objects
|
|
502
|
-
* being stable across ticks, which holds for configs declared in
|
|
503
|
-
* `FormConfig` / `createWrappers(...)`.
|
|
504
|
-
*/
|
|
505
|
-
function isSameWrapperChain(a, b) {
|
|
506
|
-
if (a === b)
|
|
507
|
-
return true;
|
|
508
|
-
if (a.length !== b.length)
|
|
509
|
-
return false;
|
|
510
|
-
for (let i = 0; i < a.length; i++) {
|
|
511
|
-
if (a[i] !== b[i])
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Resolves the wrapper chain for a field.
|
|
518
|
-
*
|
|
519
|
-
* Merge order (outermost → innermost): auto-associations, form defaults,
|
|
520
|
-
* field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
|
|
521
|
-
* `skipDefaultWrappers` on the field opt out of individual layers. Fresh
|
|
522
|
-
* array per call — downstream memoisation is on the consuming computed.
|
|
523
|
-
*/
|
|
524
|
-
function resolveWrappers(field, defaultWrappers, autoAssociations) {
|
|
525
|
-
if (field.wrappers === null) {
|
|
526
|
-
return EMPTY_WRAPPERS;
|
|
527
|
-
}
|
|
528
|
-
const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
|
|
529
|
-
const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
|
|
530
|
-
const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
|
|
531
|
-
if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
|
|
532
|
-
return EMPTY_WRAPPERS;
|
|
533
|
-
}
|
|
534
|
-
return [...autoWrappers, ...defaults, ...fieldLevel];
|
|
535
|
-
}
|
|
536
|
-
|
|
537
514
|
/**
|
|
538
515
|
* Owns the wrapper-chain lifecycle for a field or container: async component
|
|
539
516
|
* loading, switchMap-based cancellation of stale loads, flicker-tolerant
|
|
@@ -545,6 +522,9 @@ function createWrapperChainController(opts) {
|
|
|
545
522
|
const state = createStateSignal(opts);
|
|
546
523
|
const mounted = { value: null };
|
|
547
524
|
let refs = [];
|
|
525
|
+
// INVARIANT: toObservable schedules emissions as microtasks, so `renderInnermost`
|
|
526
|
+
// runs outside any parent CD pass. Don't swap for sync signal wiring without
|
|
527
|
+
// restoring df-field-outlet's detectChanges — NG0950 would re-surface.
|
|
548
528
|
buildEmissionStream(state, deps)
|
|
549
529
|
.pipe(takeUntilDestroyed(deps.destroyRef))
|
|
550
530
|
.subscribe((emission) => {
|
|
@@ -691,6 +671,20 @@ function toReadonlyFieldTree(field) {
|
|
|
691
671
|
errors: state.errors,
|
|
692
672
|
};
|
|
693
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Write a value into a FieldTree's underlying signal. Signal Forms stores
|
|
676
|
+
* `FieldState.value` as a `WritableSignal`; this helper centralises the cast
|
|
677
|
+
* and warns if that contract ever changes (vs a silent no-op).
|
|
678
|
+
*/
|
|
679
|
+
function writeToFieldValue(value, next, logger) {
|
|
680
|
+
const candidate = value;
|
|
681
|
+
if (typeof candidate.set !== 'function') {
|
|
682
|
+
logger?.warn('writeToFieldValue: FieldState.value is not a WritableSignal — Signal Forms contract changed.');
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
candidate.set(next);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
694
688
|
/**
|
|
695
689
|
* DI-scoped cache for `ReadonlyFieldTree` views, keyed on the source FieldTree
|
|
696
690
|
* identity. A fresh WeakMap per root injector keeps renders isolated under SSR —
|
|
@@ -721,6 +715,209 @@ function toReadonlyFieldTreeCached(cache, field) {
|
|
|
721
715
|
return view;
|
|
722
716
|
}
|
|
723
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Convert a field's raw mapper outputs into the {@link WrapperFieldInputs}
|
|
720
|
+
* bag consumed by wrappers and addons.
|
|
721
|
+
*
|
|
722
|
+
* The transformation hides write access on the underlying `FieldTree` —
|
|
723
|
+
* the bag's `field` slot is a {@link ReadonlyFieldTree} view. The raw
|
|
724
|
+
* `FieldTree` is still pushed to the innermost field component (which
|
|
725
|
+
* legitimately needs to write); intermediaries (wrappers, addons) are
|
|
726
|
+
* read-only by contract.
|
|
727
|
+
*
|
|
728
|
+
* Pure, dependency-injection-free — caller supplies the cache so the same
|
|
729
|
+
* `ReadonlyFieldTree` view is returned for the same source `FieldTree`
|
|
730
|
+
* across renders. Each `<df-dynamic-form>` instance scopes its own cache
|
|
731
|
+
* (SSR-safe).
|
|
732
|
+
*
|
|
733
|
+
* @param rawInputs Mapper outputs (`{ key, label, props, field?: FieldTree, ... }`).
|
|
734
|
+
* @param cache `WeakMap<FieldTree, ReadonlyFieldTree>` provided via
|
|
735
|
+
* {@link READONLY_FIELD_TREE_CACHE}.
|
|
736
|
+
* @param fieldType Optional field-type discriminant. Injected here (rather
|
|
737
|
+
* than via the field mapper) because `type` is the
|
|
738
|
+
* registry key, not a component input — but addons and
|
|
739
|
+
* wrappers legitimately need it.
|
|
740
|
+
*/
|
|
741
|
+
function buildFieldInputs(rawInputs, cache, fieldType) {
|
|
742
|
+
const fieldTreeCandidate = rawInputs['field'];
|
|
743
|
+
// FieldTree is callable: `(): FieldState`. Anything else (undefined,
|
|
744
|
+
// primitive) means the field is rendered before the form is bound.
|
|
745
|
+
const hasFieldTree = fieldTreeCandidate !== undefined && typeof fieldTreeCandidate === 'function';
|
|
746
|
+
const readonlyField = hasFieldTree ? toReadonlyFieldTreeCached(cache, fieldTreeCandidate) : undefined;
|
|
747
|
+
// Lazy writer keyed on the raw FieldTree. Addons (e.g., `prime-button`
|
|
748
|
+
// presets) call this to mutate the host field. `writeToFieldValue`
|
|
749
|
+
// centralises the `Signal → WritableSignal` cast and warns at runtime
|
|
750
|
+
// if Signal Forms ever changes the contract.
|
|
751
|
+
const setValue = hasFieldTree
|
|
752
|
+
? (next) => writeToFieldValue(fieldTreeCandidate().value, next)
|
|
753
|
+
: undefined;
|
|
754
|
+
// Shallow spread — relies on the mapper contract (see WrapperFieldInputs)
|
|
755
|
+
// that rawInputs are emitted as fresh snapshots, not mutated in place.
|
|
756
|
+
return {
|
|
757
|
+
...rawInputs,
|
|
758
|
+
type: fieldType,
|
|
759
|
+
field: readonlyField,
|
|
760
|
+
setValue,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const EMPTY_STATE = Object.freeze({ phase: 'empty' });
|
|
765
|
+
/**
|
|
766
|
+
* Encapsulates the imperative `ViewContainerRef` / `ComponentRef` lifecycle
|
|
767
|
+
* for the innermost field component. Pulled out of `DfFieldOutlet` so the
|
|
768
|
+
* directive itself stays declarative — signals + constructor wiring — while
|
|
769
|
+
* the unavoidable mutable bookkeeping (Angular's view container API is
|
|
770
|
+
* imperative) lives in one place behind a small, focused surface.
|
|
771
|
+
*
|
|
772
|
+
* State is held in a single `signal<FieldSlotState>` discriminated union;
|
|
773
|
+
* every mutation is an explicit `state.set(...)` transition. No nullable
|
|
774
|
+
* instance fields, no `undefined`-initialized refs — each phase carries
|
|
775
|
+
* exactly the data it owns.
|
|
776
|
+
*
|
|
777
|
+
* Responsibilities:
|
|
778
|
+
* - Create the component (or reuse the same-class instance across a rebuild,
|
|
779
|
+
* preserving its DOM state, focus, and caret).
|
|
780
|
+
* - Push mapper inputs via `setInput`, dedup'd per-key so dev-mode NG0303
|
|
781
|
+
* warnings for undeclared mapper keys don't replay on every emission.
|
|
782
|
+
* - Detach cleanly before the outer VCR clears, so same-class reuse can
|
|
783
|
+
* re-insert the preserved `hostView`.
|
|
784
|
+
* - Destroy on directive teardown when the host view is still detached.
|
|
785
|
+
*/
|
|
786
|
+
class FieldComponentSlot {
|
|
787
|
+
state = signal(EMPTY_STATE, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
788
|
+
/**
|
|
789
|
+
* Per-key cache for `pushInputs`. Lives outside the lifecycle state machine on
|
|
790
|
+
* purpose — it's a `setInput`-call optimization that changes on every keystroke,
|
|
791
|
+
* not a phase transition. Keeping it here means the `state` signal only emits
|
|
792
|
+
* on real `empty ⇄ mounted ⇄ detached` transitions (≈ once per
|
|
793
|
+
* mount/destroy cycle), not once per input emission.
|
|
794
|
+
*/
|
|
795
|
+
lastInputs = null;
|
|
796
|
+
/** Read-only view of the lifecycle phase. Useful for tests and assertions. */
|
|
797
|
+
phase = () => this.state().phase;
|
|
798
|
+
/** Read-only view of the full state. Exposed so tests can assert ref stability. */
|
|
799
|
+
snapshot = () => this.state();
|
|
800
|
+
/**
|
|
801
|
+
* Mount the field component into `slot`. Reuses the existing `ComponentRef`
|
|
802
|
+
* when its component type matches (preserving DOM/focus/caret across a
|
|
803
|
+
* wrapper-chain rebuild); otherwise discards the old ref and creates a fresh
|
|
804
|
+
* one.
|
|
805
|
+
*/
|
|
806
|
+
mountOrReuse(slot, component, fieldInjector, environmentInjector, rawInputs, fieldInputs) {
|
|
807
|
+
const current = this.state();
|
|
808
|
+
if (current.phase !== 'empty' && current.ref.componentType === component) {
|
|
809
|
+
slot.insert(current.ref.hostView);
|
|
810
|
+
this.lastInputs = null;
|
|
811
|
+
this.state.set(Object.freeze({ phase: 'mounted', ref: current.ref, slot }));
|
|
812
|
+
this.pushInputs(rawInputs, fieldInputs);
|
|
813
|
+
if (current.phase === 'detached')
|
|
814
|
+
this.replayFocus(current.focusSnapshot);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
// Different component class — destroy the prior ref (if any) and create
|
|
818
|
+
// a fresh one. The `empty` transition before `createComponent` ensures a
|
|
819
|
+
// throw inside the factory doesn't leave a dangling ref the next mount
|
|
820
|
+
// would try to re-insert.
|
|
821
|
+
if (current.phase !== 'empty')
|
|
822
|
+
current.ref.destroy();
|
|
823
|
+
this.lastInputs = null;
|
|
824
|
+
this.state.set(EMPTY_STATE);
|
|
825
|
+
const ref = slot.createComponent(component, {
|
|
826
|
+
environmentInjector,
|
|
827
|
+
injector: createWrapperAwareInjector(fieldInjector, slot.injector),
|
|
828
|
+
});
|
|
829
|
+
this.state.set(Object.freeze({ phase: 'mounted', ref, slot }));
|
|
830
|
+
this.pushInputs(rawInputs, fieldInputs);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Push mapper inputs onto the mounted component. Per-key reference compare
|
|
834
|
+
* skips `setInput` for unchanged values — necessary in dev mode where
|
|
835
|
+
* `setInput` on unknown keys logs NG0303 per call (Angular 21 is lenient
|
|
836
|
+
* at runtime but warns); without the cache, every emission would re-warn
|
|
837
|
+
* for every undeclared mapper key.
|
|
838
|
+
*
|
|
839
|
+
* Updates the `lastInputs` cache (a plain field, not part of `state`) so the
|
|
840
|
+
* `state` signal doesn't churn its reference on every keystroke.
|
|
841
|
+
*
|
|
842
|
+
* `reflectComponentType` does NOT include hostDirective-forwarded inputs
|
|
843
|
+
* (angular/angular#49734), so a declared-input filter here would skip every
|
|
844
|
+
* value flowing through `NgForgeFieldShell` / `NgForgeField` /
|
|
845
|
+
* `NgForgeAction`. Mapper-as-contract is enforced by the `*_INPUTS`
|
|
846
|
+
* lockstep type assertions, not at runtime here.
|
|
847
|
+
*/
|
|
848
|
+
pushInputs(rawInputs, fieldInputs) {
|
|
849
|
+
const current = this.state();
|
|
850
|
+
if (current.phase !== 'mounted')
|
|
851
|
+
return;
|
|
852
|
+
const prev = this.lastInputs;
|
|
853
|
+
if (prev === rawInputs)
|
|
854
|
+
return;
|
|
855
|
+
for (const [key, value] of Object.entries(rawInputs)) {
|
|
856
|
+
if (prev && prev[key] === value)
|
|
857
|
+
continue;
|
|
858
|
+
current.ref.setInput(key, value);
|
|
859
|
+
}
|
|
860
|
+
// Side-channel forwarding for the `fieldInputs` bag — gated on
|
|
861
|
+
// `setInputIfDeclared` so leaf fields that don't declare it (most
|
|
862
|
+
// non-addon-aware components) skip silently rather than triggering
|
|
863
|
+
// NG0303 in dev. Pushed unconditionally on every emission since the bag
|
|
864
|
+
// changes identity when `rawInputs` does.
|
|
865
|
+
if (fieldInputs !== undefined) {
|
|
866
|
+
setInputIfDeclared(current.ref, 'fieldInputs', fieldInputs);
|
|
867
|
+
}
|
|
868
|
+
this.lastInputs = rawInputs;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Detach the host view from its current slot, capturing focus + caret so the
|
|
872
|
+
* next `mountOrReuse()` can replay them after re-inserting into the new
|
|
873
|
+
* slot. A stranded `mounted` state with a bad slot index (re-entrancy during
|
|
874
|
+
* a partial rebuild) is treated like `empty` to avoid leaks.
|
|
875
|
+
*/
|
|
876
|
+
detach() {
|
|
877
|
+
const current = this.state();
|
|
878
|
+
if (current.phase !== 'mounted')
|
|
879
|
+
return;
|
|
880
|
+
const focusSnapshot = this.captureFocus(current.ref);
|
|
881
|
+
const idx = current.slot.indexOf(current.ref.hostView);
|
|
882
|
+
if (idx >= 0)
|
|
883
|
+
current.slot.detach(idx);
|
|
884
|
+
this.lastInputs = null;
|
|
885
|
+
this.state.set(Object.freeze({ phase: 'detached', ref: current.ref, focusSnapshot }));
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Called on directive teardown. Only destroys when the host view is still
|
|
889
|
+
* detached — otherwise the outer `vcr.clear()` cascade has already destroyed
|
|
890
|
+
* it (calling `.destroy()` again would throw).
|
|
891
|
+
*/
|
|
892
|
+
destroyOnTeardown() {
|
|
893
|
+
const current = this.state();
|
|
894
|
+
if (current.phase === 'detached')
|
|
895
|
+
current.ref.destroy();
|
|
896
|
+
this.lastInputs = null;
|
|
897
|
+
this.state.set(EMPTY_STATE);
|
|
898
|
+
}
|
|
899
|
+
captureFocus(ref) {
|
|
900
|
+
const hostEl = ref.location.nativeElement;
|
|
901
|
+
const active = document.activeElement;
|
|
902
|
+
if (!(active instanceof HTMLElement) || !hostEl.contains(active))
|
|
903
|
+
return null;
|
|
904
|
+
const sel = active;
|
|
905
|
+
return {
|
|
906
|
+
element: active,
|
|
907
|
+
selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
|
|
908
|
+
selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
replayFocus(snap) {
|
|
912
|
+
if (!snap || !snap.element.isConnected)
|
|
913
|
+
return;
|
|
914
|
+
snap.element.focus();
|
|
915
|
+
if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
|
|
916
|
+
snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
724
921
|
/**
|
|
725
922
|
* Structural directive that renders a `ResolvedField` with its effective
|
|
726
923
|
* wrapper chain.
|
|
@@ -741,12 +938,16 @@ function toReadonlyFieldTreeCached(cache, field) {
|
|
|
741
938
|
* includes the field's mapper outputs and a `ReadonlyFieldTree` view of its
|
|
742
939
|
* form state.
|
|
743
940
|
*
|
|
744
|
-
* Rendering is gated by `field.renderReady()` — the
|
|
745
|
-
* the mapper produces the required inputs
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* a rebuild.
|
|
941
|
+
* Rendering is gated by `field.renderReady() && !field.hidden()` — the
|
|
942
|
+
* directive waits until the mapper produces the required inputs AND the
|
|
943
|
+
* field isn't hidden before instantiating the component. Once rendered, a
|
|
944
|
+
* transient `renderReady → false` does *not* tear the chain down; the
|
|
945
|
+
* controller keeps the mounted components alive and ignores the flicker.
|
|
946
|
+
* Only a structural change (wrappers or component class) triggers a rebuild.
|
|
947
|
+
*
|
|
948
|
+
* Imperative `ComponentRef` / `ViewContainerRef` lifecycle is encapsulated
|
|
949
|
+
* in `FieldComponentSlot` — the directive itself is signal definitions
|
|
950
|
+
* plus three method calls (`mountOrReuse`, `detach`, `destroyOnTeardown`).
|
|
750
951
|
*
|
|
751
952
|
* @example
|
|
752
953
|
* ```html
|
|
@@ -766,25 +967,16 @@ class DfFieldOutlet {
|
|
|
766
967
|
wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);
|
|
767
968
|
defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });
|
|
768
969
|
readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
* VCR when there are no wrappers, or the innermost wrapper's `#fieldComponent`
|
|
773
|
-
* slot). Tracked so `beforeRebuild` can detach the view before the outer VCR
|
|
774
|
-
* cascade-destroys it.
|
|
775
|
-
*/
|
|
776
|
-
fieldSlot;
|
|
777
|
-
/** Focus + caret captured during detach, replayed after re-insert. */
|
|
778
|
-
focusSnapshot;
|
|
970
|
+
/** Encapsulates Angular's imperative ComponentRef / ViewContainerRef lifecycle. */
|
|
971
|
+
fieldComponent = new FieldComponentSlot();
|
|
972
|
+
componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
|
|
779
973
|
/**
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
974
|
+
* Gate for the wrapper chain controller: true only when required inputs are populated AND the
|
|
975
|
+
* field isn't hidden. Combining the two prevents an initial-mount race where the chain mounts
|
|
976
|
+
* during the brief window between `renderReady` flipping true and `hidden()` settling — that
|
|
977
|
+
* window is exactly where Angular Signal Forms' `[formField]` directive emits NG01916.
|
|
784
978
|
*/
|
|
785
|
-
|
|
786
|
-
componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
|
|
787
|
-
renderReady = computed(() => this.dfFieldOutlet().renderReady(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
|
|
979
|
+
renderReady = computed(() => this.dfFieldOutlet().renderReady() && !this.dfFieldOutlet().hidden(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
|
|
788
980
|
rawInputs = computed(() => this.dfFieldOutlet().inputs(), ...(ngDevMode ? [{ debugName: "rawInputs" }] : /* istanbul ignore next */ []));
|
|
789
981
|
/**
|
|
790
982
|
* Effective wrapper chain. Element-wise identity comparison keeps the signal
|
|
@@ -793,11 +985,13 @@ class DfFieldOutlet {
|
|
|
793
985
|
*/
|
|
794
986
|
wrappers = computed(() => resolveWrappers(this.dfFieldOutlet().fieldDef, this.defaultWrappersSignal?.(), this.wrapperAutoAssociations), { ...(ngDevMode ? { debugName: "wrappers" } : /* istanbul ignore next */ {}), equal: isSameWrapperChain });
|
|
795
987
|
/**
|
|
796
|
-
* `fieldInputs` bag handed to every wrapper in the chain
|
|
797
|
-
*
|
|
798
|
-
*
|
|
988
|
+
* `fieldInputs` bag handed to every wrapper in the chain AND forwarded to
|
|
989
|
+
* the innermost field component (when it declares `fieldInputs` as an
|
|
990
|
+
* input). Memoised on `rawInputs` identity so repeated emissions with the
|
|
991
|
+
* same underlying object return the same view and don't cascade OnPush
|
|
992
|
+
* re-evaluations.
|
|
799
993
|
*/
|
|
800
|
-
fieldInputs = computed(() =>
|
|
994
|
+
fieldInputs = computed(() => buildFieldInputs(this.rawInputs(), this.readonlyFieldCache, this.dfFieldOutlet().fieldDef.type), ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
801
995
|
defaultEnvInjector = inject(EnvironmentInjector);
|
|
802
996
|
/** Environment injector for the innermost field component — `[environmentInjector]` input takes precedence over the directive's own DI. */
|
|
803
997
|
fieldEnvInjector = computed(() => this.dfFieldOutletEnvironmentInjector() ?? this.defaultEnvInjector, ...(ngDevMode ? [{ debugName: "fieldEnvInjector" }] : /* istanbul ignore next */ []));
|
|
@@ -811,126 +1005,23 @@ class DfFieldOutlet {
|
|
|
811
1005
|
rebuildKey: this.componentIdentity,
|
|
812
1006
|
fieldInputs: this.fieldInputs,
|
|
813
1007
|
fieldInjector: this.fieldInjector,
|
|
814
|
-
beforeRebuild: () => this.
|
|
1008
|
+
beforeRebuild: () => this.fieldComponent.detach(),
|
|
815
1009
|
renderInnermost: (slot) => {
|
|
816
1010
|
const resolved = this.dfFieldOutlet();
|
|
817
|
-
|
|
818
|
-
// Same component class — re-insert the preserved hostView. Browser
|
|
819
|
-
// loses focus on detach, so we replay the snapshot captured in
|
|
820
|
-
// detachFieldRef. Input value persists on the DOM node itself.
|
|
821
|
-
slot.insert(this.fieldRef.hostView);
|
|
822
|
-
this.fieldSlot = slot;
|
|
823
|
-
this.pushRawInputs(this.fieldRef, this.rawInputs());
|
|
824
|
-
this.restoreFocusSnapshot();
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
// Different component class — discard the old ref and create fresh.
|
|
828
|
-
// Merge field + element injectors so the field can inject ancestor
|
|
829
|
-
// wrappers AND form-context tokens (ARRAY_CONTEXT etc.).
|
|
830
|
-
// Clear state before createComponent — if it throws (bad provider,
|
|
831
|
-
// invalid class) we don't want to leak a reference to a destroyed ref
|
|
832
|
-
// that the same-component branch would later try to re-insert.
|
|
833
|
-
this.focusSnapshot = undefined;
|
|
834
|
-
this.lastPushedInputs = undefined;
|
|
835
|
-
const previousRef = this.fieldRef;
|
|
836
|
-
this.fieldRef = undefined;
|
|
837
|
-
this.fieldSlot = undefined;
|
|
838
|
-
previousRef?.destroy();
|
|
839
|
-
this.fieldRef = slot.createComponent(resolved.component, {
|
|
840
|
-
environmentInjector: this.fieldEnvInjector(),
|
|
841
|
-
injector: createWrapperAwareInjector(resolved.injector, slot.injector),
|
|
842
|
-
});
|
|
843
|
-
this.fieldSlot = slot;
|
|
844
|
-
this.pushRawInputs(this.fieldRef, this.rawInputs());
|
|
1011
|
+
this.fieldComponent.mountOrReuse(slot, resolved.component, resolved.injector, this.fieldEnvInjector(), this.rawInputs(), this.fieldInputs());
|
|
845
1012
|
},
|
|
846
1013
|
});
|
|
847
|
-
// Push rawInputs
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
return;
|
|
854
|
-
this.pushRawInputs(this.fieldRef, rawInputs);
|
|
855
|
-
});
|
|
856
|
-
this.destroyRef.onDestroy(() => {
|
|
857
|
-
// Only destroy when detached — vcr.clear() cascades otherwise.
|
|
858
|
-
if (this.fieldRef && !this.fieldSlot)
|
|
859
|
-
this.fieldRef.destroy();
|
|
860
|
-
this.fieldRef = undefined;
|
|
861
|
-
this.fieldSlot = undefined;
|
|
862
|
-
this.lastPushedInputs = undefined;
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
detachFieldRef() {
|
|
866
|
-
if (!this.fieldRef)
|
|
867
|
-
return;
|
|
868
|
-
if (!this.fieldSlot) {
|
|
869
|
-
// Stranded fieldRef — destroy to prevent a leak.
|
|
870
|
-
this.fieldRef.destroy();
|
|
871
|
-
this.fieldRef = undefined;
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
this.captureFocusSnapshot();
|
|
875
|
-
const idx = this.fieldSlot.indexOf(this.fieldRef.hostView);
|
|
876
|
-
if (idx >= 0)
|
|
877
|
-
this.fieldSlot.detach(idx);
|
|
878
|
-
this.fieldSlot = undefined;
|
|
879
|
-
}
|
|
880
|
-
/** Capture active element + selection before the hostView is detached from DOM. */
|
|
881
|
-
captureFocusSnapshot() {
|
|
882
|
-
if (!this.fieldRef)
|
|
883
|
-
return;
|
|
884
|
-
const hostEl = this.fieldRef.location.nativeElement;
|
|
885
|
-
const active = document.activeElement;
|
|
886
|
-
if (!(active instanceof HTMLElement) || !hostEl.contains(active))
|
|
887
|
-
return;
|
|
888
|
-
const sel = active;
|
|
889
|
-
this.focusSnapshot = {
|
|
890
|
-
element: active,
|
|
891
|
-
selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
|
|
892
|
-
selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
/** Re-apply the pre-detach focus + caret after the hostView is back in the DOM. */
|
|
896
|
-
restoreFocusSnapshot() {
|
|
897
|
-
const snap = this.focusSnapshot;
|
|
898
|
-
this.focusSnapshot = undefined;
|
|
899
|
-
if (!snap || !snap.element.isConnected)
|
|
900
|
-
return;
|
|
901
|
-
snap.element.focus();
|
|
902
|
-
if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
|
|
903
|
-
snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
pushRawInputs(ref, rawInputs) {
|
|
907
|
-
// Ref-identity dedupe per key — mappers must emit immutable snapshots.
|
|
908
|
-
const last = this.lastPushedInputs;
|
|
909
|
-
for (const [key, value] of Object.entries(rawInputs)) {
|
|
910
|
-
if (last && last[key] === value)
|
|
911
|
-
continue;
|
|
912
|
-
setInputIfDeclared(ref, key, value);
|
|
913
|
-
}
|
|
914
|
-
this.lastPushedInputs = rawInputs;
|
|
915
|
-
}
|
|
916
|
-
buildFieldInputs(rawInputs) {
|
|
917
|
-
const fieldTreeCandidate = rawInputs['field'];
|
|
918
|
-
// A FieldTree is a callable (() => FieldState). We expose it to wrappers via a narrow
|
|
919
|
-
// read-only view; raw FieldTree is still pushed to the innermost component for writes.
|
|
920
|
-
const readonlyField = fieldTreeCandidate && typeof fieldTreeCandidate === 'function'
|
|
921
|
-
? toReadonlyFieldTreeCached(this.readonlyFieldCache, fieldTreeCandidate)
|
|
922
|
-
: undefined;
|
|
923
|
-
// Shallow spread — relies on the mapper contract (see WrapperFieldInputs)
|
|
924
|
-
// that rawInputs are emitted as fresh snapshots, not mutated in place.
|
|
925
|
-
return {
|
|
926
|
-
...rawInputs,
|
|
927
|
-
field: readonlyField,
|
|
928
|
-
};
|
|
1014
|
+
// Push rawInputs (plus the wrapper-style fieldInputs bag) to the innermost
|
|
1015
|
+
// field on every change. The slot dedupes per-key, so the initial render's
|
|
1016
|
+
// synchronous push from `renderInnermost` and the effect's first fire
|
|
1017
|
+
// converge cleanly with no double-pushing.
|
|
1018
|
+
explicitEffect([this.rawInputs, this.fieldInputs], ([rawInputs, fieldInputs]) => this.fieldComponent.pushInputs(rawInputs, fieldInputs));
|
|
1019
|
+
this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());
|
|
929
1020
|
}
|
|
930
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
931
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1021
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1022
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: DfFieldOutlet, isStandalone: true, selector: "[dfFieldOutlet]", inputs: { dfFieldOutlet: { classPropertyName: "dfFieldOutlet", publicName: "dfFieldOutlet", isSignal: true, isRequired: true, transformFunction: null }, dfFieldOutletEnvironmentInjector: { classPropertyName: "dfFieldOutletEnvironmentInjector", publicName: "dfFieldOutletEnvironmentInjector", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
932
1023
|
}
|
|
933
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1024
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldOutlet, decorators: [{
|
|
934
1025
|
type: Directive,
|
|
935
1026
|
args: [{
|
|
936
1027
|
selector: '[dfFieldOutlet]',
|
|
@@ -1230,10 +1321,10 @@ class EventBus {
|
|
|
1230
1321
|
}
|
|
1231
1322
|
return this.pipeline$.pipe(filter((event) => event.type === eventType));
|
|
1232
1323
|
}
|
|
1233
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1234
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1324
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1325
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus });
|
|
1235
1326
|
}
|
|
1236
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1327
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventBus, decorators: [{
|
|
1237
1328
|
type: Injectable
|
|
1238
1329
|
}] });
|
|
1239
1330
|
|
|
@@ -2744,16 +2835,25 @@ function evaluateCondition(expression, context) {
|
|
|
2744
2835
|
case 'javascript':
|
|
2745
2836
|
return evaluateJavaScriptExpression(expression, context);
|
|
2746
2837
|
case 'custom': {
|
|
2747
|
-
|
|
2838
|
+
// Read both keys via cast — the XOR type narrows to `never` when both
|
|
2839
|
+
// would be truthy, but JSON-loaded configs can produce that state at
|
|
2840
|
+
// runtime. We accept it here, warn, and let inline win.
|
|
2841
|
+
const bothSet = expression;
|
|
2842
|
+
const inlineFn = bothSet.fn;
|
|
2843
|
+
const registeredName = bothSet.functionName;
|
|
2844
|
+
if (inlineFn && registeredName) {
|
|
2845
|
+
context.logger.warn('Both "fn" and "functionName" are set on custom condition. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
2846
|
+
}
|
|
2847
|
+
const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);
|
|
2748
2848
|
if (!customFn) {
|
|
2749
|
-
context.logger.error('Custom function not found:',
|
|
2849
|
+
context.logger.error('Custom function not found:', registeredName);
|
|
2750
2850
|
return false;
|
|
2751
2851
|
}
|
|
2752
2852
|
try {
|
|
2753
2853
|
return !!customFn(context);
|
|
2754
2854
|
}
|
|
2755
2855
|
catch (error) {
|
|
2756
|
-
context.logger.error('Error executing custom function:',
|
|
2856
|
+
context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);
|
|
2757
2857
|
return false;
|
|
2758
2858
|
}
|
|
2759
2859
|
}
|
|
@@ -2762,7 +2862,7 @@ function evaluateCondition(expression, context) {
|
|
|
2762
2862
|
case 'or':
|
|
2763
2863
|
return evaluateOrCondition(expression, context);
|
|
2764
2864
|
case 'http':
|
|
2765
|
-
context.logger.warn('
|
|
2865
|
+
context.logger.warn('HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
|
|
2766
2866
|
'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.');
|
|
2767
2867
|
return false;
|
|
2768
2868
|
case 'async':
|
|
@@ -3298,10 +3398,10 @@ class FunctionRegistryService {
|
|
|
3298
3398
|
this.clearAsyncValidators();
|
|
3299
3399
|
this.clearHttpValidators();
|
|
3300
3400
|
}
|
|
3301
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3302
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3401
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3402
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService });
|
|
3303
3403
|
}
|
|
3304
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3404
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FunctionRegistryService, decorators: [{
|
|
3305
3405
|
type: Injectable
|
|
3306
3406
|
}] });
|
|
3307
3407
|
|
|
@@ -3752,10 +3852,10 @@ class FieldContextRegistryService {
|
|
|
3752
3852
|
},
|
|
3753
3853
|
};
|
|
3754
3854
|
}
|
|
3755
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3756
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3855
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3856
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService });
|
|
3757
3857
|
}
|
|
3758
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3858
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldContextRegistryService, decorators: [{
|
|
3759
3859
|
type: Injectable
|
|
3760
3860
|
}] });
|
|
3761
3861
|
|
|
@@ -4110,8 +4210,8 @@ class PageOrchestratorComponent {
|
|
|
4110
4210
|
}
|
|
4111
4211
|
return false;
|
|
4112
4212
|
}
|
|
4113
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4114
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4213
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4214
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
|
|
4115
4215
|
{
|
|
4116
4216
|
provide: FIELD_SIGNAL_CONTEXT,
|
|
4117
4217
|
useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
|
|
@@ -4141,9 +4241,9 @@ class PageOrchestratorComponent {
|
|
|
4141
4241
|
}
|
|
4142
4242
|
}
|
|
4143
4243
|
}
|
|
4144
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4244
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)]] });
|
|
4145
4245
|
}
|
|
4146
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.
|
|
4246
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
4147
4247
|
type: Component,
|
|
4148
4248
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
4149
4249
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -4575,6 +4675,85 @@ function isValidNonPagedForm(fields) {
|
|
|
4575
4675
|
return result.mode === 'non-paged' && result.isValid;
|
|
4576
4676
|
}
|
|
4577
4677
|
|
|
4678
|
+
function createSlot() {
|
|
4679
|
+
let counter = 0;
|
|
4680
|
+
const templates = new Map();
|
|
4681
|
+
const itemOrder = [];
|
|
4682
|
+
const primitiveFieldKey = signal(undefined, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
|
|
4683
|
+
return {
|
|
4684
|
+
templates,
|
|
4685
|
+
itemOrder,
|
|
4686
|
+
primitiveFieldKey,
|
|
4687
|
+
nextId: () => `item-${counter++}`,
|
|
4688
|
+
setTemplate(itemId, t) {
|
|
4689
|
+
templates.set(itemId, t);
|
|
4690
|
+
},
|
|
4691
|
+
getTemplate(itemId) {
|
|
4692
|
+
return templates.get(itemId);
|
|
4693
|
+
},
|
|
4694
|
+
hasTemplate(itemId) {
|
|
4695
|
+
return templates.has(itemId);
|
|
4696
|
+
},
|
|
4697
|
+
setIdAt(index, itemId) {
|
|
4698
|
+
itemOrder[index] = itemId;
|
|
4699
|
+
},
|
|
4700
|
+
removeAt(index) {
|
|
4701
|
+
const [removed] = itemOrder.splice(index, 1);
|
|
4702
|
+
if (removed !== undefined)
|
|
4703
|
+
templates.delete(removed);
|
|
4704
|
+
},
|
|
4705
|
+
moveItem(fromIndex, toIndex) {
|
|
4706
|
+
const [id] = itemOrder.splice(fromIndex, 1);
|
|
4707
|
+
if (id !== undefined)
|
|
4708
|
+
itemOrder.splice(toIndex, 0, id);
|
|
4709
|
+
},
|
|
4710
|
+
truncate(length) {
|
|
4711
|
+
const removed = itemOrder.splice(length);
|
|
4712
|
+
for (const id of removed)
|
|
4713
|
+
templates.delete(id);
|
|
4714
|
+
},
|
|
4715
|
+
clear() {
|
|
4716
|
+
templates.clear();
|
|
4717
|
+
itemOrder.length = 0;
|
|
4718
|
+
counter = 0;
|
|
4719
|
+
primitiveFieldKey.set(undefined);
|
|
4720
|
+
},
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
/**
|
|
4724
|
+
* Form-scoped registry of per-array durable state. One slot per array path
|
|
4725
|
+
* (e.g. 'members', 'team.members', 'orders.lineItems'). Survives
|
|
4726
|
+
* `ArrayFieldComponent` lifecycle, dies with the `DynamicForm`. Cleared on
|
|
4727
|
+
* schema swap by the `formSetup` effect in `FormStateManager`.
|
|
4728
|
+
*
|
|
4729
|
+
* `ArrayFieldComponent` injects this service and obtains its slot via
|
|
4730
|
+
* `slotFor(arrayPath)` on mount. Replaces the prior per-component
|
|
4731
|
+
* `ARRAY_TEMPLATE_REGISTRY` + `ARRAY_ITEM_ID_GENERATOR` providers.
|
|
4732
|
+
*/
|
|
4733
|
+
class ArrayItemRegistryService {
|
|
4734
|
+
slots = new Map();
|
|
4735
|
+
/** Get-or-create a slot for the array at the given path. */
|
|
4736
|
+
slotFor(arrayPath) {
|
|
4737
|
+
let slot = this.slots.get(arrayPath);
|
|
4738
|
+
if (!slot) {
|
|
4739
|
+
slot = createSlot();
|
|
4740
|
+
this.slots.set(arrayPath, slot);
|
|
4741
|
+
}
|
|
4742
|
+
return slot;
|
|
4743
|
+
}
|
|
4744
|
+
/** Drop all slots — called from the `formSetup` effect on schema change. */
|
|
4745
|
+
clearAll() {
|
|
4746
|
+
for (const slot of this.slots.values())
|
|
4747
|
+
slot.clear();
|
|
4748
|
+
this.slots.clear();
|
|
4749
|
+
}
|
|
4750
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4751
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService });
|
|
4752
|
+
}
|
|
4753
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayItemRegistryService, decorators: [{
|
|
4754
|
+
type: Injectable
|
|
4755
|
+
}] });
|
|
4756
|
+
|
|
4578
4757
|
class SchemaRegistryService {
|
|
4579
4758
|
registeredSchemas = new Map();
|
|
4580
4759
|
/**
|
|
@@ -4610,10 +4789,10 @@ class SchemaRegistryService {
|
|
|
4610
4789
|
}
|
|
4611
4790
|
return schema;
|
|
4612
4791
|
}
|
|
4613
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4614
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4792
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4793
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService });
|
|
4615
4794
|
}
|
|
4616
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4795
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SchemaRegistryService, decorators: [{
|
|
4617
4796
|
type: Injectable
|
|
4618
4797
|
}] });
|
|
4619
4798
|
|
|
@@ -4624,7 +4803,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
4624
4803
|
* Use this guard when you need type-safe access to `FunctionHttpValidatorConfig` properties.
|
|
4625
4804
|
*/
|
|
4626
4805
|
function isFunctionHttpValidator(config) {
|
|
4627
|
-
|
|
4806
|
+
const c = config;
|
|
4807
|
+
return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');
|
|
4628
4808
|
}
|
|
4629
4809
|
|
|
4630
4810
|
/**
|
|
@@ -4820,8 +5000,6 @@ class HttpConditionFunctionCacheService {
|
|
|
4820
5000
|
* );
|
|
4821
5001
|
* // user.value() keeps the old user data during loading transitions
|
|
4822
5002
|
* ```
|
|
4823
|
-
*
|
|
4824
|
-
* @experimental Uses Angular's experimental resource composition APIs.
|
|
4825
5003
|
*/
|
|
4826
5004
|
function withPreviousValue(input) {
|
|
4827
5005
|
const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
|
|
@@ -5043,11 +5221,18 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5043
5221
|
const cacheService = inject(AsyncConditionFunctionCacheService);
|
|
5044
5222
|
const pendingValue = condition.pendingValue ?? false;
|
|
5045
5223
|
const debounceMs = condition.debounceMs ?? 300;
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5224
|
+
if (condition.asyncFn && condition.asyncFunctionName) {
|
|
5225
|
+
logger.warn('Both "asyncFn" and "asyncFunctionName" are set on async condition. Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.');
|
|
5226
|
+
}
|
|
5227
|
+
// Skip cross-condition caching when an inline function is used: stableStringify
|
|
5228
|
+
// cannot distinguish two different inline functions, which would cause cache hits
|
|
5229
|
+
// to wrongly reuse a LogicFn closing over a different function reference.
|
|
5230
|
+
const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);
|
|
5231
|
+
if (cacheKey !== undefined) {
|
|
5232
|
+
const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
|
|
5233
|
+
if (cached) {
|
|
5234
|
+
return cached;
|
|
5235
|
+
}
|
|
5051
5236
|
}
|
|
5052
5237
|
// Each condition needs its own per-field signal store.
|
|
5053
5238
|
const perFunctionSignalStore = new Map();
|
|
@@ -5072,8 +5257,10 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5072
5257
|
const asyncResource = rxResource({
|
|
5073
5258
|
params: () => debouncedTrigger() ?? undefined,
|
|
5074
5259
|
stream: ({ params: triggerKey }) => {
|
|
5075
|
-
//
|
|
5076
|
-
|
|
5260
|
+
// Inline `asyncFn` wins; otherwise look up the registered function at call time
|
|
5261
|
+
// (fresh reference, in case the registry was updated after LogicFn creation).
|
|
5262
|
+
const asyncFn = condition.asyncFn ??
|
|
5263
|
+
(condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);
|
|
5077
5264
|
if (!asyncFn) {
|
|
5078
5265
|
logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
|
|
5079
5266
|
`Register it in customFnConfig.asyncConditions.`);
|
|
@@ -5125,7 +5312,9 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5125
5312
|
});
|
|
5126
5313
|
return resultResource.value();
|
|
5127
5314
|
};
|
|
5128
|
-
|
|
5315
|
+
if (cacheKey !== undefined) {
|
|
5316
|
+
cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
|
|
5317
|
+
}
|
|
5129
5318
|
return fn;
|
|
5130
5319
|
}
|
|
5131
5320
|
|
|
@@ -5760,12 +5949,12 @@ function applyCustomValidator(config, fieldPath) {
|
|
|
5760
5949
|
if (config.expression) {
|
|
5761
5950
|
validatorFn = createExpressionValidator(config);
|
|
5762
5951
|
}
|
|
5763
|
-
else if (config.functionName) {
|
|
5952
|
+
else if (config.fn || config.functionName) {
|
|
5764
5953
|
validatorFn = createFunctionValidator(config);
|
|
5765
5954
|
}
|
|
5766
5955
|
else {
|
|
5767
5956
|
const logger = inject(DynamicFormLogger);
|
|
5768
|
-
logger.warn('Custom validator must have
|
|
5957
|
+
logger.warn('Custom validator must have one of "expression", "functionName", or "fn"');
|
|
5769
5958
|
return;
|
|
5770
5959
|
}
|
|
5771
5960
|
const whenLogic = createConditionalLogic(config.when);
|
|
@@ -5777,6 +5966,13 @@ function applyCustomValidator(config, fieldPath) {
|
|
|
5777
5966
|
}
|
|
5778
5967
|
function createFunctionValidator(config) {
|
|
5779
5968
|
const logger = inject(DynamicFormLogger);
|
|
5969
|
+
if (config.fn && config.functionName) {
|
|
5970
|
+
logger.warn('Both "fn" and "functionName" are set on custom validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
5971
|
+
}
|
|
5972
|
+
if (config.fn) {
|
|
5973
|
+
const inlineFn = config.fn;
|
|
5974
|
+
return (ctx) => inlineFn(ctx, config.params);
|
|
5975
|
+
}
|
|
5780
5976
|
const functionName = config.functionName;
|
|
5781
5977
|
if (!functionName) {
|
|
5782
5978
|
logger.warn('Custom validator missing functionName');
|
|
@@ -5837,10 +6033,24 @@ function createExpressionValidator(config) {
|
|
|
5837
6033
|
* - onError: Optional handler for resource errors
|
|
5838
6034
|
*/
|
|
5839
6035
|
function applyAsyncValidator(config, fieldPath) {
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
6036
|
+
if (config.fn && config.functionName) {
|
|
6037
|
+
const logger = inject(DynamicFormLogger);
|
|
6038
|
+
logger.warn('Both "fn" and "functionName" are set on async validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
6039
|
+
}
|
|
6040
|
+
let validatorConfig;
|
|
6041
|
+
if (config.fn) {
|
|
6042
|
+
validatorConfig = config.fn;
|
|
6043
|
+
}
|
|
6044
|
+
else if (config.functionName) {
|
|
6045
|
+
const registry = inject(FunctionRegistryService);
|
|
6046
|
+
validatorConfig = registry.getAsyncValidator(config.functionName);
|
|
6047
|
+
if (!validatorConfig) {
|
|
6048
|
+
throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
else {
|
|
6052
|
+
// XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
|
|
6053
|
+
throw new DynamicFormError('Async validator requires one of "functionName" or "fn".');
|
|
5844
6054
|
}
|
|
5845
6055
|
const whenLogic = createConditionalLogic(config.when);
|
|
5846
6056
|
const asyncOptions = {
|
|
@@ -5862,10 +6072,16 @@ function applyAsyncValidator(config, fieldPath) {
|
|
|
5862
6072
|
function applyUnifiedHttpValidator(config, fieldPath) {
|
|
5863
6073
|
if (isFunctionHttpValidator(config)) {
|
|
5864
6074
|
applyFunctionHttpValidator(config, fieldPath);
|
|
6075
|
+
return;
|
|
5865
6076
|
}
|
|
5866
|
-
|
|
5867
|
-
|
|
6077
|
+
// Declarative path requires both `http` and `responseMapping` — reject early
|
|
6078
|
+
// for JSON-loaded configs that have neither side fully populated. The
|
|
6079
|
+
// discriminated union forbids this at compile time.
|
|
6080
|
+
const declarative = config;
|
|
6081
|
+
if (!declarative.http || !declarative.responseMapping) {
|
|
6082
|
+
throw new DynamicFormError('HTTP validator requires one of "functionName" / "fn" (function-based) or "http" + "responseMapping" (declarative).');
|
|
5868
6083
|
}
|
|
6084
|
+
applyDeclarativeHttpValidator(config, fieldPath);
|
|
5869
6085
|
}
|
|
5870
6086
|
/**
|
|
5871
6087
|
* Apply function-based HTTP validator to field path using Angular's public validateHttp() API.
|
|
@@ -5876,10 +6092,24 @@ function applyUnifiedHttpValidator(config, fieldPath) {
|
|
|
5876
6092
|
* - onError: Optional handler for HTTP errors
|
|
5877
6093
|
*/
|
|
5878
6094
|
function applyFunctionHttpValidator(config, fieldPath) {
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6095
|
+
if (config.fn && config.functionName) {
|
|
6096
|
+
const logger = inject(DynamicFormLogger);
|
|
6097
|
+
logger.warn('Both "fn" and "functionName" are set on HTTP validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
6098
|
+
}
|
|
6099
|
+
let httpValidatorConfig;
|
|
6100
|
+
if (config.fn) {
|
|
6101
|
+
httpValidatorConfig = config.fn;
|
|
6102
|
+
}
|
|
6103
|
+
else if (config.functionName) {
|
|
6104
|
+
const registry = inject(FunctionRegistryService);
|
|
6105
|
+
httpValidatorConfig = registry.getHttpValidator(config.functionName);
|
|
6106
|
+
if (!httpValidatorConfig) {
|
|
6107
|
+
throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
else {
|
|
6111
|
+
// XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
|
|
6112
|
+
throw new DynamicFormError('HTTP validator requires one of "functionName" or "fn".');
|
|
5883
6113
|
}
|
|
5884
6114
|
const whenLogic = createConditionalLogic(config.when);
|
|
5885
6115
|
const httpOptions = {
|
|
@@ -8232,9 +8462,13 @@ function buildClassName(fieldDef) {
|
|
|
8232
8462
|
* @returns Record of input names to values
|
|
8233
8463
|
*/
|
|
8234
8464
|
function buildBaseInputs(fieldDef, defaultProps) {
|
|
8235
|
-
const { key, label, className, tabIndex, props, meta } = fieldDef;
|
|
8465
|
+
const { key, label, className, tabIndex, props, meta, addons } = fieldDef;
|
|
8236
8466
|
const inputs = {};
|
|
8237
|
-
// Always include key
|
|
8467
|
+
// Always include key — required by components for accessibility and identification.
|
|
8468
|
+
// `type` is intentionally NOT propagated to the field component (it's the
|
|
8469
|
+
// registry discriminant, not a component input); wrappers and addons read it
|
|
8470
|
+
// from `WrapperFieldInputs.type` which is injected at the buildFieldInputs()
|
|
8471
|
+
// boundary by `DfFieldOutlet`.
|
|
8238
8472
|
inputs['key'] = key;
|
|
8239
8473
|
if (label !== undefined) {
|
|
8240
8474
|
inputs['label'] = label;
|
|
@@ -8264,6 +8498,13 @@ function buildBaseInputs(fieldDef, defaultProps) {
|
|
|
8264
8498
|
if (meta !== undefined) {
|
|
8265
8499
|
inputs['meta'] = meta;
|
|
8266
8500
|
}
|
|
8501
|
+
// Forward addons. Field components that don't declare an `addons` input
|
|
8502
|
+
// signal (Tier 3 — toggle, checkbox, radio, slider) ignore this key via
|
|
8503
|
+
// Angular's `setInput` no-op-on-unknown behaviour, so it's safe to forward
|
|
8504
|
+
// unconditionally.
|
|
8505
|
+
if (addons !== undefined) {
|
|
8506
|
+
inputs['addons'] = addons;
|
|
8507
|
+
}
|
|
8267
8508
|
return inputs;
|
|
8268
8509
|
}
|
|
8269
8510
|
/**
|
|
@@ -8463,22 +8704,31 @@ const PLACEHOLDER_INDEX = '$';
|
|
|
8463
8704
|
/**
|
|
8464
8705
|
* Builds a property override store key for a field.
|
|
8465
8706
|
*
|
|
8466
|
-
* For non-array fields, the key is
|
|
8467
|
-
*
|
|
8468
|
-
*
|
|
8707
|
+
* For non-array fields, the key is the dot-joined group ancestors plus the
|
|
8708
|
+
* field's leaf key (e.g., `address.street` for `street` inside `address`).
|
|
8709
|
+
* For array item fields, the key includes the array path, either a concrete
|
|
8710
|
+
* index or the `$` placeholder, and any group ancestors INSIDE the array
|
|
8711
|
+
* item (e.g., `contacts.0.email`, `contacts.$.email`, or
|
|
8712
|
+
* `contacts.0.profile.email` for a group nested under each contact).
|
|
8713
|
+
*
|
|
8714
|
+
* `groupPath` matches the form-value path of nested groups (collector resets
|
|
8715
|
+
* it at array boundaries — see `property-derivation-collector.ts`), so the
|
|
8716
|
+
* resulting key always mirrors the field's location in `formValue`.
|
|
8469
8717
|
*
|
|
8470
8718
|
* @param arrayKey - The array field key (e.g., 'contacts'), or undefined for non-array fields
|
|
8471
8719
|
* @param index - The array item index, `PLACEHOLDER_INDEX` for the wildcard format, or undefined for non-array fields
|
|
8472
8720
|
* @param fieldKey - The leaf field key (e.g., 'email')
|
|
8473
|
-
* @
|
|
8721
|
+
* @param groupPath - Dot-joined group ancestors scoped to the current array item (or top-level), or undefined when not inside any group
|
|
8722
|
+
* @returns The store key (e.g., 'email', 'address.street', 'contacts.0.email', 'contacts.$.profile.email')
|
|
8474
8723
|
*
|
|
8475
8724
|
* @public
|
|
8476
8725
|
*/
|
|
8477
|
-
function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
|
|
8726
|
+
function buildPropertyOverrideKey(arrayKey, index, fieldKey, groupPath) {
|
|
8727
|
+
const leafScope = groupPath ? `${groupPath}.${fieldKey}` : fieldKey;
|
|
8478
8728
|
if (arrayKey != null && index != null) {
|
|
8479
|
-
return `${arrayKey}.${index}.${
|
|
8729
|
+
return `${arrayKey}.${index}.${leafScope}`;
|
|
8480
8730
|
}
|
|
8481
|
-
return
|
|
8731
|
+
return leafScope;
|
|
8482
8732
|
}
|
|
8483
8733
|
|
|
8484
8734
|
/**
|
|
@@ -8559,6 +8809,25 @@ function applyIndexSuffix(inputs, index) {
|
|
|
8559
8809
|
key: `${key}_${index}`,
|
|
8560
8810
|
};
|
|
8561
8811
|
}
|
|
8812
|
+
/**
|
|
8813
|
+
* Prefixes the key with the underscored group ancestor path so that the same
|
|
8814
|
+
* leaf key inside different groups produces distinct DOM IDs (issue #401).
|
|
8815
|
+
* Form-schema keys remain clean — only the rendered `key` input is rewritten.
|
|
8816
|
+
*
|
|
8817
|
+
* Dots in the group path become underscores, then the original leaf key is
|
|
8818
|
+
* appended: `(groupPath='user.address', inputs.key='street')` →
|
|
8819
|
+
* `inputs.key = 'user_address_street'`.
|
|
8820
|
+
*/
|
|
8821
|
+
function applyGroupPrefix(inputs, groupPath) {
|
|
8822
|
+
const key = inputs['key'];
|
|
8823
|
+
if (typeof key !== 'string' || groupPath.length === 0) {
|
|
8824
|
+
return inputs;
|
|
8825
|
+
}
|
|
8826
|
+
return {
|
|
8827
|
+
...inputs,
|
|
8828
|
+
key: `${groupPath.replace(/\./g, '_')}_${key}`,
|
|
8829
|
+
};
|
|
8830
|
+
}
|
|
8562
8831
|
/**
|
|
8563
8832
|
* Main field mapper function that uses the field registry to get the appropriate mapper
|
|
8564
8833
|
* based on the field's type property.
|
|
@@ -8595,6 +8864,10 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8595
8864
|
// Check if we're inside an array item context - if so, we need to suffix keys
|
|
8596
8865
|
// Optional because ARRAY_CONTEXT is only provided inside array item injectors
|
|
8597
8866
|
const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
8867
|
+
// Check if we're inside a group context - if so, prefix the key with the group path
|
|
8868
|
+
// so overlapping leaf keys across groups produce distinct DOM IDs (issue #401).
|
|
8869
|
+
// Optional because GROUP_CONTEXT is only provided by GroupFieldComponent.
|
|
8870
|
+
const groupContext = inject(GROUP_CONTEXT, { optional: true });
|
|
8598
8871
|
// Inject the property override store for property derivation overrides
|
|
8599
8872
|
// Always available — provided at the DynamicForm component level via provideDynamicFormDI
|
|
8600
8873
|
const store = inject(PROPERTY_OVERRIDE_STORE);
|
|
@@ -8605,6 +8878,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8605
8878
|
// Check that arrayContext exists and has a valid index signal (guards against mock injectors)
|
|
8606
8879
|
const indexSignal = arrayContext?.index;
|
|
8607
8880
|
const hasArrayContext = indexSignal !== undefined && isSignal(indexSignal);
|
|
8881
|
+
const hasGroupContext = groupContext != null;
|
|
8608
8882
|
// Fast-path check for property overrides: only fields whose definition declares
|
|
8609
8883
|
// a property-derivation enter the computed() wrapper. We inspect the FieldDef
|
|
8610
8884
|
// directly rather than asking the store, because the store's registration
|
|
@@ -8613,7 +8887,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8613
8887
|
// collector apply the same inclusion rule, so the answers always agree.
|
|
8614
8888
|
const hasOverrides = fieldHasPropertyDerivations(fieldDef);
|
|
8615
8889
|
// Fast path: no transformations needed
|
|
8616
|
-
if (!hasPropsForwarding && !hasArrayContext && !hasOverrides) {
|
|
8890
|
+
if (!hasPropsForwarding && !hasArrayContext && !hasGroupContext && !hasOverrides) {
|
|
8617
8891
|
return mapperResult;
|
|
8618
8892
|
}
|
|
8619
8893
|
// Wrap in computed to apply transformations
|
|
@@ -8623,18 +8897,34 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8623
8897
|
if (hasPropsForwarding) {
|
|
8624
8898
|
inputs = mergeForwardedPropsToMeta(inputs, propsToMeta);
|
|
8625
8899
|
}
|
|
8900
|
+
// Apply group prefix BEFORE array suffix so the resulting key reads as
|
|
8901
|
+
// `{groupPath}_{key}_{index}` (e.g. `address_street_0`) — matching the
|
|
8902
|
+
// form-value path order: group → leaf → array index.
|
|
8903
|
+
if (hasGroupContext) {
|
|
8904
|
+
// Non-null after the hasGroupContext guard.
|
|
8905
|
+
inputs = applyGroupPrefix(inputs, groupContext.groupPath());
|
|
8906
|
+
}
|
|
8626
8907
|
// Apply index suffix for array items
|
|
8627
8908
|
if (hasArrayContext) {
|
|
8628
8909
|
const index = indexSignal();
|
|
8629
8910
|
inputs = applyIndexSuffix(inputs, index);
|
|
8630
8911
|
}
|
|
8631
|
-
// Apply property overrides from the store (AFTER all static transformations)
|
|
8912
|
+
// Apply property overrides from the store (AFTER all static transformations).
|
|
8913
|
+
//
|
|
8914
|
+
// INVARIANT: the store key MUST be built from `fieldDef.key` + the group/array
|
|
8915
|
+
// ancestry (via buildPropertyOverrideKey) — NEVER from the DOM-scoped
|
|
8916
|
+
// `inputs['key']` produced by applyGroupPrefix/applyIndexSuffix above. The
|
|
8917
|
+
// collector (property-derivation-collector.ts) registers using the same
|
|
8918
|
+
// shape; if these drift, overrides silently miss their target.
|
|
8632
8919
|
if (hasOverrides) {
|
|
8633
|
-
// Build
|
|
8634
|
-
//
|
|
8920
|
+
// Build inside computed() so the index/group signals establish reactive deps.
|
|
8921
|
+
// GROUP_CONTEXT resets at array boundaries (see create-array-item-injector.ts),
|
|
8922
|
+
// so groupPath() represents ancestors inside the current array item (or
|
|
8923
|
+
// top-level groups when not in an array) — matching the collector.
|
|
8924
|
+
const groupPath = hasGroupContext ? groupContext.groupPath() : undefined;
|
|
8635
8925
|
const key = hasArrayContext
|
|
8636
|
-
? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key)
|
|
8637
|
-
: fieldDef.key;
|
|
8926
|
+
? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key, groupPath || undefined)
|
|
8927
|
+
: buildPropertyOverrideKey(undefined, undefined, fieldDef.key, groupPath || undefined);
|
|
8638
8928
|
const overrides = store.getOverrides(key)();
|
|
8639
8929
|
inputs = applyPropertyOverrides(inputs, overrides);
|
|
8640
8930
|
}
|
|
@@ -8653,6 +8943,40 @@ function createRenderReadySignal(inputs, definition) {
|
|
|
8653
8943
|
return requiredInputs.every((inputName) => currentInputs[inputName] !== undefined);
|
|
8654
8944
|
});
|
|
8655
8945
|
}
|
|
8946
|
+
/**
|
|
8947
|
+
* Reads the current hidden state from mapper inputs. Uniform across all field
|
|
8948
|
+
* types — value-bearing leaves source from the `FieldTree`'s `state.hidden()`
|
|
8949
|
+
* (where Angular Signal Forms cascades `hidden(path, …)`), and non-field outputs
|
|
8950
|
+
* (button/text/containers) source from the mapper's explicit `inputs.hidden`.
|
|
8951
|
+
*
|
|
8952
|
+
* The `@if (!field.hidden())` gate at every `*dfFieldOutlet` consumption site uses
|
|
8953
|
+
* this signal, removing hidden fields from the DOM. That's required to silence
|
|
8954
|
+
* Angular's `NG01916: Field 'X' is hidden but is being rendered` warning — and
|
|
8955
|
+
* is the prescribed pattern from the
|
|
8956
|
+
* [`hidden` API docs](https://angular.dev/api/forms/signals/hidden).
|
|
8957
|
+
*
|
|
8958
|
+
* Container internal state (e.g. an `ArrayFieldComponent`'s items + dynamic
|
|
8959
|
+
* templates + id counter) survives this destroy/recreate via the form-scoped
|
|
8960
|
+
* `ArrayItemRegistryService` keyed by array path.
|
|
8961
|
+
*/
|
|
8962
|
+
function createHiddenSignal(inputs) {
|
|
8963
|
+
return computed(() => {
|
|
8964
|
+
const currentInputs = inputs();
|
|
8965
|
+
const fieldCandidate = currentInputs['field'];
|
|
8966
|
+
// Value-bearing leaves expose `field` as a callable `FieldTree` whose invocation returns a
|
|
8967
|
+
// `FieldState` with a `hidden` accessor. `isSignal()` doesn't recognise the Proxy-wrapped
|
|
8968
|
+
// FieldTree (no SIGNAL symbol), so we duck-type the shape and call `state.hidden()` directly.
|
|
8969
|
+
if (typeof fieldCandidate === 'function') {
|
|
8970
|
+
const state = fieldCandidate();
|
|
8971
|
+
if (state && typeof state.hidden === 'function') {
|
|
8972
|
+
return state.hidden() === true;
|
|
8973
|
+
}
|
|
8974
|
+
}
|
|
8975
|
+
// Non-value fields (button, text, containers) carry an explicit `hidden` input set by
|
|
8976
|
+
// `applyHiddenLogic` based on the mapper-side evaluation.
|
|
8977
|
+
return currentInputs['hidden'] === true;
|
|
8978
|
+
});
|
|
8979
|
+
}
|
|
8656
8980
|
/**
|
|
8657
8981
|
* Resolves a single field definition to a ResolvedField using RxJS.
|
|
8658
8982
|
* Loads the component asynchronously and maps inputs in the injection context.
|
|
@@ -8689,6 +9013,7 @@ function resolveField(fieldDef, context) {
|
|
|
8689
9013
|
injector: context.injector,
|
|
8690
9014
|
inputs,
|
|
8691
9015
|
renderReady: createRenderReadySignal(inputs, definition),
|
|
9016
|
+
hidden: createHiddenSignal(inputs),
|
|
8692
9017
|
};
|
|
8693
9018
|
}), catchError((error) => {
|
|
8694
9019
|
// Only call onError if component is not destroyed to avoid accessing cleaned-up state
|
|
@@ -8725,6 +9050,7 @@ function resolveFieldSync(fieldDef, context) {
|
|
|
8725
9050
|
injector: context.injector,
|
|
8726
9051
|
inputs,
|
|
8727
9052
|
renderReady: createRenderReadySignal(inputs, definition),
|
|
9053
|
+
hidden: createHiddenSignal(inputs),
|
|
8728
9054
|
};
|
|
8729
9055
|
}
|
|
8730
9056
|
/**
|
|
@@ -8751,13 +9077,14 @@ function reconcileFields(prev, curr) {
|
|
|
8751
9077
|
}
|
|
8752
9078
|
// Same component instance + injector, but the field definition changed.
|
|
8753
9079
|
// Preserve component identity (so ngComponentOutlet keeps the same instance)
|
|
8754
|
-
// while swapping in the freshly-mapped inputs/renderReady signals
|
|
8755
|
-
// updates (e.g. select options) reach the component.
|
|
9080
|
+
// while swapping in the freshly-mapped inputs/renderReady/hidden signals
|
|
9081
|
+
// so prop updates (e.g. select options) reach the component.
|
|
8756
9082
|
return {
|
|
8757
9083
|
...existing,
|
|
8758
9084
|
fieldDef: field.fieldDef,
|
|
8759
9085
|
inputs: field.inputs,
|
|
8760
9086
|
renderReady: field.renderReady,
|
|
9087
|
+
hidden: field.hidden,
|
|
8761
9088
|
};
|
|
8762
9089
|
}
|
|
8763
9090
|
// New field, type changed, or context changed (new injector) - use new field
|
|
@@ -9075,15 +9402,32 @@ function injectFieldRegistry() {
|
|
|
9075
9402
|
* Collects all field keys, types, and validates regex patterns from a field tree
|
|
9076
9403
|
* by recursively traversing containers (page, row, group, array).
|
|
9077
9404
|
*
|
|
9078
|
-
*
|
|
9079
|
-
*
|
|
9080
|
-
*
|
|
9081
|
-
*
|
|
9405
|
+
* Keys are scoped by their group ancestors (e.g. `address.city` instead of `city`),
|
|
9406
|
+
* so the same leaf key can appear inside different groups without conflict. Page and
|
|
9407
|
+
* row containers do not contribute to the path because they flatten their children
|
|
9408
|
+
* into the parent scope. Array items are excluded entirely from key collection
|
|
9409
|
+
* because they share template keys across items by design.
|
|
9410
|
+
*
|
|
9411
|
+
* Fields whose registered `valueHandling` is anything other than `include` (e.g.
|
|
9412
|
+
* buttons with `exclude`, page/row with `flatten`) are not collected: they don't
|
|
9413
|
+
* bind to a form-value path, so duplicate-key uniqueness isn't meaningful for them.
|
|
9414
|
+
*
|
|
9415
|
+
* @param collectKeys - When false, the entire subtree skips key collection (used
|
|
9416
|
+
* when descending into an array container).
|
|
9082
9417
|
*/
|
|
9083
|
-
function collectFieldData(fields, data, collectKeys = true) {
|
|
9418
|
+
function collectFieldData(fields, data, registry, pathPrefix, collectKeys = true) {
|
|
9084
9419
|
for (const field of fields) {
|
|
9085
|
-
|
|
9086
|
-
|
|
9420
|
+
// Defensively skip only known non-include modes — unexpected/missing
|
|
9421
|
+
// valueHandling defaults back to participating in duplicate detection so a
|
|
9422
|
+
// mis-registered field type doesn't silently drop from collision checks.
|
|
9423
|
+
const valueHandling = field.type ? getFieldValueHandling(field.type, registry) : 'include';
|
|
9424
|
+
const participatesInValue = valueHandling !== 'exclude' && valueHandling !== 'flatten';
|
|
9425
|
+
if (collectKeys && field.key && participatesInValue) {
|
|
9426
|
+
const scopedKey = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;
|
|
9427
|
+
data.keys.push(scopedKey);
|
|
9428
|
+
// Project to the DOM-ID format (dots → underscores) so we can detect
|
|
9429
|
+
// collisions like top-level `'foo_bar'` vs group `'foo'` + leaf `'bar'`.
|
|
9430
|
+
data.domIds.push(scopedKey.replace(/\./g, '_'));
|
|
9087
9431
|
}
|
|
9088
9432
|
if (field.type) {
|
|
9089
9433
|
data.types.add(field.type);
|
|
@@ -9103,13 +9447,16 @@ function collectFieldData(fields, data, collectKeys = true) {
|
|
|
9103
9447
|
// Array item fields share template keys across items by design — stop collecting keys
|
|
9104
9448
|
// once inside an array. Types and regex patterns are still validated.
|
|
9105
9449
|
const childCollectKeys = collectKeys && field.type !== 'array';
|
|
9450
|
+
// Only group containers extend the scoping path — page/row flatten into the parent
|
|
9451
|
+
// scope, and array items use dynamic indices that aren't statically knowable.
|
|
9452
|
+
const childPrefix = isGroupField(field) && field.key ? (pathPrefix ? `${pathPrefix}.${field.key}` : field.key) : pathPrefix;
|
|
9106
9453
|
const children = normalizeFieldsArray(field.fields);
|
|
9107
9454
|
for (const child of children) {
|
|
9108
9455
|
if (Array.isArray(child)) {
|
|
9109
|
-
collectFieldData(child, data, childCollectKeys);
|
|
9456
|
+
collectFieldData(child, data, registry, childPrefix, childCollectKeys);
|
|
9110
9457
|
}
|
|
9111
9458
|
else {
|
|
9112
|
-
collectFieldData([child], data, childCollectKeys);
|
|
9459
|
+
collectFieldData([child], data, registry, childPrefix, childCollectKeys);
|
|
9113
9460
|
}
|
|
9114
9461
|
}
|
|
9115
9462
|
}
|
|
@@ -9145,7 +9492,38 @@ function validateNoDuplicateKeys(allKeys) {
|
|
|
9145
9492
|
const duplicateList = Array.from(duplicates)
|
|
9146
9493
|
.map((k) => `'${k}'`)
|
|
9147
9494
|
.join(', ');
|
|
9148
|
-
throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}.
|
|
9495
|
+
throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).`);
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
/**
|
|
9499
|
+
* Validates that the underscore-projected DOM-ID namespace has no collisions.
|
|
9500
|
+
*
|
|
9501
|
+
* DOM IDs are built by underscore-joining the group ancestor path with the
|
|
9502
|
+
* leaf key (e.g. `address_street`). Two scoped paths can land on the same
|
|
9503
|
+
* DOM ID even when their form-value paths differ — e.g. a top-level key
|
|
9504
|
+
* `'foo_bar'` and a leaf `'bar'` inside group `'foo'` both render as
|
|
9505
|
+
* `id="foo_bar"`. The form-value-path check accepts this (paths are
|
|
9506
|
+
* `'foo_bar'` vs `'foo.bar'` — distinct) but the rendered DOM would have
|
|
9507
|
+
* duplicate IDs, breaking aria associations and CSS selectors.
|
|
9508
|
+
*
|
|
9509
|
+
* @throws {DynamicFormError} When two scoped paths project to the same DOM ID
|
|
9510
|
+
*/
|
|
9511
|
+
function validateNoDomIdCollisions(domIds) {
|
|
9512
|
+
const seen = new Set();
|
|
9513
|
+
const duplicates = new Set();
|
|
9514
|
+
for (const id of domIds) {
|
|
9515
|
+
if (seen.has(id)) {
|
|
9516
|
+
duplicates.add(id);
|
|
9517
|
+
}
|
|
9518
|
+
seen.add(id);
|
|
9519
|
+
}
|
|
9520
|
+
if (duplicates.size > 0) {
|
|
9521
|
+
const duplicateList = Array.from(duplicates)
|
|
9522
|
+
.map((k) => `'${k}'`)
|
|
9523
|
+
.join(', ');
|
|
9524
|
+
throw new DynamicFormError(`DOM id collision detected for: ${duplicateList}. Group-nested keys are joined with '_' to form DOM ids ` +
|
|
9525
|
+
`(e.g. group 'foo' + leaf 'bar' renders as id='foo_bar'). A separate top-level key like 'foo_bar' or a different ` +
|
|
9526
|
+
`group/leaf pair that underscores to the same string will produce duplicate ids. Rename one of the colliding keys.`);
|
|
9149
9527
|
}
|
|
9150
9528
|
}
|
|
9151
9529
|
/**
|
|
@@ -9183,11 +9561,13 @@ function validateFieldTypesRegistered(allTypes, registry, logger) {
|
|
|
9183
9561
|
function validateFormConfig(fields, registry, logger) {
|
|
9184
9562
|
const data = {
|
|
9185
9563
|
keys: [],
|
|
9564
|
+
domIds: [],
|
|
9186
9565
|
types: new Set(),
|
|
9187
9566
|
regexErrors: [],
|
|
9188
9567
|
};
|
|
9189
|
-
collectFieldData(fields, data);
|
|
9568
|
+
collectFieldData(fields, data, registry, '');
|
|
9190
9569
|
validateNoDuplicateKeys(data.keys);
|
|
9570
|
+
validateNoDomIdCollisions(data.domIds);
|
|
9191
9571
|
validateFieldTypesRegistered(data.types, registry, logger);
|
|
9192
9572
|
if (data.regexErrors.length > 0) {
|
|
9193
9573
|
throw new DynamicFormError(data.regexErrors.length === 1
|
|
@@ -9196,6 +9576,272 @@ function validateFormConfig(fields, registry, logger) {
|
|
|
9196
9576
|
}
|
|
9197
9577
|
}
|
|
9198
9578
|
|
|
9579
|
+
/**
|
|
9580
|
+
* Walk a field's addons; return the survivors plus a list of warnings for
|
|
9581
|
+
* anything dropped.
|
|
9582
|
+
*
|
|
9583
|
+
* Pure: receives the registries as arguments (so the function is reusable
|
|
9584
|
+
* outside Angular's DI — tests, server-side validation, etc.).
|
|
9585
|
+
*
|
|
9586
|
+
* @param field The field to validate.
|
|
9587
|
+
* @param fieldRegistry Snapshot of `FIELD_REGISTRY` map.
|
|
9588
|
+
* @param kindRegistry Snapshot of `ADDON_KIND_REGISTRY` map.
|
|
9589
|
+
* @param source Whether the config originated from JSON (lenient drop
|
|
9590
|
+
* of code-only kinds) or inline TypeScript.
|
|
9591
|
+
*/
|
|
9592
|
+
function validateFieldAddons(field, fieldRegistry, kindRegistry, source) {
|
|
9593
|
+
const warnings = [];
|
|
9594
|
+
const incoming = field.addons;
|
|
9595
|
+
if (!incoming?.length) {
|
|
9596
|
+
return { addons: [], warnings };
|
|
9597
|
+
}
|
|
9598
|
+
const fieldType = field.type;
|
|
9599
|
+
const fieldDef = fieldRegistry.get(fieldType);
|
|
9600
|
+
if (!fieldDef) {
|
|
9601
|
+
warnings.push({ type: 'unknown-field-type', fieldKey: field.key, fieldType });
|
|
9602
|
+
return { addons: [], warnings };
|
|
9603
|
+
}
|
|
9604
|
+
const addonSupport = fieldDef.addons;
|
|
9605
|
+
if (!addonSupport) {
|
|
9606
|
+
warnings.push({ type: 'field-type-no-addon-support', fieldKey: field.key, fieldType });
|
|
9607
|
+
return { addons: [], warnings };
|
|
9608
|
+
}
|
|
9609
|
+
const allowedSlots = addonSupport.slots;
|
|
9610
|
+
const allowedKinds = addonSupport.allowedKinds;
|
|
9611
|
+
const registeredKinds = Array.from(kindRegistry.keys());
|
|
9612
|
+
const survivors = [];
|
|
9613
|
+
for (const addon of incoming) {
|
|
9614
|
+
// Slot allowed?
|
|
9615
|
+
if (!allowedSlots.includes(addon.slot)) {
|
|
9616
|
+
warnings.push({
|
|
9617
|
+
type: 'unknown-slot',
|
|
9618
|
+
fieldKey: field.key,
|
|
9619
|
+
fieldType,
|
|
9620
|
+
slot: String(addon.slot),
|
|
9621
|
+
allowedSlots,
|
|
9622
|
+
});
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
// Kind registered? `AnyAddon.kind` is already a string-literal union via
|
|
9626
|
+
// DynamicFormAddonRegistry; no need to cast.
|
|
9627
|
+
const kindDef = kindRegistry.get(addon.kind);
|
|
9628
|
+
if (!kindDef) {
|
|
9629
|
+
warnings.push({
|
|
9630
|
+
type: 'unknown-kind',
|
|
9631
|
+
fieldKey: field.key,
|
|
9632
|
+
kind: addon.kind,
|
|
9633
|
+
registeredKinds,
|
|
9634
|
+
});
|
|
9635
|
+
continue;
|
|
9636
|
+
}
|
|
9637
|
+
// Kind whitelisted (if `allowedKinds` was set)?
|
|
9638
|
+
if (allowedKinds && !allowedKinds.includes(kindDef.kind)) {
|
|
9639
|
+
warnings.push({
|
|
9640
|
+
type: 'kind-not-allowed',
|
|
9641
|
+
fieldKey: field.key,
|
|
9642
|
+
fieldType,
|
|
9643
|
+
kind: kindDef.kind,
|
|
9644
|
+
allowedKinds,
|
|
9645
|
+
});
|
|
9646
|
+
continue;
|
|
9647
|
+
}
|
|
9648
|
+
// Code-only kind dropped from JSON-source configs. A kind opts out of
|
|
9649
|
+
// JSON-safety via `AddonKindDefinition.jsonSafe: false` (defaults to true).
|
|
9650
|
+
if (source === 'json' && kindDef.jsonSafe === false) {
|
|
9651
|
+
warnings.push({
|
|
9652
|
+
type: 'code-only-kind-in-json',
|
|
9653
|
+
fieldKey: field.key,
|
|
9654
|
+
kind: kindDef.kind,
|
|
9655
|
+
});
|
|
9656
|
+
continue;
|
|
9657
|
+
}
|
|
9658
|
+
// Multi-set XOR + JSON-source function stripping. Both runs operate on
|
|
9659
|
+
// a working copy via `ensureCopy()` so the original config isn't
|
|
9660
|
+
// mutated — the survivor pushed at the bottom is either the original
|
|
9661
|
+
// addon (no strips) or the sanitized copy.
|
|
9662
|
+
let sanitized = addon;
|
|
9663
|
+
let stripped = false;
|
|
9664
|
+
const ensureCopy = () => {
|
|
9665
|
+
if (!stripped)
|
|
9666
|
+
sanitized = { ...sanitized };
|
|
9667
|
+
stripped = true;
|
|
9668
|
+
};
|
|
9669
|
+
// Snapshot which click variants the original config declared BEFORE
|
|
9670
|
+
// any stripping. Computing variants AFTER function-strip would
|
|
9671
|
+
// underreport the multi-variant warning (`action` would silently
|
|
9672
|
+
// vanish from the reported list).
|
|
9673
|
+
const declaredVariants = ['preset', 'actionRef', 'action'].filter((k) => sanitized[k] !== undefined);
|
|
9674
|
+
// JSON-source function strip — inline `action`/`hidden`/`disabled`/
|
|
9675
|
+
// `loading` functions can't round-trip through serialization, so they
|
|
9676
|
+
// get dropped here with a warning. Inline-source configs keep them.
|
|
9677
|
+
if (source === 'json') {
|
|
9678
|
+
const dropReactiveFn = (key) => {
|
|
9679
|
+
if (typeof sanitized[key] === 'function') {
|
|
9680
|
+
ensureCopy();
|
|
9681
|
+
delete sanitized[key];
|
|
9682
|
+
warnings.push({
|
|
9683
|
+
type: 'code-only-action-in-json',
|
|
9684
|
+
fieldKey: field.key,
|
|
9685
|
+
reason: `function '${key}' on kind '${kindDef.kind}' (use a signal/observable in code, or omit in JSON)`,
|
|
9686
|
+
});
|
|
9687
|
+
}
|
|
9688
|
+
};
|
|
9689
|
+
if (typeof sanitized.action === 'function') {
|
|
9690
|
+
ensureCopy();
|
|
9691
|
+
delete sanitized.action;
|
|
9692
|
+
warnings.push({
|
|
9693
|
+
type: 'code-only-action-in-json',
|
|
9694
|
+
fieldKey: field.key,
|
|
9695
|
+
reason: `inline 'action' function on kind '${kindDef.kind}'`,
|
|
9696
|
+
});
|
|
9697
|
+
}
|
|
9698
|
+
dropReactiveFn('hidden');
|
|
9699
|
+
dropReactiveFn('disabled');
|
|
9700
|
+
dropReactiveFn('loading');
|
|
9701
|
+
}
|
|
9702
|
+
// Multi-set XOR — applies to BOTH inline and JSON sources. TypeScript
|
|
9703
|
+
// discriminated unions already reject multi-set at compile time, but
|
|
9704
|
+
// configs that bypass type-checking (loose `any`, runtime composition,
|
|
9705
|
+
// JSON parsing) still need defence-in-depth. Keep the first variant,
|
|
9706
|
+
// strip the rest; mirrors the runtime precedence
|
|
9707
|
+
// (preset > actionRef > action) so the chosen handler matches what
|
|
9708
|
+
// the dispatcher would have picked.
|
|
9709
|
+
if (declaredVariants.length > 1) {
|
|
9710
|
+
ensureCopy();
|
|
9711
|
+
for (const k of declaredVariants.slice(1))
|
|
9712
|
+
delete sanitized[k];
|
|
9713
|
+
warnings.push({
|
|
9714
|
+
type: 'code-only-action-in-json',
|
|
9715
|
+
fieldKey: field.key,
|
|
9716
|
+
reason: `multiple click variants on kind '${kindDef.kind}' (${declaredVariants.join(', ')}); kept '${declaredVariants[0]}'`,
|
|
9717
|
+
});
|
|
9718
|
+
}
|
|
9719
|
+
if (stripped) {
|
|
9720
|
+
survivors.push(sanitized);
|
|
9721
|
+
continue;
|
|
9722
|
+
}
|
|
9723
|
+
// Per-kind shape validator (throws on violation).
|
|
9724
|
+
if (kindDef.validate) {
|
|
9725
|
+
try {
|
|
9726
|
+
kindDef.validate(addon, field.key);
|
|
9727
|
+
}
|
|
9728
|
+
catch (error) {
|
|
9729
|
+
warnings.push({
|
|
9730
|
+
type: 'shape-violation',
|
|
9731
|
+
fieldKey: field.key,
|
|
9732
|
+
kind: kindDef.kind,
|
|
9733
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
9734
|
+
});
|
|
9735
|
+
continue;
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
survivors.push(addon);
|
|
9739
|
+
}
|
|
9740
|
+
return { addons: survivors, warnings };
|
|
9741
|
+
}
|
|
9742
|
+
/**
|
|
9743
|
+
* Recursively walk a field tree, validating each field's addons. Returns a
|
|
9744
|
+
* sanitized copy of the input field array plus accumulated warnings.
|
|
9745
|
+
*/
|
|
9746
|
+
function walkAndValidateAddons(fields, fieldRegistry, kindRegistry, source) {
|
|
9747
|
+
const allWarnings = [];
|
|
9748
|
+
const sanitized = fields.map((field) => {
|
|
9749
|
+
let next = field;
|
|
9750
|
+
if (field.addons?.length) {
|
|
9751
|
+
const { addons, warnings } = validateFieldAddons(field, fieldRegistry, kindRegistry, source);
|
|
9752
|
+
allWarnings.push(...warnings);
|
|
9753
|
+
next = { ...next, addons };
|
|
9754
|
+
}
|
|
9755
|
+
// Recurse into containers — every container field type carries its
|
|
9756
|
+
// children on a `fields` array.
|
|
9757
|
+
const children = next.fields;
|
|
9758
|
+
if (Array.isArray(children)) {
|
|
9759
|
+
const { fields: visited, warnings } = walkAndValidateAddons(children, fieldRegistry, kindRegistry, source);
|
|
9760
|
+
allWarnings.push(...warnings);
|
|
9761
|
+
next = { ...next, fields: visited };
|
|
9762
|
+
}
|
|
9763
|
+
return next;
|
|
9764
|
+
});
|
|
9765
|
+
return { fields: sanitized, warnings: allWarnings };
|
|
9766
|
+
}
|
|
9767
|
+
|
|
9768
|
+
/**
|
|
9769
|
+
* Stable structural fingerprint for an `AddonWarning`, used by the
|
|
9770
|
+
* FormStateManager dedup cache. Includes every distinguishing field
|
|
9771
|
+
* (`type`, `fieldKey`, `kind`/`slot`/`reason` as relevant) so warnings
|
|
9772
|
+
* for different fields or different kinds never collapse to one log
|
|
9773
|
+
* line — independent of the human-readable format which may change.
|
|
9774
|
+
*/
|
|
9775
|
+
function addonWarningKey(w) {
|
|
9776
|
+
switch (w.type) {
|
|
9777
|
+
case 'unknown-field-type':
|
|
9778
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
9779
|
+
case 'field-type-no-addon-support':
|
|
9780
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
9781
|
+
case 'unknown-slot':
|
|
9782
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.slot}`;
|
|
9783
|
+
case 'unknown-kind':
|
|
9784
|
+
return `${w.type}|${w.fieldKey}|${w.kind}`;
|
|
9785
|
+
case 'kind-not-allowed':
|
|
9786
|
+
return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.kind}`;
|
|
9787
|
+
case 'shape-violation':
|
|
9788
|
+
return `${w.type}|${w.fieldKey}|${w.kind}|${w.reason}`;
|
|
9789
|
+
case 'code-only-kind-in-json':
|
|
9790
|
+
return `${w.type}|${w.fieldKey}|${w.kind}`;
|
|
9791
|
+
case 'code-only-action-in-json':
|
|
9792
|
+
return `${w.type}|${w.fieldKey}|${w.reason}`;
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* Render an `AddonWarning` to a developer-friendly message.
|
|
9797
|
+
*
|
|
9798
|
+
* Returns the bare message — the `[Dynamic Forms]` prefix is added by the
|
|
9799
|
+
* library's logger (`ConsoleLogger`) and by `DynamicFormError`. Pass the
|
|
9800
|
+
* result of this function directly into `logger.warn(...)` to get the
|
|
9801
|
+
* standard prefixed line; use `logAddonWarnings()` for direct
|
|
9802
|
+
* `console.warn` output (which prefixes manually).
|
|
9803
|
+
*
|
|
9804
|
+
* Messages are designed to be actionable: they enumerate the relevant set
|
|
9805
|
+
* (registered kinds, allowed slots) and suggest a likely fix.
|
|
9806
|
+
*/
|
|
9807
|
+
function formatAddonWarning(w) {
|
|
9808
|
+
switch (w.type) {
|
|
9809
|
+
case 'unknown-field-type':
|
|
9810
|
+
return `Field '${w.fieldKey}': unknown field type '${w.fieldType}'. Addons dropped.`;
|
|
9811
|
+
case 'field-type-no-addon-support':
|
|
9812
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): this field type does not declare addon support. Addons dropped. ` +
|
|
9813
|
+
`If this is incorrect, add 'addons: { slots: [...] }' to the field type registration.`);
|
|
9814
|
+
case 'unknown-slot':
|
|
9815
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): slot '${w.slot}' is not supported. ` +
|
|
9816
|
+
`Allowed: ${w.allowedSlots.join(', ')}. Addon dropped.`);
|
|
9817
|
+
case 'unknown-kind':
|
|
9818
|
+
return (`Field '${w.fieldKey}': addon kind '${w.kind}' is not registered. ` +
|
|
9819
|
+
`Did you forget withCustomAddon({ kind: '${w.kind}', ... })? ` +
|
|
9820
|
+
`Currently registered: ${w.registeredKinds.join(', ') || '(none)'}. Addon dropped.`);
|
|
9821
|
+
case 'kind-not-allowed':
|
|
9822
|
+
return (`Field '${w.fieldKey}' (type: '${w.fieldType}'): addon kind '${w.kind}' is not in the allowed list. ` +
|
|
9823
|
+
`Allowed: ${w.allowedKinds.join(', ')}. Addon dropped.`);
|
|
9824
|
+
case 'shape-violation':
|
|
9825
|
+
return `Field '${w.fieldKey}': addon kind '${w.kind}' shape invalid: ${w.reason}. Addon dropped.`;
|
|
9826
|
+
case 'code-only-kind-in-json':
|
|
9827
|
+
return `Field '${w.fieldKey}': addon kind '${w.kind}' is code-only and cannot be used with JSON-derived configs. Addon dropped.`;
|
|
9828
|
+
case 'code-only-action-in-json':
|
|
9829
|
+
return `Field '${w.fieldKey}': ${w.reason}. Use 'preset' or 'actionRef' instead. Addon dropped.`;
|
|
9830
|
+
}
|
|
9831
|
+
}
|
|
9832
|
+
|
|
9833
|
+
/**
|
|
9834
|
+
* Global registry of addon kinds.
|
|
9835
|
+
*
|
|
9836
|
+
* Populated by `withCustomAddon(...)` features and adapter feature helpers
|
|
9837
|
+
* (e.g., `withPrimeNGAddons()`). Read by `<df-addon-slot>` to resolve a
|
|
9838
|
+
* kind discriminant to its renderer component.
|
|
9839
|
+
*/
|
|
9840
|
+
const ADDON_KIND_REGISTRY = new InjectionToken('ADDON_KIND_REGISTRY', {
|
|
9841
|
+
providedIn: 'root',
|
|
9842
|
+
factory: () => new Map(),
|
|
9843
|
+
});
|
|
9844
|
+
|
|
9199
9845
|
/**
|
|
9200
9846
|
* Injection token for global value exclusion defaults.
|
|
9201
9847
|
*
|
|
@@ -9889,6 +10535,12 @@ class FormStateManager {
|
|
|
9889
10535
|
eventBus = inject(EventBus);
|
|
9890
10536
|
functionRegistry = inject(FunctionRegistryService);
|
|
9891
10537
|
schemaRegistry = inject(SchemaRegistryService);
|
|
10538
|
+
// Optional so unit-test setups that drive FormStateManager directly without provideDynamicFormDI()
|
|
10539
|
+
// (e.g. mocked RootFormRegistryService specs) don't have to manually wire the registry. Real
|
|
10540
|
+
// DynamicForm usage always provides it via coreProviders; if you hit the dev warning below in
|
|
10541
|
+
// your own setup, you're using DynamicForm without provideDynamicFormDI() — fix the providers
|
|
10542
|
+
// chain rather than ignoring it.
|
|
10543
|
+
arrayItemRegistry = inject(ArrayItemRegistryService, { optional: true });
|
|
9892
10544
|
/** Host component dependencies (config, formOptions, value). */
|
|
9893
10545
|
deps = (() => {
|
|
9894
10546
|
const raw = inject(FORM_STATE_DEPS);
|
|
@@ -9897,12 +10549,28 @@ class FormStateManager {
|
|
|
9897
10549
|
'Ensure DynamicForm assigns its input signals to FORM_STATE_DEPS.');
|
|
9898
10550
|
}
|
|
9899
10551
|
// Safe cast: DynamicForm populates FormStateDeps with its concrete signals.
|
|
10552
|
+
// `source` is optional — `DynamicForm` always populates it, but other
|
|
10553
|
+
// callers of `FormStateManager` (e.g., specs) may omit it.
|
|
9900
10554
|
return raw;
|
|
9901
10555
|
})();
|
|
9902
10556
|
/** Global value exclusion defaults. */
|
|
9903
10557
|
valueExclusionDefaults = inject(VALUE_EXCLUSION_DEFAULTS);
|
|
9904
10558
|
/** Field registry for loading components. */
|
|
9905
10559
|
fieldRegistry = injectFieldRegistry();
|
|
10560
|
+
/** Addon-kind registry — feeds the addon validator at config-setup time. */
|
|
10561
|
+
addonKindRegistry = inject(ADDON_KIND_REGISTRY);
|
|
10562
|
+
/**
|
|
10563
|
+
* Fingerprints of addon warnings emitted by the last config setup.
|
|
10564
|
+
* Used to skip re-logging warnings that already fired for the previous
|
|
10565
|
+
* config — avoids spam when the form swaps config repeatedly but the
|
|
10566
|
+
* addon issues haven't changed.
|
|
10567
|
+
*
|
|
10568
|
+
* Scoping: per-FormStateManager (i.e., per `<df-dynamic-form>` component
|
|
10569
|
+
* instance). Replaced wholesale on every config setup — bounded by the
|
|
10570
|
+
* current config's warning count, never grows unbounded across swaps.
|
|
10571
|
+
* A warning that disappears and later returns re-logs as expected.
|
|
10572
|
+
*/
|
|
10573
|
+
lastAddonWarningKeys = new Set();
|
|
9906
10574
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9907
10575
|
// State Machine
|
|
9908
10576
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -10515,12 +11183,24 @@ class FormStateManager {
|
|
|
10515
11183
|
}
|
|
10516
11184
|
});
|
|
10517
11185
|
// Schema change resets the saved store + transition tracker so values from a stale schema
|
|
10518
|
-
// can't leak into the new one.
|
|
11186
|
+
// can't leak into the new one. Also drops every per-array slot in the registry so a fresh
|
|
11187
|
+
// schema starts with an empty template/itemOrder/id-counter state.
|
|
11188
|
+
let warnedAboutMissingArrayRegistry = false;
|
|
10519
11189
|
explicitEffect([this.formSetup], () => {
|
|
10520
11190
|
this.prevFieldStateSnapshot.set({});
|
|
10521
11191
|
if (Object.keys(this.excludedValueStore()).length > 0) {
|
|
10522
11192
|
this.excludedValueStore.set({});
|
|
10523
11193
|
}
|
|
11194
|
+
if (this.arrayItemRegistry) {
|
|
11195
|
+
this.arrayItemRegistry.clearAll();
|
|
11196
|
+
}
|
|
11197
|
+
else if (DEV_MODE && !warnedAboutMissingArrayRegistry) {
|
|
11198
|
+
warnedAboutMissingArrayRegistry = true;
|
|
11199
|
+
this.logger.warn('ArrayItemRegistryService was not provided to FormStateManager. ' +
|
|
11200
|
+
'Array fields will lose dynamically-added item templates across hide/show cycles. ' +
|
|
11201
|
+
'This usually means DynamicForm was bootstrapped without provideDynamicFormDI() — ' +
|
|
11202
|
+
'check your providers chain.');
|
|
11203
|
+
}
|
|
10524
11204
|
});
|
|
10525
11205
|
// Outward sync uses boundFormValue (honors only explicit excludeValueIf* opt-ins) so
|
|
10526
11206
|
// global defaults that target submission output don't reshape the host's bound model.
|
|
@@ -10578,11 +11258,35 @@ class FormStateManager {
|
|
|
10578
11258
|
}
|
|
10579
11259
|
createFormSetupFromConfig(fields, mode, registry) {
|
|
10580
11260
|
const normalizedFields = normalizeSimplifiedArrays(fields);
|
|
10581
|
-
//
|
|
10582
|
-
// into full ArrayField.fields and
|
|
11261
|
+
// Two validation passes run after normalization so simplified array templates
|
|
11262
|
+
// are already expanded into full ArrayField.fields and reachable during traversal:
|
|
11263
|
+
// 1. Structural — duplicate keys, unknown field types, invalid regex patterns.
|
|
11264
|
+
// 2. Addon pass below — strips invalid/unsupported addons with lenient warnings.
|
|
10583
11265
|
validateFormConfig(normalizedFields, registry, this.logger);
|
|
10584
|
-
|
|
10585
|
-
|
|
11266
|
+
// Addon pass — strip invalid / unsupported addon entries and log a warning
|
|
11267
|
+
// for each one dropped. Lenient by design: the form keeps rendering even
|
|
11268
|
+
// when a JSON-source config ships an addon the FE doesn't understand.
|
|
11269
|
+
//
|
|
11270
|
+
// Warnings are deduped across consecutive config setups: a warning that
|
|
11271
|
+
// already fired for the previous config is skipped, so swapping config
|
|
11272
|
+
// back and forth doesn't spam the console.
|
|
11273
|
+
const source = this.deps.source?.() ?? 'inline';
|
|
11274
|
+
const { fields: sanitizedFields, warnings: addonWarnings } = walkAndValidateAddons(normalizedFields, registry, this.addonKindRegistry, source);
|
|
11275
|
+
const nextKeys = new Set();
|
|
11276
|
+
for (const w of addonWarnings) {
|
|
11277
|
+
// Dedup uses a structural fingerprint so different fields with the
|
|
11278
|
+
// same warning category never collide; the rendered message is what
|
|
11279
|
+
// we log.
|
|
11280
|
+
const key = addonWarningKey(w);
|
|
11281
|
+
nextKeys.add(key);
|
|
11282
|
+
if (!this.lastAddonWarningKeys.has(key)) {
|
|
11283
|
+
this.logger.warn(formatAddonWarning(w));
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
this.lastAddonWarningKeys = nextKeys;
|
|
11287
|
+
const validatedFields = sanitizedFields;
|
|
11288
|
+
const flattenedFields = this.fieldProcessors.memoizedFlattenFields(validatedFields, registry);
|
|
11289
|
+
const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(validatedFields, registry);
|
|
10586
11290
|
const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
|
|
10587
11291
|
const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
|
|
10588
11292
|
const fieldsToRender = mode === 'paged' ? [] : flattenedFieldsForRendering;
|
|
@@ -10610,10 +11314,10 @@ class FormStateManager {
|
|
|
10610
11314
|
ngOnDestroy() {
|
|
10611
11315
|
this.machine.dispatch({ type: Action.Destroy });
|
|
10612
11316
|
}
|
|
10613
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
10614
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
11317
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
11318
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager });
|
|
10615
11319
|
}
|
|
10616
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
11320
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormStateManager, decorators: [{
|
|
10617
11321
|
type: Injectable
|
|
10618
11322
|
}], ctorParameters: () => [] });
|
|
10619
11323
|
|
|
@@ -11032,15 +11736,18 @@ function computeValueFromEntry(entry, evalContext, options) {
|
|
|
11032
11736
|
if (entry.expression) {
|
|
11033
11737
|
return ExpressionParser.evaluate(entry.expression, evalContext);
|
|
11034
11738
|
}
|
|
11035
|
-
if (entry.functionName) {
|
|
11036
|
-
|
|
11739
|
+
if (entry.fn || entry.functionName) {
|
|
11740
|
+
if (entry.fn && entry.functionName) {
|
|
11741
|
+
evalContext.logger.warn(`Both "fn" and "functionName" are set on derivation for '${options.subject}'. Inline "fn" takes precedence; "functionName" is ignored.`);
|
|
11742
|
+
}
|
|
11743
|
+
const fn = entry.fn ?? (entry.functionName ? options.derivationFunctions?.[entry.functionName] : undefined);
|
|
11037
11744
|
if (!fn) {
|
|
11038
11745
|
const kind = options.functionKind ?? 'Derivation function';
|
|
11039
11746
|
throw new DynamicFormError(`${kind} '${entry.functionName}' not found in customFnConfig.derivations`);
|
|
11040
11747
|
}
|
|
11041
11748
|
return fn(evalContext);
|
|
11042
11749
|
}
|
|
11043
|
-
throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', or '
|
|
11750
|
+
throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', 'functionName', or 'fn'.`);
|
|
11044
11751
|
}
|
|
11045
11752
|
|
|
11046
11753
|
/**
|
|
@@ -11443,7 +12150,7 @@ function getEntriesForChangedFields$1(entries, changedFields) {
|
|
|
11443
12150
|
function tryApplyDerivation(entry, context, chainContext) {
|
|
11444
12151
|
const { derivationLogger } = context;
|
|
11445
12152
|
// HTTP and async entries are processed asynchronously in their own streams — skip in sync loop
|
|
11446
|
-
if (entry.http || entry.asyncFunctionName) {
|
|
12153
|
+
if (entry.http || entry.asyncFunctionName || entry.asyncFn) {
|
|
11447
12154
|
return { applied: false, fieldKey: entry.fieldKey };
|
|
11448
12155
|
}
|
|
11449
12156
|
// Check if this is an array field derivation (has '$' placeholder)
|
|
@@ -11750,9 +12457,11 @@ function formatDerivationError(entry, phase, message) {
|
|
|
11750
12457
|
? `expression: "${entry.expression.substring(0, 50)}${entry.expression.length > 50 ? '...' : ''}"`
|
|
11751
12458
|
: entry.functionName
|
|
11752
12459
|
? `function: "${entry.functionName}"`
|
|
11753
|
-
: entry.
|
|
11754
|
-
? '
|
|
11755
|
-
:
|
|
12460
|
+
: entry.fn
|
|
12461
|
+
? 'inline fn'
|
|
12462
|
+
: entry.value !== undefined
|
|
12463
|
+
? 'static value'
|
|
12464
|
+
: 'unknown source';
|
|
11756
12465
|
return `${ERROR_PREFIX$1} Failed to ${phase} derivation (field: ${entry.fieldKey}, ${source}): ${message}`;
|
|
11757
12466
|
}
|
|
11758
12467
|
/**
|
|
@@ -11821,7 +12530,7 @@ function shouldSkipForUserOverride(entry, resolvedFieldKey, context, chainContex
|
|
|
11821
12530
|
function applyDerivationsForTrigger(collection, trigger, context, changedFields) {
|
|
11822
12531
|
// Filter entries by trigger type, excluding HTTP and async entries (processed in async streams)
|
|
11823
12532
|
const filteredEntries = collection.entries.filter((entry) => {
|
|
11824
|
-
if (entry.http || entry.asyncFunctionName)
|
|
12533
|
+
if (entry.http || entry.asyncFunctionName || entry.asyncFn)
|
|
11825
12534
|
return false;
|
|
11826
12535
|
if (trigger === 'onChange') {
|
|
11827
12536
|
return !entry.trigger || entry.trigger === 'onChange';
|
|
@@ -11990,7 +12699,7 @@ function topologicalSort(entries) {
|
|
|
11990
12699
|
* Combines:
|
|
11991
12700
|
* - Explicit `dependsOn` (when provided, it takes precedence over auto-detection)
|
|
11992
12701
|
* - Expression-detected dependencies (only used when `dependsOn` is empty)
|
|
11993
|
-
* - `'*'` wildcard for `functionName` (only when `dependsOn` is empty)
|
|
12702
|
+
* - `'*'` wildcard for `functionName` / inline `fn` (only when `dependsOn` is empty)
|
|
11994
12703
|
* - Condition expression dependencies (always included)
|
|
11995
12704
|
*
|
|
11996
12705
|
* @internal
|
|
@@ -12004,7 +12713,9 @@ function extractDependenciesFromConfig(config) {
|
|
|
12004
12713
|
if (config.expression) {
|
|
12005
12714
|
extractStringDependencies(config.expression).forEach((dep) => deps.add(dep));
|
|
12006
12715
|
}
|
|
12007
|
-
|
|
12716
|
+
// Inline `fn` mirrors `functionName` semantics: without explicit dependsOn,
|
|
12717
|
+
// assume the function may depend on anything in the form.
|
|
12718
|
+
if (config.functionName || config.fn) {
|
|
12008
12719
|
deps.add('*');
|
|
12009
12720
|
}
|
|
12010
12721
|
}
|
|
@@ -12014,6 +12725,32 @@ function extractDependenciesFromConfig(config) {
|
|
|
12014
12725
|
return Array.from(deps);
|
|
12015
12726
|
}
|
|
12016
12727
|
|
|
12728
|
+
/**
|
|
12729
|
+
* Reads the optional inline `fn` property off a derivation config.
|
|
12730
|
+
*
|
|
12731
|
+
* `fn` only appears on the function-mode branch of the discriminated union, so
|
|
12732
|
+
* call sites that need to copy it through unconditionally must widen via cast.
|
|
12733
|
+
* This helper centralises the cast and keeps it read-only — no widening, no
|
|
12734
|
+
* mutation. Returns `undefined` for any non-function-mode variant.
|
|
12735
|
+
*
|
|
12736
|
+
* @internal
|
|
12737
|
+
*/
|
|
12738
|
+
function extractInlineFn(config) {
|
|
12739
|
+
return config.fn;
|
|
12740
|
+
}
|
|
12741
|
+
/**
|
|
12742
|
+
* Reads the optional inline `asyncFn` property off a derivation config.
|
|
12743
|
+
*
|
|
12744
|
+
* Same rationale as {@link extractInlineFn}: `asyncFn` only appears on the
|
|
12745
|
+
* `source: 'asyncFunction'` branch, so collectors that copy it through need a
|
|
12746
|
+
* cast. Read-only access, no widening.
|
|
12747
|
+
*
|
|
12748
|
+
* @internal
|
|
12749
|
+
*/
|
|
12750
|
+
function extractInlineAsyncFn(config) {
|
|
12751
|
+
return config.asyncFn;
|
|
12752
|
+
}
|
|
12753
|
+
|
|
12017
12754
|
/**
|
|
12018
12755
|
* Recursively walks a field-definition tree, invoking `visitor` for each field.
|
|
12019
12756
|
*
|
|
@@ -12356,9 +13093,11 @@ function createLogicEntry(fieldKey, config, context) {
|
|
|
12356
13093
|
value: config.value,
|
|
12357
13094
|
expression: config.expression,
|
|
12358
13095
|
functionName: config.functionName,
|
|
13096
|
+
fn: extractInlineFn(config),
|
|
12359
13097
|
http: config.http,
|
|
12360
13098
|
responseExpression: config.responseExpression,
|
|
12361
13099
|
asyncFunctionName: config.asyncFunctionName,
|
|
13100
|
+
asyncFn: extractInlineAsyncFn(config),
|
|
12362
13101
|
trigger,
|
|
12363
13102
|
debounceMs,
|
|
12364
13103
|
isShorthand: false,
|
|
@@ -12907,9 +13646,16 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
12907
13646
|
`Array async derivations will be supported in a future release.`);
|
|
12908
13647
|
return EMPTY;
|
|
12909
13648
|
}
|
|
12910
|
-
if (!entry.asyncFunctionName) {
|
|
13649
|
+
if (!entry.asyncFunctionName && !entry.asyncFn) {
|
|
13650
|
+
context.logger.warn(`${LOG_PREFIX} Derivation for '${entry.fieldKey}' has neither "asyncFunctionName" nor "asyncFn". ` +
|
|
13651
|
+
`One of the two is required for source: 'asyncFunction'.`);
|
|
12911
13652
|
return EMPTY;
|
|
12912
13653
|
}
|
|
13654
|
+
if (entry.asyncFn && entry.asyncFunctionName) {
|
|
13655
|
+
context.logger.warn(`${LOG_PREFIX} Both "asyncFn" and "asyncFunctionName" are set on derivation for '${entry.fieldKey}'. ` +
|
|
13656
|
+
`Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.`);
|
|
13657
|
+
}
|
|
13658
|
+
const inlineAsyncFn = entry.asyncFn;
|
|
12913
13659
|
const asyncFunctionName = entry.asyncFunctionName;
|
|
12914
13660
|
const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS;
|
|
12915
13661
|
// startWith(null) → pairwise() causes all fields to appear "changed" on the
|
|
@@ -12955,8 +13701,8 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
12955
13701
|
const customFunctions = context.customFunctions?.();
|
|
12956
13702
|
const asyncDerivationFunctions = context.asyncDerivationFunctions?.();
|
|
12957
13703
|
const externalData = context.externalData?.();
|
|
12958
|
-
//
|
|
12959
|
-
const asyncFn = asyncDerivationFunctions?.[asyncFunctionName];
|
|
13704
|
+
// Inline `asyncFn` wins; otherwise look up the registered function.
|
|
13705
|
+
const asyncFn = inlineAsyncFn ?? (asyncFunctionName ? asyncDerivationFunctions?.[asyncFunctionName] : undefined);
|
|
12960
13706
|
if (!asyncFn) {
|
|
12961
13707
|
context.logger.warn(`${LOG_PREFIX} Async derivation function '${asyncFunctionName}' not found for field '${entry.fieldKey}'. ` +
|
|
12962
13708
|
`Register it in customFnConfig.asyncDerivations.`);
|
|
@@ -13384,7 +14130,7 @@ class DerivationOrchestrator {
|
|
|
13384
14130
|
toObservable(this.derivationCollection, { injector: this.injector })
|
|
13385
14131
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
13386
14132
|
.subscribe((collection) => {
|
|
13387
|
-
const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName) ?? [];
|
|
14133
|
+
const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [];
|
|
13388
14134
|
if (asyncEntries.length === 0) {
|
|
13389
14135
|
this.teardownAsyncFunctionStreams();
|
|
13390
14136
|
this.lastAsyncEntryKeys = null;
|
|
@@ -13431,9 +14177,14 @@ class DerivationOrchestrator {
|
|
|
13431
14177
|
* Used for smart teardown comparison.
|
|
13432
14178
|
*/
|
|
13433
14179
|
computeAsyncEntryKeys(entries) {
|
|
14180
|
+
// For inline `asyncFn` entries we tag the key with the field path (the real
|
|
14181
|
+
// identity, since derivations are self-targeting) rather than the array
|
|
14182
|
+
// index. Index-based tagging churned identities whenever topologicalSort
|
|
14183
|
+
// reordered entries, causing unnecessary teardown + re-subscribe.
|
|
13434
14184
|
return new Set(entries.map((entry) => {
|
|
13435
14185
|
const config = {
|
|
13436
14186
|
asyncFunctionName: entry.asyncFunctionName,
|
|
14187
|
+
asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}` : undefined,
|
|
13437
14188
|
dependsOn: entry.dependsOn,
|
|
13438
14189
|
debounceMs: entry.debounceMs,
|
|
13439
14190
|
stopOnUserOverride: entry.stopOnUserOverride,
|
|
@@ -13482,10 +14233,11 @@ function warnAboutWildcardDependencies$1(logger, entries, fieldCount) {
|
|
|
13482
14233
|
// HTTP and async entries require explicit dependsOn (validated at collection time)
|
|
13483
14234
|
const implicitWildcards = wildcardEntries.filter((entry) => !entry.http &&
|
|
13484
14235
|
!entry.asyncFunctionName &&
|
|
13485
|
-
entry.
|
|
14236
|
+
!entry.asyncFn &&
|
|
14237
|
+
(entry.functionName || entry.fn) &&
|
|
13486
14238
|
(!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
13487
14239
|
if (implicitWildcards.length > 0) {
|
|
13488
|
-
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName})`);
|
|
14240
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName ?? '<inline fn>'})`);
|
|
13489
14241
|
logger.warn('Derivation: custom functions without explicit dependsOn detected. ' +
|
|
13490
14242
|
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
13491
14243
|
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
@@ -13649,7 +14401,11 @@ function isDynamicFormFeature(value) {
|
|
|
13649
14401
|
Array.isArray(value.ɵproviders));
|
|
13650
14402
|
}
|
|
13651
14403
|
/**
|
|
13652
|
-
* Helper to create a feature with proper typing
|
|
14404
|
+
* Helper to create a feature with proper typing.
|
|
14405
|
+
*
|
|
14406
|
+
* `kind` is the discriminant compared by string equality at the
|
|
14407
|
+
* provider-resolution site (and in feature-specific type guards). Each
|
|
14408
|
+
* feature module owns its own kind literal — no central registry.
|
|
13653
14409
|
*/
|
|
13654
14410
|
function createFeature(kind, providers) {
|
|
13655
14411
|
return {
|
|
@@ -13968,8 +14724,16 @@ function collectPropertyDerivations(fields, logger, tracker) {
|
|
|
13968
14724
|
const entries = [];
|
|
13969
14725
|
const context = { logger, tracker };
|
|
13970
14726
|
traverseFieldsWithContext(fields, context, (field, ctx) => collectFromField(field, entries, ctx), {
|
|
13971
|
-
|
|
13972
|
-
//
|
|
14727
|
+
// Array boundary: each item is its own model root. Reset groupPath so
|
|
14728
|
+
// ancestor groups OUTSIDE the array don't bleed into per-item keys.
|
|
14729
|
+
onArrayChild: (_, field) => ({ arrayPath: field.key, groupPath: undefined }),
|
|
14730
|
+
onGroupChild: (parent, field) => {
|
|
14731
|
+
// Keyless groups (rare) act like layout containers — pass through.
|
|
14732
|
+
if (!field.key)
|
|
14733
|
+
return {};
|
|
14734
|
+
return { groupPath: parent.groupPath ? `${parent.groupPath}.${field.key}` : field.key };
|
|
14735
|
+
},
|
|
14736
|
+
// Layout containers (page, row, container) leave context unchanged.
|
|
13973
14737
|
});
|
|
13974
14738
|
return { entries };
|
|
13975
14739
|
}
|
|
@@ -14001,7 +14765,7 @@ function collectFromField(field, entries, context) {
|
|
|
14001
14765
|
* @internal
|
|
14002
14766
|
*/
|
|
14003
14767
|
function createPropertyDerivationEntryFromDerivation(fieldKey, config, context) {
|
|
14004
|
-
const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
|
|
14768
|
+
const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey, context.groupPath);
|
|
14005
14769
|
const dependsOn = extractDependenciesFromConfig(config);
|
|
14006
14770
|
const condition = config.condition ?? true;
|
|
14007
14771
|
const trigger = config.trigger ?? 'onChange';
|
|
@@ -14014,6 +14778,7 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
|
|
|
14014
14778
|
value: config.value,
|
|
14015
14779
|
expression: config.expression,
|
|
14016
14780
|
functionName: config.functionName,
|
|
14781
|
+
fn: extractInlineFn(config),
|
|
14017
14782
|
trigger,
|
|
14018
14783
|
debounceMs,
|
|
14019
14784
|
debugName: config.debugName,
|
|
@@ -14168,16 +14933,40 @@ const PROPERTY_DERIVATION_ORCHESTRATOR = new InjectionToken('PROPERTY_DERIVATION
|
|
|
14168
14933
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14169
14934
|
function warnAboutWildcardDependencies(logger, entries, fieldCount) {
|
|
14170
14935
|
const implicitWildcards = entries.filter((entry) => entry.dependsOn.includes('*') &&
|
|
14171
|
-
entry.functionName &&
|
|
14936
|
+
(entry.functionName || entry.fn) &&
|
|
14172
14937
|
(!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
14173
14938
|
if (implicitWildcards.length > 0) {
|
|
14174
|
-
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName})`);
|
|
14939
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName ?? '<inline fn>'})`);
|
|
14175
14940
|
logger.warn('PropertyDerivation: custom functions without explicit dependsOn detected. ' +
|
|
14176
14941
|
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
14177
14942
|
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
14178
14943
|
}
|
|
14179
14944
|
}
|
|
14180
14945
|
|
|
14946
|
+
/**
|
|
14947
|
+
* Multi-provider token populated by `provideAddonActions({...})` calls.
|
|
14948
|
+
* `ADDON_ACTION_REGISTRY` aggregates these into a single name → handler map.
|
|
14949
|
+
*
|
|
14950
|
+
* @internal
|
|
14951
|
+
*/
|
|
14952
|
+
const ADDON_ACTION_HANDLERS = new InjectionToken('ADDON_ACTION_HANDLERS');
|
|
14953
|
+
/**
|
|
14954
|
+
* Form-scoped name → handler map. Read by adapter button kinds at click
|
|
14955
|
+
* time for `actionRef: 'name'` lookups. Provided at form scope (not root)
|
|
14956
|
+
* because `ADDON_ACTION_HANDLERS` is form-scoped via `provideAddonActions`.
|
|
14957
|
+
*/
|
|
14958
|
+
const ADDON_ACTION_REGISTRY = new InjectionToken('ADDON_ACTION_REGISTRY');
|
|
14959
|
+
function createAddonActionRegistry() {
|
|
14960
|
+
const sources = inject(ADDON_ACTION_HANDLERS, { optional: true }) ?? [];
|
|
14961
|
+
const map = new Map();
|
|
14962
|
+
for (const handlers of sources) {
|
|
14963
|
+
for (const [name, fn] of Object.entries(handlers)) {
|
|
14964
|
+
map.set(name, fn);
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14967
|
+
return map;
|
|
14968
|
+
}
|
|
14969
|
+
|
|
14181
14970
|
/**
|
|
14182
14971
|
* Always-on providers for any DynamicForm: state machine, registries, signal-context tokens.
|
|
14183
14972
|
* Pure form bootstrap — no derivation, HTTP, or async machinery.
|
|
@@ -14192,7 +14981,7 @@ function coreProviders() {
|
|
|
14192
14981
|
FunctionRegistryService,
|
|
14193
14982
|
{
|
|
14194
14983
|
provide: FORM_STATE_DEPS,
|
|
14195
|
-
useFactory: () => ({ config: null, formOptions: null, value: null }),
|
|
14984
|
+
useFactory: () => ({ config: null, formOptions: null, value: null, source: null }),
|
|
14196
14985
|
},
|
|
14197
14986
|
FormStateManager,
|
|
14198
14987
|
{
|
|
@@ -14201,6 +14990,7 @@ function coreProviders() {
|
|
|
14201
14990
|
deps: [FormStateManager],
|
|
14202
14991
|
},
|
|
14203
14992
|
FieldContextRegistryService,
|
|
14993
|
+
ArrayItemRegistryService,
|
|
14204
14994
|
{
|
|
14205
14995
|
provide: DEFAULT_PROPS,
|
|
14206
14996
|
useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultProps),
|
|
@@ -14227,6 +15017,11 @@ function coreProviders() {
|
|
|
14227
15017
|
deps: [FormStateManager],
|
|
14228
15018
|
},
|
|
14229
15019
|
{ provide: DEPRECATION_WARNING_TRACKER, useFactory: createWarningTracker },
|
|
15020
|
+
// ADDON_ACTION_REGISTRY must live at form scope (not root) because the
|
|
15021
|
+
// multi-provider source `ADDON_ACTION_HANDLERS` is contributed by
|
|
15022
|
+
// `provideAddonActions(...)` features passed to `provideDynamicForm(...)`
|
|
15023
|
+
// — those entries are only visible inside the form's own injector tree.
|
|
15024
|
+
{ provide: ADDON_ACTION_REGISTRY, useFactory: createAddonActionRegistry },
|
|
14230
15025
|
];
|
|
14231
15026
|
}
|
|
14232
15027
|
/**
|
|
@@ -14418,13 +15213,90 @@ class EventDispatcher {
|
|
|
14418
15213
|
disconnect() {
|
|
14419
15214
|
this.bus = undefined;
|
|
14420
15215
|
}
|
|
14421
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
14422
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
15216
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
15217
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher });
|
|
15218
|
+
}
|
|
15219
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EventDispatcher, decorators: [{
|
|
15220
|
+
type: Injectable
|
|
15221
|
+
}] });
|
|
15222
|
+
|
|
15223
|
+
/**
|
|
15224
|
+
* Marks an `<ng-template>` projected into `<df-dynamic-form>` content as
|
|
15225
|
+
* available to template addons by name.
|
|
15226
|
+
*
|
|
15227
|
+
* The host form collects all `DfTemplate` instances via `contentChildren`
|
|
15228
|
+
* and exposes them through {@link DF_FIELD_TEMPLATES}, where the template
|
|
15229
|
+
* addon kind looks them up by `templateKey` at render time.
|
|
15230
|
+
*
|
|
15231
|
+
* @example
|
|
15232
|
+
* ```html
|
|
15233
|
+
* <df-dynamic-form [config]="config">
|
|
15234
|
+
* <ng-template dfTemplate="searchIcon" let-field>
|
|
15235
|
+
* <my-icon [field]="field" />
|
|
15236
|
+
* </ng-template>
|
|
15237
|
+
* </df-dynamic-form>
|
|
15238
|
+
* ```
|
|
15239
|
+
*
|
|
15240
|
+
* In the field config:
|
|
15241
|
+
* ```ts
|
|
15242
|
+
* { slot: 'prefix', kind: 'template', templateKey: 'searchIcon' }
|
|
15243
|
+
* ```
|
|
15244
|
+
*/
|
|
15245
|
+
class DfTemplate {
|
|
15246
|
+
/** Name this template registers under — referenced from `TemplateAddon.templateKey`. */
|
|
15247
|
+
dfTemplate = input.required(...(ngDevMode ? [{ debugName: "dfTemplate" }] : /* istanbul ignore next */ []));
|
|
15248
|
+
/** The wrapped template, captured for registration and `NgTemplateOutlet` rendering. */
|
|
15249
|
+
templateRef = inject(TemplateRef);
|
|
15250
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
15251
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: DfTemplate, isStandalone: true, selector: "ng-template[dfTemplate]", inputs: { dfTemplate: { classPropertyName: "dfTemplate", publicName: "dfTemplate", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["dfTemplate"], ngImport: i0 });
|
|
14423
15252
|
}
|
|
14424
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15253
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfTemplate, decorators: [{
|
|
15254
|
+
type: Directive,
|
|
15255
|
+
args: [{
|
|
15256
|
+
selector: 'ng-template[dfTemplate]',
|
|
15257
|
+
exportAs: 'dfTemplate',
|
|
15258
|
+
}]
|
|
15259
|
+
}], propDecorators: { dfTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "dfTemplate", required: true }] }] } });
|
|
15260
|
+
/**
|
|
15261
|
+
* Holder service published at the `<df-dynamic-form>` component scope.
|
|
15262
|
+
* `DynamicForm` collects projected `<ng-template dfTemplate="...">` via
|
|
15263
|
+
* `contentChildren(DfTemplate)` and pushes the resulting map through
|
|
15264
|
+
* `DfFieldTemplateRegistry.set(...)`. The {@link DF_FIELD_TEMPLATES}
|
|
15265
|
+
* injection token is provided as a factory that returns this service's
|
|
15266
|
+
* `map` signal, so template-addon components can resolve `templateKey`
|
|
15267
|
+
* without coupling to the host form's generic class.
|
|
15268
|
+
*/
|
|
15269
|
+
class DfFieldTemplateRegistry {
|
|
15270
|
+
_map = signal(new Map(), ...(ngDevMode ? [{ debugName: "_map" }] : /* istanbul ignore next */ []));
|
|
15271
|
+
/** Reactive view consumed by the `template` addon kind via DI. */
|
|
15272
|
+
map = this._map.asReadonly();
|
|
15273
|
+
/** Replace the registry contents — called from `DynamicForm` when projected templates change. */
|
|
15274
|
+
set(value) {
|
|
15275
|
+
this._map.set(value);
|
|
15276
|
+
}
|
|
15277
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
15278
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry });
|
|
15279
|
+
}
|
|
15280
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfFieldTemplateRegistry, decorators: [{
|
|
14425
15281
|
type: Injectable
|
|
14426
15282
|
}] });
|
|
14427
15283
|
|
|
15284
|
+
/**
|
|
15285
|
+
* Map of named `TemplateRef`s collected by `<df-dynamic-form>` from
|
|
15286
|
+
* `<ng-template dfTemplate="...">` children projected into its content.
|
|
15287
|
+
* The directive selector is `dfTemplate` (see `DfTemplate`); the input
|
|
15288
|
+
* value is the name addons reference via `templateKey`.
|
|
15289
|
+
*
|
|
15290
|
+
* The `template` addon kind looks up its `templateKey` in this map at
|
|
15291
|
+
* render time. Exposed as a `Signal` so projections can be added or
|
|
15292
|
+
* removed reactively (e.g., when the host conditionally projects).
|
|
15293
|
+
*
|
|
15294
|
+
* Provided at the form-component level (not root) so multiple
|
|
15295
|
+
* `<df-dynamic-form>` instances on the same page have independent
|
|
15296
|
+
* template scopes.
|
|
15297
|
+
*/
|
|
15298
|
+
const DF_FIELD_TEMPLATES = new InjectionToken('DF_FIELD_TEMPLATES');
|
|
15299
|
+
|
|
14428
15300
|
/**
|
|
14429
15301
|
* Dynamic form component — renders a form based on configuration.
|
|
14430
15302
|
* Delegates state management to `FormStateManager`.
|
|
@@ -14444,6 +15316,13 @@ class DynamicForm {
|
|
|
14444
15316
|
formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : /* istanbul ignore next */ []));
|
|
14445
15317
|
/** Form values for two-way data binding. */
|
|
14446
15318
|
value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
15319
|
+
/**
|
|
15320
|
+
* How lenient the addon validator is on `config.fields[...].addons`.
|
|
15321
|
+
* `'inline'` (default) keeps everything; `'json'` drops code-only kinds
|
|
15322
|
+
* with a warning. Read at form init and on every config swap — change
|
|
15323
|
+
* `source` AND the config to force re-validation mid-flight.
|
|
15324
|
+
*/
|
|
15325
|
+
source = input('inline', ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
|
|
14447
15326
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14448
15327
|
// Dependencies
|
|
14449
15328
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -14453,6 +15332,12 @@ class DynamicForm {
|
|
|
14453
15332
|
eventBus = inject(EventBus);
|
|
14454
15333
|
logger = inject(DynamicFormLogger);
|
|
14455
15334
|
dispatcher = inject(EventDispatcher, { optional: true });
|
|
15335
|
+
templateRegistry = inject(DfFieldTemplateRegistry);
|
|
15336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15337
|
+
// Projected templates — collected for the `template` addon kind via DI.
|
|
15338
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15339
|
+
/** All `<ng-template dfTemplate="...">` instances projected into this form. */
|
|
15340
|
+
_projectedTemplates = contentChildren(DfTemplate, ...(ngDevMode ? [{ debugName: "_projectedTemplates" }] : /* istanbul ignore next */ []));
|
|
14456
15341
|
/** State manager that owns all form state. Initialized via connectDeps() to guarantee
|
|
14457
15342
|
* FORM_STATE_DEPS is populated before FormStateManager is injected. */
|
|
14458
15343
|
stateManager = this.connectDeps();
|
|
@@ -14605,6 +15490,15 @@ class DynamicForm {
|
|
|
14605
15490
|
}, { injector: this.injector });
|
|
14606
15491
|
}
|
|
14607
15492
|
});
|
|
15493
|
+
// Mirror projected `<ng-template dfTemplate="...">` into the template
|
|
15494
|
+
// registry so `template` addons can resolve `templateKey` via DI.
|
|
15495
|
+
explicitEffect([this._projectedTemplates], ([templates]) => {
|
|
15496
|
+
const map = new Map();
|
|
15497
|
+
for (const t of templates) {
|
|
15498
|
+
map.set(t.dfTemplate(), t.templateRef);
|
|
15499
|
+
}
|
|
15500
|
+
this.templateRegistry.set(map);
|
|
15501
|
+
});
|
|
14608
15502
|
}
|
|
14609
15503
|
/**
|
|
14610
15504
|
* Populates FORM_STATE_DEPS with this component's input signals, then injects
|
|
@@ -14617,6 +15511,7 @@ class DynamicForm {
|
|
|
14617
15511
|
deps.config = this.config;
|
|
14618
15512
|
deps.formOptions = this.formOptions;
|
|
14619
15513
|
deps.value = this.value;
|
|
15514
|
+
deps.source = this.source;
|
|
14620
15515
|
return inject((FormStateManager));
|
|
14621
15516
|
}
|
|
14622
15517
|
setupEventHandlers() {
|
|
@@ -14632,8 +15527,12 @@ class DynamicForm {
|
|
|
14632
15527
|
error: (err) => this.logger.error('Submission handler error', err),
|
|
14633
15528
|
});
|
|
14634
15529
|
}
|
|
14635
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
14636
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
15530
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15531
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { attributes: { "novalidate": "" }, listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [
|
|
15532
|
+
provideDynamicFormDI(),
|
|
15533
|
+
DfFieldTemplateRegistry,
|
|
15534
|
+
{ provide: DF_FIELD_TEMPLATES, useFactory: () => inject(DfFieldTemplateRegistry).map },
|
|
15535
|
+
], queries: [{ propertyName: "_projectedTemplates", predicate: DfTemplate, isSignal: true }], ngImport: i0, template: `
|
|
14637
15536
|
@if (shouldRender()) {
|
|
14638
15537
|
@switch (formModeDetection().mode) {
|
|
14639
15538
|
@case ('paged') {
|
|
@@ -14641,7 +15540,13 @@ class DynamicForm {
|
|
|
14641
15540
|
}
|
|
14642
15541
|
@case ('non-paged') {
|
|
14643
15542
|
@for (field of resolvedFields(); track field.key) {
|
|
14644
|
-
|
|
15543
|
+
<!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
|
|
15544
|
+
the template @if removes the host from the DOM (Angular's prescribed pattern), and
|
|
15545
|
+
the directive's gate stops a same-CD-pass mount before [formField] can warn.
|
|
15546
|
+
Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
|
|
15547
|
+
@if (!field.hidden()) {
|
|
15548
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15549
|
+
}
|
|
14645
15550
|
}
|
|
14646
15551
|
}
|
|
14647
15552
|
@default {
|
|
@@ -14649,9 +15554,9 @@ class DynamicForm {
|
|
|
14649
15554
|
}
|
|
14650
15555
|
}
|
|
14651
15556
|
}
|
|
14652
|
-
`, isInline: true, styles: ["
|
|
15557
|
+
`, isInline: true, styles: [".df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap, .5rem);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-sm, .5rem)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns, 12),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-md, .5rem)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-lg, .5rem)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-xl, .5rem)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--df-form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{gap:0}\n"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }, { kind: "component", type: PageOrchestratorComponent, selector: "div[page-orchestrator]", inputs: ["pageFields", "form", "fieldSignalContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14653
15558
|
}
|
|
14654
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15559
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicForm, decorators: [{
|
|
14655
15560
|
type: Component,
|
|
14656
15561
|
args: [{ selector: 'form[dynamic-form]', imports: [DfFieldOutlet, PageOrchestratorComponent], template: `
|
|
14657
15562
|
@if (shouldRender()) {
|
|
@@ -14661,7 +15566,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
14661
15566
|
}
|
|
14662
15567
|
@case ('non-paged') {
|
|
14663
15568
|
@for (field of resolvedFields(); track field.key) {
|
|
14664
|
-
|
|
15569
|
+
<!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
|
|
15570
|
+
the template @if removes the host from the DOM (Angular's prescribed pattern), and
|
|
15571
|
+
the directive's gate stops a same-CD-pass mount before [formField] can warn.
|
|
15572
|
+
Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
|
|
15573
|
+
@if (!field.hidden()) {
|
|
15574
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15575
|
+
}
|
|
14665
15576
|
}
|
|
14666
15577
|
}
|
|
14667
15578
|
@default {
|
|
@@ -14669,7 +15580,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
14669
15580
|
}
|
|
14670
15581
|
}
|
|
14671
15582
|
}
|
|
14672
|
-
`, providers: [
|
|
15583
|
+
`, providers: [
|
|
15584
|
+
provideDynamicFormDI(),
|
|
15585
|
+
DfFieldTemplateRegistry,
|
|
15586
|
+
{ provide: DF_FIELD_TEMPLATES, useFactory: () => inject(DfFieldTemplateRegistry).map },
|
|
15587
|
+
], host: {
|
|
14673
15588
|
class: 'df-dynamic-form df-form',
|
|
14674
15589
|
novalidate: '', // Disable browser validation - Angular Signal Forms handles validation
|
|
14675
15590
|
'[class.disabled]': 'disabled()',
|
|
@@ -14677,8 +15592,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
14677
15592
|
'[class.df-form-non-paged]': 'formModeDetection().mode === "non-paged"',
|
|
14678
15593
|
'[attr.data-form-mode]': 'formModeDetection().mode',
|
|
14679
15594
|
'(submit)': 'onNativeSubmit($event)',
|
|
14680
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["
|
|
14681
|
-
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamic-form", required: true }] }], formOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formOptions", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], reset: [{ type: i0.Output, args: ["reset"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], events: [{ type: i0.Output, args: ["events"] }], initialized: [{ type: i0.Output, args: ["initialized"] }], onPageChange: [{ type: i0.Output, args: ["onPageChange"] }], onPageNavigationStateChange: [{ type: i0.Output, args: ["onPageNavigationStateChange"] }] } });
|
|
15595
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap, .5rem);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-sm, .5rem)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns, 12),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-md, .5rem)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-lg, .5rem)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-xl, .5rem)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--df-form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{gap:0}\n"] }]
|
|
15596
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamic-form", required: true }] }], formOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formOptions", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], _projectedTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => DfTemplate), { isSignal: true }] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], reset: [{ type: i0.Output, args: ["reset"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], events: [{ type: i0.Output, args: ["events"] }], initialized: [{ type: i0.Output, args: ["initialized"] }], onPageChange: [{ type: i0.Output, args: ["onPageChange"] }], onPageNavigationStateChange: [{ type: i0.Output, args: ["onPageNavigationStateChange"] }] } });
|
|
14682
15597
|
/**
|
|
14683
15598
|
* Recursively counts container fields (page, row, group, array, wrapper) in a field tree.
|
|
14684
15599
|
* Descends into container children including array item templates to ensure
|
|
@@ -14708,6 +15623,147 @@ function countContainersRecursive(fields) {
|
|
|
14708
15623
|
return count;
|
|
14709
15624
|
}
|
|
14710
15625
|
|
|
15626
|
+
/**
|
|
15627
|
+
* Cache for resolved addon-kind components.
|
|
15628
|
+
*
|
|
15629
|
+
* Provided per-form by `provideDynamicFormDI()` so two `<df-dynamic-form>`
|
|
15630
|
+
* instances with conflicting `loadComponent` factories for the same kind
|
|
15631
|
+
* never share a stale entry. Has no `providedIn: 'root'` factory — the
|
|
15632
|
+
* registry consumer is always inside a form scope, and a root fallback
|
|
15633
|
+
* would only hide misconfiguration (an SSR cross-request hit if a future
|
|
15634
|
+
* code path forgot to provide it locally).
|
|
15635
|
+
*
|
|
15636
|
+
* Mirrors `COMPONENT_CACHE` (field types) and `WRAPPER_COMPONENT_CACHE`
|
|
15637
|
+
* (wrappers).
|
|
15638
|
+
*
|
|
15639
|
+
* @internal
|
|
15640
|
+
*/
|
|
15641
|
+
const ADDON_KIND_COMPONENT_CACHE = new InjectionToken('ADDON_KIND_COMPONENT_CACHE');
|
|
15642
|
+
/**
|
|
15643
|
+
* Injection function for accessing the addon kind registry.
|
|
15644
|
+
*
|
|
15645
|
+
* Usage parallels {@link injectFieldRegistry}: lookup, lazy load, cache.
|
|
15646
|
+
* Must be called within an injection context.
|
|
15647
|
+
*
|
|
15648
|
+
* @example
|
|
15649
|
+
* ```typescript
|
|
15650
|
+
* const addonKinds = injectAddonKindRegistry();
|
|
15651
|
+
* const Component = await addonKinds.loadKindComponent('prime-icon');
|
|
15652
|
+
* ```
|
|
15653
|
+
*/
|
|
15654
|
+
function injectAddonKindRegistry() {
|
|
15655
|
+
const registry = inject(ADDON_KIND_REGISTRY);
|
|
15656
|
+
const componentCache = inject(ADDON_KIND_COMPONENT_CACHE);
|
|
15657
|
+
return {
|
|
15658
|
+
/** Look up a kind definition by its discriminant. */
|
|
15659
|
+
getKind(kind) {
|
|
15660
|
+
return registry.get(kind);
|
|
15661
|
+
},
|
|
15662
|
+
/** Whether a kind is currently registered. */
|
|
15663
|
+
hasKind(kind) {
|
|
15664
|
+
return registry.has(kind);
|
|
15665
|
+
},
|
|
15666
|
+
/** All registered kind names — useful for actionable validator warnings. */
|
|
15667
|
+
getKindNames() {
|
|
15668
|
+
return Array.from(registry.keys());
|
|
15669
|
+
},
|
|
15670
|
+
/**
|
|
15671
|
+
* Resolve a kind to its renderer component. Caches on first resolution.
|
|
15672
|
+
*
|
|
15673
|
+
* @throws DynamicFormError when the kind is not registered.
|
|
15674
|
+
*/
|
|
15675
|
+
async loadKindComponent(kind) {
|
|
15676
|
+
const definition = registry.get(kind);
|
|
15677
|
+
if (!definition) {
|
|
15678
|
+
const known = Array.from(registry.keys()).join(', ') || '(none)';
|
|
15679
|
+
throw new DynamicFormError(`Addon kind '${kind}' is not registered. Currently registered kinds: ${known}.`);
|
|
15680
|
+
}
|
|
15681
|
+
const cached = componentCache.get(kind);
|
|
15682
|
+
if (cached) {
|
|
15683
|
+
return cached;
|
|
15684
|
+
}
|
|
15685
|
+
try {
|
|
15686
|
+
const component = resolveDefaultExport(await definition.loadComponent());
|
|
15687
|
+
if (component) {
|
|
15688
|
+
componentCache.set(kind, component);
|
|
15689
|
+
return component;
|
|
15690
|
+
}
|
|
15691
|
+
throw new DynamicFormError(`Addon kind '${kind}' loadComponent resolved to null.`);
|
|
15692
|
+
}
|
|
15693
|
+
catch (error) {
|
|
15694
|
+
if (error instanceof DynamicFormError)
|
|
15695
|
+
throw error;
|
|
15696
|
+
throw new DynamicFormError(`Failed to load component for addon kind '${kind}': ${error instanceof Error ? error.message : String(error)}`);
|
|
15697
|
+
}
|
|
15698
|
+
},
|
|
15699
|
+
/**
|
|
15700
|
+
* Synchronous lookup of an already-loaded kind component, or undefined.
|
|
15701
|
+
* Returns `undefined` when the kind isn't registered in this scope even
|
|
15702
|
+
* if a prior scope cached a component under the same name — prevents
|
|
15703
|
+
* cross-form cache hits when registrations diverge.
|
|
15704
|
+
*/
|
|
15705
|
+
getLoadedKindComponent(kind) {
|
|
15706
|
+
if (!registry.has(kind))
|
|
15707
|
+
return undefined;
|
|
15708
|
+
return componentCache.get(kind);
|
|
15709
|
+
},
|
|
15710
|
+
/** Direct read-only access to the registry map. */
|
|
15711
|
+
get raw() {
|
|
15712
|
+
return registry;
|
|
15713
|
+
},
|
|
15714
|
+
};
|
|
15715
|
+
}
|
|
15716
|
+
|
|
15717
|
+
/**
|
|
15718
|
+
* Built-in addon kinds shipped by core.
|
|
15719
|
+
*
|
|
15720
|
+
* Universally adapter-independent: `text`, `template`, `component`. Adapters
|
|
15721
|
+
* register their own kinds (e.g., `prime-icon`, `prime-button`) via
|
|
15722
|
+
* `withCustomAddon(...)` features.
|
|
15723
|
+
*/
|
|
15724
|
+
const BUILT_IN_ADDON_KINDS = [
|
|
15725
|
+
{
|
|
15726
|
+
kind: 'text',
|
|
15727
|
+
loadComponent: () => Promise.resolve().then(function () { return textAddon_component; }).then((m) => m.TextAddonComponent),
|
|
15728
|
+
validate: (addon, fieldKey) => {
|
|
15729
|
+
const text = addon.text;
|
|
15730
|
+
// Validates structural presence: rejects missing / null / non-string-non-callable,
|
|
15731
|
+
// and rejects literal empty strings. DynamicText values that resolve to empty
|
|
15732
|
+
// at runtime (signal/observable yielding '') are NOT caught here — they're
|
|
15733
|
+
// legal as far as the config is concerned, just render an empty span.
|
|
15734
|
+
if (text === undefined || text === null) {
|
|
15735
|
+
throw new DynamicFormError(`Addon kind 'text' requires a 'text' field (field: '${fieldKey}').`);
|
|
15736
|
+
}
|
|
15737
|
+
if (typeof text === 'string' && text.length === 0) {
|
|
15738
|
+
throw new DynamicFormError(`Addon kind 'text' requires a non-empty 'text' string (field: '${fieldKey}').`);
|
|
15739
|
+
}
|
|
15740
|
+
},
|
|
15741
|
+
},
|
|
15742
|
+
{
|
|
15743
|
+
kind: 'template',
|
|
15744
|
+
loadComponent: () => Promise.resolve().then(function () { return templateAddon_component; }).then((m) => m.TemplateAddonComponent),
|
|
15745
|
+
validate: (addon, fieldKey) => {
|
|
15746
|
+
const key = addon.templateKey;
|
|
15747
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
15748
|
+
throw new DynamicFormError(`Addon kind 'template' requires a non-empty 'templateKey' string (field: '${fieldKey}').`);
|
|
15749
|
+
}
|
|
15750
|
+
},
|
|
15751
|
+
},
|
|
15752
|
+
{
|
|
15753
|
+
kind: 'component',
|
|
15754
|
+
// The shape carries a function loader — not serialisable; dropped by the
|
|
15755
|
+
// validator when the form config originated from JSON.
|
|
15756
|
+
jsonSafe: false,
|
|
15757
|
+
loadComponent: () => Promise.resolve().then(function () { return componentAddon_component; }).then((m) => m.ComponentAddonComponent),
|
|
15758
|
+
validate: (addon, fieldKey) => {
|
|
15759
|
+
const loader = addon.component;
|
|
15760
|
+
if (typeof loader !== 'function') {
|
|
15761
|
+
throw new DynamicFormError(`Addon kind 'component' requires a 'component' loader function (field: '${fieldKey}').`);
|
|
15762
|
+
}
|
|
15763
|
+
},
|
|
15764
|
+
},
|
|
15765
|
+
];
|
|
15766
|
+
|
|
14711
15767
|
/**
|
|
14712
15768
|
* No-op logger for when no logger is provided.
|
|
14713
15769
|
* Used as fallback in button logic evaluation to avoid breaking downstream packages.
|
|
@@ -15244,8 +16300,14 @@ function containerFieldMapper(fieldDef) {
|
|
|
15244
16300
|
* ```
|
|
15245
16301
|
*/
|
|
15246
16302
|
/**
|
|
15247
|
-
* Base
|
|
15248
|
-
*
|
|
16303
|
+
* Base for container field types that consume the mapper-emitted `field` input
|
|
16304
|
+
* (row, group, array, page, container).
|
|
16305
|
+
*/
|
|
16306
|
+
const CONTAINER_FIELD_TYPES_BASE = {
|
|
16307
|
+
renderReadyWhen: ['field'],
|
|
16308
|
+
};
|
|
16309
|
+
/**
|
|
16310
|
+
* Base for display-only fields (`text`) that render immediately without
|
|
15249
16311
|
* waiting for form value integration.
|
|
15250
16312
|
*/
|
|
15251
16313
|
const DISPLAY_FIELD_TYPES_BASE = {
|
|
@@ -15264,31 +16326,35 @@ const BUILT_IN_FIELDS = [
|
|
|
15264
16326
|
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
15265
16327
|
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
15266
16328
|
// renders the flex/grid layout via RowWrapperComponent.
|
|
15267
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
16329
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
|
|
15268
16330
|
mapper: rowFieldMapper,
|
|
15269
16331
|
valueHandling: 'flatten',
|
|
16332
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15270
16333
|
},
|
|
15271
16334
|
{
|
|
15272
16335
|
name: 'group',
|
|
15273
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-
|
|
16336
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs'),
|
|
15274
16337
|
mapper: groupFieldMapper,
|
|
15275
16338
|
valueHandling: 'include',
|
|
16339
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15276
16340
|
},
|
|
15277
16341
|
{
|
|
15278
16342
|
name: 'array',
|
|
15279
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-
|
|
16343
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs'),
|
|
15280
16344
|
mapper: arrayFieldMapper,
|
|
15281
16345
|
valueHandling: 'include',
|
|
16346
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15282
16347
|
},
|
|
15283
16348
|
{
|
|
15284
16349
|
name: 'page',
|
|
15285
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
16350
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs'),
|
|
15286
16351
|
mapper: pageFieldMapper,
|
|
15287
16352
|
valueHandling: 'flatten',
|
|
16353
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15288
16354
|
},
|
|
15289
16355
|
{
|
|
15290
16356
|
name: 'text',
|
|
15291
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
16357
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs'),
|
|
15292
16358
|
mapper: textFieldMapper,
|
|
15293
16359
|
valueHandling: 'exclude',
|
|
15294
16360
|
...DISPLAY_FIELD_TYPES_BASE,
|
|
@@ -15300,22 +16366,32 @@ const BUILT_IN_FIELDS = [
|
|
|
15300
16366
|
},
|
|
15301
16367
|
{
|
|
15302
16368
|
name: 'container',
|
|
15303
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
16369
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
|
|
15304
16370
|
mapper: containerFieldMapper,
|
|
15305
16371
|
valueHandling: 'flatten',
|
|
16372
|
+
...CONTAINER_FIELD_TYPES_BASE,
|
|
15306
16373
|
},
|
|
15307
16374
|
];
|
|
15308
16375
|
const BUILT_IN_WRAPPERS = [
|
|
15309
16376
|
{
|
|
15310
16377
|
wrapperName: 'css',
|
|
15311
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-
|
|
16378
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs'),
|
|
15312
16379
|
},
|
|
15313
16380
|
{
|
|
15314
16381
|
wrapperName: 'row',
|
|
15315
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-row-wrapper.component-
|
|
16382
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs'),
|
|
15316
16383
|
},
|
|
15317
16384
|
];
|
|
15318
16385
|
|
|
16386
|
+
/**
|
|
16387
|
+
* Multi-provider token used by `withCustomAddon(...)` features to contribute
|
|
16388
|
+
* addon kind definitions. `provideDynamicForm` aggregates all values into
|
|
16389
|
+
* the form-scoped `ADDON_KIND_REGISTRY` map.
|
|
16390
|
+
*
|
|
16391
|
+
* @internal
|
|
16392
|
+
*/
|
|
16393
|
+
const ADDON_KIND_DEFINITIONS = new InjectionToken('ADDON_KIND_DEFINITIONS');
|
|
16394
|
+
|
|
15319
16395
|
/**
|
|
15320
16396
|
* Bundle wrapper registrations into a single object that can be passed to
|
|
15321
16397
|
* `provideDynamicForm(...)`.
|
|
@@ -15432,8 +16508,6 @@ function provideDynamicForm(...items) {
|
|
|
15432
16508
|
const defaultLoggerProvider = hasLoggerFeature ? [] : [{ provide: DynamicFormLogger, useValue: new ConsoleLogger() }];
|
|
15433
16509
|
return makeEnvironmentProviders([
|
|
15434
16510
|
...defaultLoggerProvider,
|
|
15435
|
-
// Always provide default Signal Forms classes (ng-touched, ng-invalid, etc.)
|
|
15436
|
-
provideSignalFormsConfig({ classes: NG_STATUS_CLASSES }),
|
|
15437
16511
|
{
|
|
15438
16512
|
provide: FIELD_REGISTRY,
|
|
15439
16513
|
useFactory: () => {
|
|
@@ -15484,9 +16558,34 @@ function provideDynamicForm(...items) {
|
|
|
15484
16558
|
return autoMap;
|
|
15485
16559
|
},
|
|
15486
16560
|
},
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
16561
|
+
// Form-scoped addon kind registry. Built from BUILT_IN_ADDON_KINDS plus
|
|
16562
|
+
// every kind contributed by `withCustomAddon(...)` features (via the
|
|
16563
|
+
// ADDON_KIND_DEFINITIONS multi-provider). Overrides the empty
|
|
16564
|
+
// `providedIn: 'root'` default so contributions from form-scoped
|
|
16565
|
+
// providers are visible.
|
|
16566
|
+
{
|
|
16567
|
+
provide: ADDON_KIND_REGISTRY,
|
|
16568
|
+
useFactory: () => {
|
|
16569
|
+
const logger = inject(DynamicFormLogger);
|
|
16570
|
+
const contributed = inject(ADDON_KIND_DEFINITIONS, { optional: true }) ?? [];
|
|
16571
|
+
const registry = new Map();
|
|
16572
|
+
for (const def of BUILT_IN_ADDON_KINDS) {
|
|
16573
|
+
registry.set(def.kind, def);
|
|
16574
|
+
}
|
|
16575
|
+
for (const def of contributed) {
|
|
16576
|
+
if (registry.has(def.kind)) {
|
|
16577
|
+
logger.warn(`Addon kind "${def.kind}" is already registered. Overwriting.`);
|
|
16578
|
+
}
|
|
16579
|
+
registry.set(def.kind, def);
|
|
16580
|
+
}
|
|
16581
|
+
return registry;
|
|
16582
|
+
},
|
|
16583
|
+
},
|
|
16584
|
+
// Per-form addon-component cache (no root fallback — isolated by form scope).
|
|
16585
|
+
{ provide: ADDON_KIND_COMPONENT_CACHE, useFactory: () => new Map() },
|
|
16586
|
+
...featureProviders,
|
|
16587
|
+
]);
|
|
16588
|
+
}
|
|
15490
16589
|
|
|
15491
16590
|
/**
|
|
15492
16591
|
* Enables automatic form value attachment to all events dispatched through the EventBus.
|
|
@@ -15660,6 +16759,35 @@ function withValidationExecutionDefaults(config) {
|
|
|
15660
16759
|
return createFeature('validation-execution', [{ provide: VALIDATION_EXECUTION_DEFAULTS, useValue: resolved }]);
|
|
15661
16760
|
}
|
|
15662
16761
|
|
|
16762
|
+
/**
|
|
16763
|
+
* Opt into Angular Signal Forms' legacy `.ng-touched` / `.ng-invalid` /
|
|
16764
|
+
* `.ng-dirty` / `.ng-pristine` / `.ng-untouched` / `.ng-pending` /
|
|
16765
|
+
* `.ng-valid` CSS classes on form-bound elements.
|
|
16766
|
+
*
|
|
16767
|
+
* Wires Angular's `@angular/forms/signals/compat` `NG_STATUS_CLASSES` strategy
|
|
16768
|
+
* via `provideSignalFormsConfig`. Without this feature the modern Signal Forms
|
|
16769
|
+
* class strategy is active, which omits the legacy `ng-*` classes.
|
|
16770
|
+
*
|
|
16771
|
+
* @example Opt in once at the application root
|
|
16772
|
+
* ```typescript
|
|
16773
|
+
* bootstrapApplication(AppComponent, {
|
|
16774
|
+
* providers: [
|
|
16775
|
+
* provideDynamicForm(
|
|
16776
|
+
* ...withMaterialFields(),
|
|
16777
|
+
* withLegacyStatusClasses()
|
|
16778
|
+
* )
|
|
16779
|
+
* ]
|
|
16780
|
+
* });
|
|
16781
|
+
* ```
|
|
16782
|
+
*
|
|
16783
|
+
* @returns A DynamicFormFeature that enables the legacy `ng-*` class strategy
|
|
16784
|
+
*
|
|
16785
|
+
* @public
|
|
16786
|
+
*/
|
|
16787
|
+
function withLegacyStatusClasses() {
|
|
16788
|
+
return createFeature('legacy-status-classes', [...provideSignalFormsConfig({ classes: NG_STATUS_CLASSES })]);
|
|
16789
|
+
}
|
|
16790
|
+
|
|
15663
16791
|
/**
|
|
15664
16792
|
* Zero-cost type carrier used in wrapper registrations.
|
|
15665
16793
|
*
|
|
@@ -15687,6 +16815,713 @@ function wrapperProps() {
|
|
|
15687
16815
|
return undefined;
|
|
15688
16816
|
}
|
|
15689
16817
|
|
|
16818
|
+
/** Type guard narrowing {@link AddonActionContext} to its field-bound variant. */
|
|
16819
|
+
function isFieldBoundContext(ctx) {
|
|
16820
|
+
return ctx.form !== null;
|
|
16821
|
+
}
|
|
16822
|
+
|
|
16823
|
+
/**
|
|
16824
|
+
* Normalises a {@link DynamicValue} into a `Signal<T>`.
|
|
16825
|
+
*
|
|
16826
|
+
* Accepts a static value, a `Signal<T>`, or an `Observable<T>`. The result
|
|
16827
|
+
* is a `Signal<T>` that reactively reflects the current value, suitable
|
|
16828
|
+
* for direct use in Angular templates.
|
|
16829
|
+
*
|
|
16830
|
+
* Static values yield a `computed` that always returns the same constant —
|
|
16831
|
+
* preferable to wrapping in `signal()` because the computed never causes
|
|
16832
|
+
* change-detection invalidation.
|
|
16833
|
+
*
|
|
16834
|
+
* Pass an `injector` when calling outside an Angular injection context
|
|
16835
|
+
* (e.g., from inside a `computed`). `toSignal` needs a `DestroyRef` to
|
|
16836
|
+
* unsubscribe — without an injector it falls back to `inject(DestroyRef)`,
|
|
16837
|
+
* which throws when called outside injection context.
|
|
16838
|
+
*
|
|
16839
|
+
* @example
|
|
16840
|
+
* ```typescript
|
|
16841
|
+
* const isHidden = resolveDynamicValue(addon().hidden, false);
|
|
16842
|
+
* // template: \@if (isHidden()) { ... }
|
|
16843
|
+
* ```
|
|
16844
|
+
*/
|
|
16845
|
+
function resolveDynamicValue(value, fallback, injector) {
|
|
16846
|
+
if (value === undefined) {
|
|
16847
|
+
return computed(() => fallback);
|
|
16848
|
+
}
|
|
16849
|
+
if (isSignal(value)) {
|
|
16850
|
+
return value;
|
|
16851
|
+
}
|
|
16852
|
+
if (isObservable(value)) {
|
|
16853
|
+
return toSignal(value, { initialValue: fallback, injector });
|
|
16854
|
+
}
|
|
16855
|
+
// Static value
|
|
16856
|
+
return computed(() => value);
|
|
16857
|
+
}
|
|
16858
|
+
|
|
16859
|
+
/**
|
|
16860
|
+
* Dispatcher component that renders an addon by looking up its `kind` in
|
|
16861
|
+
* `ADDON_KIND_REGISTRY` and instantiating the registered component.
|
|
16862
|
+
*
|
|
16863
|
+
* Used by every adapter's field component to render slot content:
|
|
16864
|
+
*
|
|
16865
|
+
* ```html
|
|
16866
|
+
* \@for (a of addonsAt('prefix'); track $index) {
|
|
16867
|
+
* <df-addon-slot [addon]="a" />
|
|
16868
|
+
* }
|
|
16869
|
+
* ```
|
|
16870
|
+
*
|
|
16871
|
+
* Behaviour:
|
|
16872
|
+
* - Resolves and caches the kind's component asynchronously on first render.
|
|
16873
|
+
* - Forwards the `slot` HTML attribute on the host so Ionic shadow-DOM
|
|
16874
|
+
* projection works (`<ion-input>` projects children with `slot="start"`
|
|
16875
|
+
* into its native start slot).
|
|
16876
|
+
* - Honours the addon's reactive `hidden` flag — when `true`, the host is
|
|
16877
|
+
* `display: none` so it occupies no layout but stays in DOM (cheaper than
|
|
16878
|
+
* tearing down the component).
|
|
16879
|
+
* - Forwards `className` to the host element.
|
|
16880
|
+
* - ARIA semantics are owned by individual kind components — decorative
|
|
16881
|
+
* kinds (icons, text) set `aria-hidden="true"` on themselves; interactive
|
|
16882
|
+
* kinds (buttons) handle their own `aria-label`.
|
|
16883
|
+
*
|
|
16884
|
+
* If the kind cannot be resolved (registry miss, load failure), the
|
|
16885
|
+
* dispatcher logs a warning and renders nothing — never throws. The
|
|
16886
|
+
* runtime addon validator should have caught unknown kinds at config init,
|
|
16887
|
+
* but this provides a defence-in-depth fallback for misconfigured registries.
|
|
16888
|
+
*/
|
|
16889
|
+
class DfAddonSlot {
|
|
16890
|
+
registry = injectAddonKindRegistry();
|
|
16891
|
+
logger = inject(DynamicFormLogger);
|
|
16892
|
+
hostInjector = inject(Injector);
|
|
16893
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
16894
|
+
/**
|
|
16895
|
+
* Optional read-only view of the host field's mapped inputs — same shape
|
|
16896
|
+
* wrappers receive (`field?: ReadonlyFieldTree`, `key`, `props`, etc.).
|
|
16897
|
+
* Forwarded to kind components so they can read field state without
|
|
16898
|
+
* re-injecting `FIELD_SIGNAL_CONTEXT` and rebuilding context themselves.
|
|
16899
|
+
*
|
|
16900
|
+
* `undefined` when the addon is rendered outside a field (rare).
|
|
16901
|
+
*/
|
|
16902
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
16903
|
+
/**
|
|
16904
|
+
* Pre-resolved `hidden` signal supplied by the host field. When provided,
|
|
16905
|
+
* the dispatcher reads this instead of re-resolving `addon.hidden` —
|
|
16906
|
+
* lets the host (e.g., `prime-input`) share a single subscription per
|
|
16907
|
+
* Observable-typed `hidden` between its own visibility filter and the
|
|
16908
|
+
* slot's `[style.display]` binding.
|
|
16909
|
+
*/
|
|
16910
|
+
hidden = input(undefined, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
16911
|
+
/** `slot` HTML attribute forwarded to host — needed for Ionic shadow projection. */
|
|
16912
|
+
slotAttr = computed(() => this.addon().slot, ...(ngDevMode ? [{ debugName: "slotAttr" }] : /* istanbul ignore next */ []));
|
|
16913
|
+
className = computed(() => this.addon().className ?? null, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
16914
|
+
/**
|
|
16915
|
+
* Locally-resolved `hidden` signal — always reflects `addon.hidden`
|
|
16916
|
+
* regardless of whether the host supplied a pre-resolved signal.
|
|
16917
|
+
* Populated via `explicitEffect` so `resolveDynamicValue` (which may call
|
|
16918
|
+
* `toSignal` for Observable-typed values) does NOT run inside a reactive
|
|
16919
|
+
* context (NG0602). The WeakMap caches per `hidden`-value identity, so
|
|
16920
|
+
* swapping `addon` inputs that share the same Observable reuses the
|
|
16921
|
+
* subscription instead of leaking a fresh one per swap.
|
|
16922
|
+
*
|
|
16923
|
+
* Resolved unconditionally (not gated on host `hidden()` presence) so a
|
|
16924
|
+
* transition from `host hidden = Signal` → `undefined` doesn't expose a
|
|
16925
|
+
* brief window where neither value is current — the upstream effect
|
|
16926
|
+
* re-runs eventually, but the computed read can happen in between.
|
|
16927
|
+
*/
|
|
16928
|
+
_hiddenByValue = new WeakMap();
|
|
16929
|
+
_resolvedHidden = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedHidden" }] : /* istanbul ignore next */ []));
|
|
16930
|
+
/**
|
|
16931
|
+
* `hidden` resolved from `DynamicValue<boolean>` to a flat `Signal<boolean>`.
|
|
16932
|
+
* Prefers the host-supplied signal when present (avoids duplicate `toSignal`
|
|
16933
|
+
* subscriptions for Observable-typed `hidden`); falls back to the locally-
|
|
16934
|
+
* resolved signal, which is always populated by the addon-tracking effect.
|
|
16935
|
+
*/
|
|
16936
|
+
isHidden = computed(() => {
|
|
16937
|
+
const pre = this.hidden();
|
|
16938
|
+
if (pre)
|
|
16939
|
+
return pre();
|
|
16940
|
+
const local = this._resolvedHidden();
|
|
16941
|
+
return local ? local() : false;
|
|
16942
|
+
}, ...(ngDevMode ? [{ debugName: "isHidden" }] : /* istanbul ignore next */ []));
|
|
16943
|
+
/**
|
|
16944
|
+
* Latest async-loaded `{ kind, component }` pair. Tagging the result with
|
|
16945
|
+
* its kind lets `resolvedComponent` filter stale loads structurally — no
|
|
16946
|
+
* monotonic token needed. When `addon.kind` changes mid-load, the prior
|
|
16947
|
+
* load still completes and writes here, but the computed ignores it
|
|
16948
|
+
* because the recorded kind no longer matches the current one.
|
|
16949
|
+
*/
|
|
16950
|
+
asyncLoaded = signal(undefined, ...(ngDevMode ? [{ debugName: "asyncLoaded" }] : /* istanbul ignore next */ []));
|
|
16951
|
+
/**
|
|
16952
|
+
* Resolved kind component — composed declaratively from two sources of
|
|
16953
|
+
* truth: the registry's synchronous cache (warm hits, e.g., a second
|
|
16954
|
+
* `<df-addon-slot>` for an already-loaded kind) and the kind-tagged async
|
|
16955
|
+
* loader result. Reading `addon().kind` first makes the computed react to
|
|
16956
|
+
* addon swaps and naturally invalidate stale async-load results.
|
|
16957
|
+
*/
|
|
16958
|
+
resolvedComponent = computed(() => {
|
|
16959
|
+
const kind = this.addon().kind;
|
|
16960
|
+
const cached = this.registry.getLoadedKindComponent(kind);
|
|
16961
|
+
if (cached)
|
|
16962
|
+
return cached;
|
|
16963
|
+
const loaded = this.asyncLoaded();
|
|
16964
|
+
return loaded?.kind === kind ? loaded.component : undefined;
|
|
16965
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedComponent" }] : /* istanbul ignore next */ []));
|
|
16966
|
+
/** Inputs passed to the kind component — addon plus the wrapper-style host bag. */
|
|
16967
|
+
kindInputs = computed(() => ({
|
|
16968
|
+
addon: this.addon(),
|
|
16969
|
+
fieldInputs: this.fieldInputs(),
|
|
16970
|
+
}), ...(ngDevMode ? [{ debugName: "kindInputs" }] : /* istanbul ignore next */ []));
|
|
16971
|
+
constructor() {
|
|
16972
|
+
// Resolve `addon.hidden` outside any reactive context AND unconditionally —
|
|
16973
|
+
// the host-supplied signal is a perf hint, not a precondition. Keeping
|
|
16974
|
+
// `_resolvedHidden` always-fresh closes a race where `isHidden` could
|
|
16975
|
+
// return `false` between a host-`hidden` clear and the effect re-running.
|
|
16976
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
16977
|
+
const h = addon.hidden;
|
|
16978
|
+
const valueKey = typeof h === 'object' && h !== null ? h : null;
|
|
16979
|
+
let resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
|
|
16980
|
+
if (!resolved) {
|
|
16981
|
+
resolved = resolveDynamicValue(addon.hidden, false, this.hostInjector);
|
|
16982
|
+
if (valueKey)
|
|
16983
|
+
this._hiddenByValue.set(valueKey, resolved);
|
|
16984
|
+
}
|
|
16985
|
+
this._resolvedHidden.set(resolved);
|
|
16986
|
+
});
|
|
16987
|
+
// Kick off async loads on cache miss. `resolvedComponent` filters stale
|
|
16988
|
+
// results by kind identity, so this effect is a pure side-effect dispatcher.
|
|
16989
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
16990
|
+
const kind = addon.kind;
|
|
16991
|
+
if (this.registry.getLoadedKindComponent(kind))
|
|
16992
|
+
return; // sync path; computed picks it up
|
|
16993
|
+
this.registry
|
|
16994
|
+
.loadKindComponent(kind)
|
|
16995
|
+
.then((component) => this.asyncLoaded.set({ kind, component }))
|
|
16996
|
+
.catch((error) => {
|
|
16997
|
+
this.logger.warn(`Failed to load addon kind '${kind}': ${error instanceof Error ? error.message : String(error)}. ` +
|
|
16998
|
+
`Registered kinds: ${this.registry.getKindNames().join(', ') || '(none)'}.`);
|
|
16999
|
+
});
|
|
17000
|
+
});
|
|
17001
|
+
}
|
|
17002
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfAddonSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17003
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DfAddonSlot, isStandalone: true, selector: "df-addon-slot", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.slot": "slotAttr()", "class": "className()", "style.display": "isHidden() ? \"none\" : null" } }, ngImport: i0, template: `
|
|
17004
|
+
@if (resolvedComponent(); as cmp) {
|
|
17005
|
+
<ng-container *ngComponentOutlet="cmp; inputs: kindInputs()" />
|
|
17006
|
+
}
|
|
17007
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17008
|
+
}
|
|
17009
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DfAddonSlot, decorators: [{
|
|
17010
|
+
type: Component,
|
|
17011
|
+
args: [{
|
|
17012
|
+
selector: 'df-addon-slot',
|
|
17013
|
+
imports: [NgComponentOutlet],
|
|
17014
|
+
template: `
|
|
17015
|
+
@if (resolvedComponent(); as cmp) {
|
|
17016
|
+
<ng-container *ngComponentOutlet="cmp; inputs: kindInputs()" />
|
|
17017
|
+
}
|
|
17018
|
+
`,
|
|
17019
|
+
host: {
|
|
17020
|
+
'[attr.slot]': 'slotAttr()',
|
|
17021
|
+
'[class]': 'className()',
|
|
17022
|
+
'[style.display]': 'isHidden() ? "none" : null',
|
|
17023
|
+
},
|
|
17024
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17025
|
+
}]
|
|
17026
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
|
|
17027
|
+
|
|
17028
|
+
/**
|
|
17029
|
+
* Pipe that handles dynamic text resolution with support for static strings,
|
|
17030
|
+
* Observables, and Signals.
|
|
17031
|
+
*
|
|
17032
|
+
* Supports:
|
|
17033
|
+
* - Static strings (pass-through)
|
|
17034
|
+
* - Observables (subscribed internally)
|
|
17035
|
+
* - Signals (converted to Observable using toObservable for reactivity)
|
|
17036
|
+
*
|
|
17037
|
+
* @example
|
|
17038
|
+
* ```html
|
|
17039
|
+
* <!-- Static string -->
|
|
17040
|
+
* {{ 'Hello World' | dynamicText | async }}
|
|
17041
|
+
*
|
|
17042
|
+
* <!-- Observable -->
|
|
17043
|
+
* {{ transloco.selectTranslate('key') | dynamicText | async }}
|
|
17044
|
+
*
|
|
17045
|
+
* <!-- Signal -->
|
|
17046
|
+
* {{ myTextSignal | dynamicText | async }}
|
|
17047
|
+
* ```
|
|
17048
|
+
*
|
|
17049
|
+
* @public
|
|
17050
|
+
*/
|
|
17051
|
+
class DynamicTextPipe {
|
|
17052
|
+
injector = inject(Injector);
|
|
17053
|
+
/**
|
|
17054
|
+
* Transforms dynamic text input into a resolved string value
|
|
17055
|
+
*
|
|
17056
|
+
* @param value - The dynamic text value to resolve
|
|
17057
|
+
* @returns The resolved string value as an Observable
|
|
17058
|
+
*/
|
|
17059
|
+
transform(value) {
|
|
17060
|
+
if (isObservable(value)) {
|
|
17061
|
+
return value;
|
|
17062
|
+
}
|
|
17063
|
+
if (isSignal(value)) {
|
|
17064
|
+
return toObservable(value, { injector: this.injector });
|
|
17065
|
+
}
|
|
17066
|
+
return of(value || '');
|
|
17067
|
+
}
|
|
17068
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
17069
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
17070
|
+
}
|
|
17071
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
17072
|
+
type: Pipe,
|
|
17073
|
+
args: [{
|
|
17074
|
+
name: 'dynamicText',
|
|
17075
|
+
}]
|
|
17076
|
+
}] });
|
|
17077
|
+
|
|
17078
|
+
/**
|
|
17079
|
+
* Renderer for the universal `text` addon kind.
|
|
17080
|
+
*
|
|
17081
|
+
* Renders the addon's `text` (a `DynamicText`) inside a span. Decorative —
|
|
17082
|
+
* sets `aria-hidden="true"` on the host so screen readers skip it; users
|
|
17083
|
+
* who need announcement should use a `*-button` kind with `ariaLabel` instead.
|
|
17084
|
+
*/
|
|
17085
|
+
class TextAddonComponent {
|
|
17086
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17087
|
+
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
|
|
17088
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17089
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17090
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: TextAddonComponent, isStandalone: true, selector: "df-text-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "\"true\"" } }, ngImport: i0, template: `<span>{{ addon().text | dynamicText | async }}</span>`, isInline: true, dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17091
|
+
}
|
|
17092
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextAddonComponent, decorators: [{
|
|
17093
|
+
type: Component,
|
|
17094
|
+
args: [{
|
|
17095
|
+
selector: 'df-text-addon',
|
|
17096
|
+
imports: [AsyncPipe, DynamicTextPipe],
|
|
17097
|
+
template: `<span>{{ addon().text | dynamicText | async }}</span>`,
|
|
17098
|
+
host: {
|
|
17099
|
+
'[attr.aria-hidden]': '"true"',
|
|
17100
|
+
},
|
|
17101
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17102
|
+
}]
|
|
17103
|
+
}], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17104
|
+
|
|
17105
|
+
var textAddon_component = /*#__PURE__*/Object.freeze({
|
|
17106
|
+
__proto__: null,
|
|
17107
|
+
TextAddonComponent: TextAddonComponent
|
|
17108
|
+
});
|
|
17109
|
+
|
|
17110
|
+
/**
|
|
17111
|
+
* Renderer for the universal `template` addon kind.
|
|
17112
|
+
*
|
|
17113
|
+
* Resolves the addon's `templateKey` against {@link DF_FIELD_TEMPLATES}
|
|
17114
|
+
* (populated by `<df-dynamic-form>` from `<ng-template dfTemplate="...">`
|
|
17115
|
+
* children) and renders via `NgTemplateOutlet`. The wrapper-style host bag
|
|
17116
|
+
* (`fieldInputs`) is forwarded as the template's implicit context — author
|
|
17117
|
+
* the template as `<ng-template dfTemplate="..." let-fi>{{ fi.key }}</ng-template>`.
|
|
17118
|
+
*
|
|
17119
|
+
* If the key is unresolved, logs a warning and renders nothing — keeps the
|
|
17120
|
+
* form alive when a backend references a template the FE has not registered.
|
|
17121
|
+
*/
|
|
17122
|
+
class TemplateAddonComponent {
|
|
17123
|
+
templates = inject(DF_FIELD_TEMPLATES, { optional: true });
|
|
17124
|
+
logger = inject(DynamicFormLogger);
|
|
17125
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17126
|
+
/** Forwarded by `df-addon-slot`; surfaced to the template as its implicit `$implicit` context. */
|
|
17127
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17128
|
+
// Pure computed: pure lookup, no side effects. Unresolved → null; the
|
|
17129
|
+
// `@if` in the template skips the outlet and renders nothing.
|
|
17130
|
+
template = computed(() => {
|
|
17131
|
+
const map = this.templates?.();
|
|
17132
|
+
const key = this.addon().templateKey;
|
|
17133
|
+
return map?.get(key) ?? null;
|
|
17134
|
+
}, ...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
|
|
17135
|
+
/** Implicit context payload — template authors bind via `let-fi`. */
|
|
17136
|
+
outletContext = computed(() => ({ $implicit: this.fieldInputs() }), ...(ngDevMode ? [{ debugName: "outletContext" }] : /* istanbul ignore next */ []));
|
|
17137
|
+
constructor() {
|
|
17138
|
+
// Logging side effect lives in an effect, not the computed — keeps
|
|
17139
|
+
// the computed pure so signal-graph evaluations don't duplicate warnings.
|
|
17140
|
+
explicitEffect([this.template, this.addon], ([tpl, addon]) => {
|
|
17141
|
+
if (!tpl) {
|
|
17142
|
+
this.logger.warn(`Template addon: no <ng-template dfTemplate="${addon.templateKey}"> found. Did you project it as a child of <df-dynamic-form>?`);
|
|
17143
|
+
}
|
|
17144
|
+
});
|
|
17145
|
+
}
|
|
17146
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TemplateAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17147
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TemplateAddonComponent, isStandalone: true, selector: "df-template-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
17148
|
+
@if (template(); as tpl) {
|
|
17149
|
+
<ng-container *ngTemplateOutlet="tpl; context: outletContext()" />
|
|
17150
|
+
}
|
|
17151
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17152
|
+
}
|
|
17153
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TemplateAddonComponent, decorators: [{
|
|
17154
|
+
type: Component,
|
|
17155
|
+
args: [{
|
|
17156
|
+
selector: 'df-template-addon',
|
|
17157
|
+
imports: [NgTemplateOutlet],
|
|
17158
|
+
template: `
|
|
17159
|
+
@if (template(); as tpl) {
|
|
17160
|
+
<ng-container *ngTemplateOutlet="tpl; context: outletContext()" />
|
|
17161
|
+
}
|
|
17162
|
+
`,
|
|
17163
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17164
|
+
}]
|
|
17165
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17166
|
+
|
|
17167
|
+
var templateAddon_component = /*#__PURE__*/Object.freeze({
|
|
17168
|
+
__proto__: null,
|
|
17169
|
+
TemplateAddonComponent: TemplateAddonComponent
|
|
17170
|
+
});
|
|
17171
|
+
|
|
17172
|
+
/**
|
|
17173
|
+
* Renderer for the universal `component` addon kind.
|
|
17174
|
+
*
|
|
17175
|
+
* Loads the user-supplied component lazily (`addon.component()`) and renders
|
|
17176
|
+
* it via `NgComponentOutlet` with `addon.inputs` forwarded as input bindings.
|
|
17177
|
+
* The wrapper-style `fieldInputs` bag is not propagated — user components
|
|
17178
|
+
* receive only what `addon.inputs` declares. Authors that need field state
|
|
17179
|
+
* should switch to the `template` kind or build a wrapper.
|
|
17180
|
+
*
|
|
17181
|
+
* @codeOnly The `component` loader is a function; this kind is dropped in
|
|
17182
|
+
* lenient validation when the config originated from JSON.
|
|
17183
|
+
*/
|
|
17184
|
+
class ComponentAddonComponent {
|
|
17185
|
+
logger = inject(DynamicFormLogger);
|
|
17186
|
+
addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
|
|
17187
|
+
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
|
|
17188
|
+
fieldInputs = input(...(ngDevMode ? [undefined, { debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
17189
|
+
inputs = computed(() => (this.addon().inputs ?? {}), ...(ngDevMode ? [{ debugName: "inputs" }] : /* istanbul ignore next */ []));
|
|
17190
|
+
/**
|
|
17191
|
+
* Latest async-loaded `{ addon, component }` pair. Tagging the result with
|
|
17192
|
+
* its source addon lets `resolvedComponent` filter stale loads structurally
|
|
17193
|
+
* — same kind-tagging pattern `DfAddonSlot` uses. When `addon` swaps
|
|
17194
|
+
* mid-load, the prior loader still completes and writes here, but the
|
|
17195
|
+
* computed ignores it because the recorded addon no longer matches the
|
|
17196
|
+
* current one.
|
|
17197
|
+
*/
|
|
17198
|
+
asyncLoaded = signal(undefined, ...(ngDevMode ? [{ debugName: "asyncLoaded" }] : /* istanbul ignore next */ []));
|
|
17199
|
+
resolvedComponent = computed(() => {
|
|
17200
|
+
const current = this.addon();
|
|
17201
|
+
const loaded = this.asyncLoaded();
|
|
17202
|
+
return loaded?.addon === current ? loaded.component : undefined;
|
|
17203
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedComponent" }] : /* istanbul ignore next */ []));
|
|
17204
|
+
constructor() {
|
|
17205
|
+
explicitEffect([this.addon], ([addon]) => {
|
|
17206
|
+
Promise.resolve(addon.component())
|
|
17207
|
+
.then((mod) => {
|
|
17208
|
+
const component = resolveDefaultExport(mod);
|
|
17209
|
+
if (component)
|
|
17210
|
+
this.asyncLoaded.set({ addon, component });
|
|
17211
|
+
})
|
|
17212
|
+
.catch((error) => {
|
|
17213
|
+
this.logger.warn(`Failed to load component addon (slot: '${addon.slot}'): ${error instanceof Error ? error.message : String(error)}`);
|
|
17214
|
+
});
|
|
17215
|
+
});
|
|
17216
|
+
}
|
|
17217
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentAddonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17218
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ComponentAddonComponent, isStandalone: true, selector: "df-component-addon", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
17219
|
+
@if (resolvedComponent(); as cmp) {
|
|
17220
|
+
<ng-container *ngComponentOutlet="cmp; inputs: inputs()" />
|
|
17221
|
+
}
|
|
17222
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17223
|
+
}
|
|
17224
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentAddonComponent, decorators: [{
|
|
17225
|
+
type: Component,
|
|
17226
|
+
args: [{
|
|
17227
|
+
selector: 'df-component-addon',
|
|
17228
|
+
imports: [NgComponentOutlet],
|
|
17229
|
+
template: `
|
|
17230
|
+
@if (resolvedComponent(); as cmp) {
|
|
17231
|
+
<ng-container *ngComponentOutlet="cmp; inputs: inputs()" />
|
|
17232
|
+
}
|
|
17233
|
+
`,
|
|
17234
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17235
|
+
}]
|
|
17236
|
+
}], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
|
|
17237
|
+
|
|
17238
|
+
var componentAddon_component = /*#__PURE__*/Object.freeze({
|
|
17239
|
+
__proto__: null,
|
|
17240
|
+
ComponentAddonComponent: ComponentAddonComponent
|
|
17241
|
+
});
|
|
17242
|
+
|
|
17243
|
+
/**
|
|
17244
|
+
* Apply a built-in preset action against the addon's host field. Shared
|
|
17245
|
+
* across all adapters — each adapter's `ADDON_PRESET_HANDLER` provider
|
|
17246
|
+
* delegates here.
|
|
17247
|
+
*
|
|
17248
|
+
* Presets that require adapter-specific UI mechanics (clipboard, type
|
|
17249
|
+
* override) are best-effort — they no-op with a warning when the necessary
|
|
17250
|
+
* collaborator isn't available (e.g., toggle-password-visibility outside an
|
|
17251
|
+
* input field).
|
|
17252
|
+
*
|
|
17253
|
+
* @param adapterLabel Used in warnings — `'Material'` / `'Bootstrap'` / etc.
|
|
17254
|
+
* @param fieldLabel Used in warnings — `'mat-input'` / `'bs-input'` / etc.
|
|
17255
|
+
*/
|
|
17256
|
+
async function runPresetAction(preset, ctx, collaborators, adapterLabel, fieldLabel) {
|
|
17257
|
+
// Guard against orphan dispatches — none of the built-in presets do
|
|
17258
|
+
// anything useful without a host field, except `copy` and `paste` which
|
|
17259
|
+
// only need the clipboard API. The canonical discriminant per the
|
|
17260
|
+
// `AddonActionContext` discriminated union is `ctx.form` — non-null on
|
|
17261
|
+
// the field-bound variant, null on the orphan variant. `field.key` was
|
|
17262
|
+
// unreliable for nested-array scenarios where keys can legitimately be
|
|
17263
|
+
// empty strings.
|
|
17264
|
+
if (ctx.form === null && preset !== 'copy' && preset !== 'paste') {
|
|
17265
|
+
collaborators.logger.warn(`preset '${String(preset)}' fired without a host field context — ignoring.`);
|
|
17266
|
+
return;
|
|
17267
|
+
}
|
|
17268
|
+
switch (preset) {
|
|
17269
|
+
case 'clear': {
|
|
17270
|
+
// Preserve type semantics: strings clear to `''` (browser-friendly for
|
|
17271
|
+
// input.value), everything else clears to `undefined` (numeric / object
|
|
17272
|
+
// / date fields would otherwise become a string and break their type).
|
|
17273
|
+
const next = typeof ctx.value === 'string' ? '' : undefined;
|
|
17274
|
+
collaborators.fieldValueSetter?.(next);
|
|
17275
|
+
return;
|
|
17276
|
+
}
|
|
17277
|
+
case 'reset': {
|
|
17278
|
+
const defaultValue = collaborators.fieldDefaultValueGetter?.();
|
|
17279
|
+
collaborators.fieldValueSetter?.(defaultValue !== undefined ? defaultValue : typeof ctx.value === 'string' ? '' : undefined);
|
|
17280
|
+
return;
|
|
17281
|
+
}
|
|
17282
|
+
case 'paste': {
|
|
17283
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
17284
|
+
collaborators.logger.warn(`preset 'paste' requires a browser clipboard API (not available in this environment).`);
|
|
17285
|
+
return;
|
|
17286
|
+
}
|
|
17287
|
+
try {
|
|
17288
|
+
const text = await navigator.clipboard.readText();
|
|
17289
|
+
collaborators.fieldValueSetter?.(text);
|
|
17290
|
+
}
|
|
17291
|
+
catch (error) {
|
|
17292
|
+
collaborators.logger.warn(`preset 'paste' clipboard read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
17293
|
+
}
|
|
17294
|
+
return;
|
|
17295
|
+
}
|
|
17296
|
+
case 'copy': {
|
|
17297
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
17298
|
+
collaborators.logger.warn(`preset 'copy' requires a browser clipboard API (not available in this environment).`);
|
|
17299
|
+
return;
|
|
17300
|
+
}
|
|
17301
|
+
try {
|
|
17302
|
+
// Explicit null check — `String(null) === 'null'` would write the
|
|
17303
|
+
// literal string "null" to the clipboard.
|
|
17304
|
+
const v = ctx.value;
|
|
17305
|
+
await navigator.clipboard.writeText(v == null ? '' : String(v));
|
|
17306
|
+
}
|
|
17307
|
+
catch (error) {
|
|
17308
|
+
collaborators.logger.warn(`preset 'copy' clipboard write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
17309
|
+
}
|
|
17310
|
+
return;
|
|
17311
|
+
}
|
|
17312
|
+
case 'toggle-password-visibility': {
|
|
17313
|
+
const override = collaborators.typeOverride;
|
|
17314
|
+
if (!override) {
|
|
17315
|
+
collaborators.logger.warn(`preset 'toggle-password-visibility' has no effect outside a '${fieldLabel}' field.`);
|
|
17316
|
+
return;
|
|
17317
|
+
}
|
|
17318
|
+
// Refuse to flip non-password inputs. Re-check on EVERY dispatch (not
|
|
17319
|
+
// just when override is undefined) because `props().type` can change
|
|
17320
|
+
// reactively after the first toggle — e.g., 'password' → 'email'. Once
|
|
17321
|
+
// the override is touched, only the override is canonical for the
|
|
17322
|
+
// current visible/hidden state; the baseline still gates whether
|
|
17323
|
+
// toggling makes semantic sense for this field.
|
|
17324
|
+
const baseline = collaborators.baselineType?.();
|
|
17325
|
+
if (baseline !== undefined && baseline !== 'password') {
|
|
17326
|
+
collaborators.logger.warn(`preset 'toggle-password-visibility' refused: host '${fieldLabel}' has type '${baseline}', not 'password'.`);
|
|
17327
|
+
return;
|
|
17328
|
+
}
|
|
17329
|
+
const current = override();
|
|
17330
|
+
override.set(current === 'text' ? 'password' : 'text');
|
|
17331
|
+
return;
|
|
17332
|
+
}
|
|
17333
|
+
default:
|
|
17334
|
+
collaborators.logger.warn(`preset '${String(preset)}' is not implemented in the ${adapterLabel} adapter.`);
|
|
17335
|
+
}
|
|
17336
|
+
}
|
|
17337
|
+
|
|
17338
|
+
/**
|
|
17339
|
+
* Walk `FIELD_REGISTRY` and return every field type that opted into addons
|
|
17340
|
+
* (i.e., declared `addons` on its `FieldTypeDefinition`).
|
|
17341
|
+
*
|
|
17342
|
+
* Useful for tooling — admin UIs surfacing "which fields can carry addons?",
|
|
17343
|
+
* docs generators, MCP server cross-checks. Field types without an `addons`
|
|
17344
|
+
* registration ("Tier 3" — toggle, checkbox, radio, slider) are excluded.
|
|
17345
|
+
*
|
|
17346
|
+
* Must be called within an injection context.
|
|
17347
|
+
*
|
|
17348
|
+
* @example
|
|
17349
|
+
* ```typescript
|
|
17350
|
+
* @Component({ ... })
|
|
17351
|
+
* class FormBuilder {
|
|
17352
|
+
* protected readonly addonable = injectFieldsSupportingAddons();
|
|
17353
|
+
*
|
|
17354
|
+
* constructor() {
|
|
17355
|
+
* // -> [{ name: 'prime-input', slots: ['prefix', 'suffix'], allowedKinds: [...] }, ...]
|
|
17356
|
+
* }
|
|
17357
|
+
* }
|
|
17358
|
+
* ```
|
|
17359
|
+
*/
|
|
17360
|
+
function injectFieldsSupportingAddons() {
|
|
17361
|
+
const registry = inject(FIELD_REGISTRY);
|
|
17362
|
+
const entries = [];
|
|
17363
|
+
for (const [name, def] of registry.entries()) {
|
|
17364
|
+
if (def.addons === undefined)
|
|
17365
|
+
continue;
|
|
17366
|
+
entries.push({ name, slots: def.addons.slots, allowedKinds: def.addons.allowedKinds });
|
|
17367
|
+
}
|
|
17368
|
+
return entries;
|
|
17369
|
+
}
|
|
17370
|
+
|
|
17371
|
+
/**
|
|
17372
|
+
* Register a custom addon kind with the form.
|
|
17373
|
+
*
|
|
17374
|
+
* The provided definition is added to `ADDON_KIND_REGISTRY` at form
|
|
17375
|
+
* initialisation, alongside the built-in `text` / `template` / `component`
|
|
17376
|
+
* kinds and any kinds contributed by adapter feature helpers
|
|
17377
|
+
* (`withPrimeNGAddons()` etc.).
|
|
17378
|
+
*
|
|
17379
|
+
* @example
|
|
17380
|
+
* ```typescript
|
|
17381
|
+
* provideDynamicForm(
|
|
17382
|
+
* ...withMaterialFields(),
|
|
17383
|
+
* withCustomAddon({
|
|
17384
|
+
* kind: 'lucide-icon',
|
|
17385
|
+
* // Match how field types register: a default-exported component lets
|
|
17386
|
+
* // `loadComponent` return the import promise directly.
|
|
17387
|
+
* loadComponent: () => import('./lucide-icon-addon.component'),
|
|
17388
|
+
* validate: (addon) => {
|
|
17389
|
+
* if (typeof addon.icon !== 'string') {
|
|
17390
|
+
* throw new Error('lucide-icon: icon string is required');
|
|
17391
|
+
* }
|
|
17392
|
+
* },
|
|
17393
|
+
* }),
|
|
17394
|
+
* );
|
|
17395
|
+
* ```
|
|
17396
|
+
*
|
|
17397
|
+
* Multiple `withCustomAddon(...)` calls are supported; later registrations
|
|
17398
|
+
* for the same kind override earlier ones (with a warning logged).
|
|
17399
|
+
*/
|
|
17400
|
+
function withCustomAddon(definition) {
|
|
17401
|
+
return createFeature('addons', [
|
|
17402
|
+
{
|
|
17403
|
+
provide: ADDON_KIND_DEFINITIONS,
|
|
17404
|
+
useValue: definition,
|
|
17405
|
+
multi: true,
|
|
17406
|
+
},
|
|
17407
|
+
]);
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17410
|
+
/**
|
|
17411
|
+
* Register named action handlers reachable via JSON-driven addon configs.
|
|
17412
|
+
*
|
|
17413
|
+
* Once registered, a backend can ship
|
|
17414
|
+
* `{ kind: 'prime-button', actionRef: 'openProfile' }` and the FE will invoke
|
|
17415
|
+
* the corresponding handler at click time.
|
|
17416
|
+
*
|
|
17417
|
+
* Define handlers once and augment the registry from the same object:
|
|
17418
|
+
*
|
|
17419
|
+
* ```typescript
|
|
17420
|
+
* export const appActions = provideAddonActions({
|
|
17421
|
+
* openProfile: (ctx) => modalService.open(ProfileModal, { data: ctx.value }),
|
|
17422
|
+
* runWorkflow: (ctx) => workflowService.run(ctx.field.key, ctx.value),
|
|
17423
|
+
* });
|
|
17424
|
+
*
|
|
17425
|
+
* declare module '@ng-forge/dynamic-forms' {
|
|
17426
|
+
* // Pulls the keys straight off `appActions` — no manual list to keep in sync.
|
|
17427
|
+
* interface DynamicFormActionRegistry extends Record<NonNullable<typeof appActions['__handlerKeys']>, true> {}
|
|
17428
|
+
* }
|
|
17429
|
+
*
|
|
17430
|
+
* // Then in your bootstrap:
|
|
17431
|
+
* provideDynamicForm(...withPrimeNGFields(), appActions);
|
|
17432
|
+
* ```
|
|
17433
|
+
*
|
|
17434
|
+
* Multiple `provideAddonActions(...)` calls are merged; later names override
|
|
17435
|
+
* earlier ones for the same key.
|
|
17436
|
+
*/
|
|
17437
|
+
function provideAddonActions(handlers) {
|
|
17438
|
+
return createFeature('addon-actions', [
|
|
17439
|
+
{
|
|
17440
|
+
provide: ADDON_ACTION_HANDLERS,
|
|
17441
|
+
useValue: handlers,
|
|
17442
|
+
multi: true,
|
|
17443
|
+
},
|
|
17444
|
+
]);
|
|
17445
|
+
}
|
|
17446
|
+
|
|
17447
|
+
/**
|
|
17448
|
+
* Walk a `FormConfig`, validate every field's addons against the active
|
|
17449
|
+
* `FIELD_REGISTRY` and `ADDON_KIND_REGISTRY`, and return a sanitized copy
|
|
17450
|
+
* plus a list of dropped-addon warnings.
|
|
17451
|
+
*
|
|
17452
|
+
* Distinct from the INTERNAL `validateFormConfig` in
|
|
17453
|
+
* `utils/config-validation/config-validator.ts` (which throws on
|
|
17454
|
+
* structural errors like duplicate keys / unknown field types). This
|
|
17455
|
+
* helper is the PUBLIC, lenient addon-shape pass — renamed to
|
|
17456
|
+
* `sanitizeFormConfig` to avoid the name collision.
|
|
17457
|
+
*
|
|
17458
|
+
* Lenient by design: invalid addons are dropped (with warnings) rather than
|
|
17459
|
+
* thrown. The form keeps rendering even when the backend ships an addon the
|
|
17460
|
+
* FE doesn't understand.
|
|
17461
|
+
*
|
|
17462
|
+
* `<df-dynamic-form>` already runs this internally at form init — calling
|
|
17463
|
+
* the public helper AND passing the original config to the form will
|
|
17464
|
+
* double-emit warnings. Use this entry point only when you need the
|
|
17465
|
+
* sanitized config or warning list outside the form (e.g., admin-UI
|
|
17466
|
+
* diagnostics, build-time lints).
|
|
17467
|
+
*
|
|
17468
|
+
* Must be called within an Angular injection context — typically inside
|
|
17469
|
+
* an admin-UI component or a `runInInjectionContext()` block on the server.
|
|
17470
|
+
*
|
|
17471
|
+
* @example
|
|
17472
|
+
* ```typescript
|
|
17473
|
+
* @Component({ ... })
|
|
17474
|
+
* class AdminFormBuilder {
|
|
17475
|
+
* private readonly check = (cfg: FormConfig) => {
|
|
17476
|
+
* const { sanitized, warnings } = sanitizeFormConfig(cfg, { source: 'json' });
|
|
17477
|
+
* if (warnings.length > 0) {
|
|
17478
|
+
* this.diagnostics.set(warnings);
|
|
17479
|
+
* }
|
|
17480
|
+
* return sanitized;
|
|
17481
|
+
* };
|
|
17482
|
+
* }
|
|
17483
|
+
* ```
|
|
17484
|
+
*/
|
|
17485
|
+
function sanitizeFormConfig(config, options = {}) {
|
|
17486
|
+
const fieldRegistry = inject(FIELD_REGISTRY);
|
|
17487
|
+
const kindRegistry = inject(ADDON_KIND_REGISTRY);
|
|
17488
|
+
const source = options.source ?? 'inline';
|
|
17489
|
+
const { fields, warnings } = walkAndValidateAddons((config.fields ?? []), fieldRegistry, kindRegistry, source);
|
|
17490
|
+
return {
|
|
17491
|
+
sanitized: { ...config, fields: fields },
|
|
17492
|
+
warnings,
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
/**
|
|
17496
|
+
* Default warning sink. Pass a `DynamicFormLogger` to route through the
|
|
17497
|
+
* form's configured logger (respects `NoopLogger` opt-out); omit it to
|
|
17498
|
+
* fall back to `console.warn` for build-time / admin-UI use cases that
|
|
17499
|
+
* run outside an Angular injection context.
|
|
17500
|
+
*/
|
|
17501
|
+
function logAddonWarnings(warnings, logger) {
|
|
17502
|
+
for (const w of warnings) {
|
|
17503
|
+
if (logger) {
|
|
17504
|
+
logger.warn(formatAddonWarning(w));
|
|
17505
|
+
}
|
|
17506
|
+
else {
|
|
17507
|
+
console.warn('[Dynamic Forms]', formatAddonWarning(w));
|
|
17508
|
+
}
|
|
17509
|
+
}
|
|
17510
|
+
}
|
|
17511
|
+
/**
|
|
17512
|
+
* DI-free variant of {@link sanitizeFormConfig} for tooling that lives
|
|
17513
|
+
* outside Angular's injector — build-time linters, MCP-side checks, CLI
|
|
17514
|
+
* scripts.
|
|
17515
|
+
*
|
|
17516
|
+
* Caller passes the registries explicitly; the result has the same shape
|
|
17517
|
+
* as the DI-bound version.
|
|
17518
|
+
*/
|
|
17519
|
+
function sanitizeFormConfigPure(config, fieldRegistry, kindRegistry, options = {}) {
|
|
17520
|
+
const source = options.source ?? 'inline';
|
|
17521
|
+
const { fields, warnings } = walkAndValidateAddons((config.fields ?? []), fieldRegistry, kindRegistry, source);
|
|
17522
|
+
return { sanitized: { ...config, fields: fields }, warnings };
|
|
17523
|
+
}
|
|
17524
|
+
|
|
15690
17525
|
function isValueField(field) {
|
|
15691
17526
|
// `nullable: true` without an explicit `value` is still a value-bearing declaration:
|
|
15692
17527
|
// the field resolves to `null` at runtime. Treating it as a value field keeps
|
|
@@ -15695,7 +17530,7 @@ function isValueField(field) {
|
|
|
15695
17530
|
}
|
|
15696
17531
|
|
|
15697
17532
|
function isCheckedField(field) {
|
|
15698
|
-
return field.type === 'checkbox';
|
|
17533
|
+
return field.type === 'checkbox' || field.type === 'toggle';
|
|
15699
17534
|
}
|
|
15700
17535
|
|
|
15701
17536
|
/**
|
|
@@ -16201,56 +18036,6 @@ function applyMetaToElement(element, meta, previouslyApplied) {
|
|
|
16201
18036
|
return newApplied;
|
|
16202
18037
|
}
|
|
16203
18038
|
|
|
16204
|
-
/**
|
|
16205
|
-
* Pipe that handles dynamic text resolution with support for static strings,
|
|
16206
|
-
* Observables, and Signals.
|
|
16207
|
-
*
|
|
16208
|
-
* Supports:
|
|
16209
|
-
* - Static strings (pass-through)
|
|
16210
|
-
* - Observables (subscribed internally)
|
|
16211
|
-
* - Signals (converted to Observable using toObservable for reactivity)
|
|
16212
|
-
*
|
|
16213
|
-
* @example
|
|
16214
|
-
* ```html
|
|
16215
|
-
* <!-- Static string -->
|
|
16216
|
-
* {{ 'Hello World' | dynamicText | async }}
|
|
16217
|
-
*
|
|
16218
|
-
* <!-- Observable -->
|
|
16219
|
-
* {{ transloco.selectTranslate('key') | dynamicText | async }}
|
|
16220
|
-
*
|
|
16221
|
-
* <!-- Signal -->
|
|
16222
|
-
* {{ myTextSignal | dynamicText | async }}
|
|
16223
|
-
* ```
|
|
16224
|
-
*
|
|
16225
|
-
* @public
|
|
16226
|
-
*/
|
|
16227
|
-
class DynamicTextPipe {
|
|
16228
|
-
injector = inject(Injector);
|
|
16229
|
-
/**
|
|
16230
|
-
* Transforms dynamic text input into a resolved string value
|
|
16231
|
-
*
|
|
16232
|
-
* @param value - The dynamic text value to resolve
|
|
16233
|
-
* @returns The resolved string value as an Observable
|
|
16234
|
-
*/
|
|
16235
|
-
transform(value) {
|
|
16236
|
-
if (isObservable(value)) {
|
|
16237
|
-
return value;
|
|
16238
|
-
}
|
|
16239
|
-
if (isSignal(value)) {
|
|
16240
|
-
return toObservable(value, { injector: this.injector });
|
|
16241
|
-
}
|
|
16242
|
-
return of(value || '');
|
|
16243
|
-
}
|
|
16244
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
16245
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
16246
|
-
}
|
|
16247
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
16248
|
-
type: Pipe,
|
|
16249
|
-
args: [{
|
|
16250
|
-
name: 'dynamicText',
|
|
16251
|
-
}]
|
|
16252
|
-
}] });
|
|
16253
|
-
|
|
16254
18039
|
/**
|
|
16255
18040
|
* Interpolates {{param}} placeholders in message with error values
|
|
16256
18041
|
*
|
|
@@ -16382,5 +18167,5 @@ function extractErrorParams(error) {
|
|
|
16382
18167
|
* Generated bundle index. Do not edit.
|
|
16383
18168
|
*/
|
|
16384
18169
|
|
|
16385
|
-
export {
|
|
16386
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
18170
|
+
export { arrayFieldMapper as $, ADDON_ACTION_HANDLERS as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DEFAULT_PROPS as D, EventBus as E, FIELD_REGISTRY as F, GROUP_CONTEXT as G, NoopLogger as H, INITIALIZATION_TIMEOUT_MS as I, PopArrayItemEvent as J, PrependArrayItemEvent as K, PreviousPageEvent as L, MoveArrayItemEvent as M, NextPageEvent as N, RootFormRegistryService as O, PageChangeEvent as P, SubmitEvent as Q, RemoveAtIndexEvent as R, ShiftArrayItemEvent as S, TemplateAddonComponent as T, TextAddonComponent as U, WRAPPER_REGISTRY as V, WRAPPER_AUTO_ASSOCIATIONS as W, applyMetaToElement as X, applyValidator as Y, applyValidators as Z, arrayEvent as _, ADDON_ACTION_REGISTRY as a, textFieldMapper as a$, baseFieldMapper as a0, buildBaseInputs as a1, containerFieldMapper as a2, createField as a3, createFieldResolutionPipe as a4, createHiddenSignal as a5, createRenderReadySignal as a6, createSchemaFromFields as a7, createWrapperChainController as a8, createWrappers as a9, isGroupField as aA, isLeafField as aB, isPageField as aC, isRowField as aD, isSameWrapperChain as aE, isSimplifiedArrayField as aF, isValueBearingField as aG, isValueField as aH, isWrapperTypeDefinition as aI, isWrappersBundle as aJ, logAddonWarnings as aK, mapFieldToInputs as aL, omit as aM, pageFieldMapper as aN, provideAddonActions as aO, provideDynamicForm as aP, resolveDynamicValue as aQ, resolveNextButtonDisabled as aR, resolveNonFieldDisabled as aS, resolveNonFieldHidden as aT, resolveSubmitButtonDisabled as aU, resolveTokens as aV, resolveWrappers as aW, rowFieldMapper as aX, runPresetAction as aY, sanitizeFormConfig as aZ, sanitizeFormConfigPure as a_, derivedFromDeferred as aa, dynamicTextToObservable as ab, evaluateNonFieldDisabled as ac, evaluateNonFieldHidden as ad, field as ae, formConfig as af, formatAddonWarning as ag, getArrayLength as ah, getFieldDefaultValue as ai, getFieldValueHandling as aj, getNormalizedArrayMetadata as ak, groupFieldMapper as al, hasFormValue as am, injectAddonKindRegistry as an, injectFieldRegistry as ao, injectFieldSignalContext as ap, injectFieldsSupportingAddons as aq, interpolateParams as ar, isArrayField as as, isCheckedField as at, isContainerField as au, isContainerTypedField as av, isDisplayOnlyField as aw, isEqual as ax, isFieldBoundContext as ay, isFormStateCondition as az, ADDON_KIND_COMPONENT_CACHE as b, toReadonlyFieldTree as b0, validateFieldAddons as b1, validatePageNesting as b2, walkAndValidateAddons as b3, withCustomAddon as b4, withEventFormValue as b5, withLegacyStatusClasses as b6, withLoggerConfig as b7, withPreviousValue as b8, withValidationExecutionDefaults as b9, withValueExclusionDefaults as ba, wrapperProps as bb, writeToFieldValue as bc, ADDON_KIND_DEFINITIONS as c, ADDON_KIND_REGISTRY as d, ARRAY_CONTEXT as e, AppendArrayItemEvent as f, ArrayItemRegistryService as g, ComponentAddonComponent as h, ComponentInitializedEvent as i, ConsoleLogger as j, DEFAULT_VALIDATION_MESSAGES as k, DEFAULT_WRAPPERS as l, DF_FIELD_TEMPLATES as m, DfAddonSlot as n, DfFieldOutlet as o, DfTemplate as p, DynamicForm as q, DynamicFormError as r, DynamicFormLogger as s, DynamicTextPipe as t, EventDispatcher as u, FIELD_SIGNAL_CONTEXT as v, FORM_OPTIONS as w, FormClearEvent as x, FormResetEvent as y, InsertArrayItemEvent as z };
|
|
18171
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map
|