@ng-forge/dynamic-forms 0.6.1 → 0.8.0-next.1
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 +2 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Civn5kLJ.mjs +50 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Civn5kLJ.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +4 -4
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DXXp5M0q.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-D276M2vh.mjs} +1768 -803
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-D276M2vh.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs → ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs} +21 -21
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +34 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Ael-STTb.mjs → ng-forge-dynamic-forms-text-field.component-9OIByjZZ.mjs} +18 -12
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-9OIByjZZ.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +2 -2
- package/types/ng-forge-dynamic-forms-integration.d.ts +25 -23
- package/types/ng-forge-dynamic-forms.d.ts +3379 -3079
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DXXp5M0q.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-D6M_uJKu.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-Ael-STTb.mjs.map +0 -1
|
@@ -1,128 +1,58 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject,
|
|
3
|
-
import { NgComponentOutlet } from '@angular/common';
|
|
4
|
-
import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
5
|
-
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';
|
|
6
|
-
import { isObservable, firstValueFrom, exhaustMap, EMPTY, from, catchError, Subject, filter, TimeoutError, of, throwError, debounceTime, distinctUntilChanged, tap, take as take$1, startWith, pairwise, map as map$1, switchMap as switchMap$1, Observable, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer, concat, defer, combineLatest, pipe, forkJoin, scan as scan$1, concatMap } from 'rxjs';
|
|
7
|
-
import { scan, map, filter as filter$1, take, switchMap, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
|
|
2
|
+
import { InjectionToken, reflectComponentType, Injector, inject, EnvironmentInjector, DestroyRef, computed, input, ViewContainerRef, signal, Directive, Injectable, isSignal, untracked, isWritableSignal, isDevMode, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, model, makeEnvironmentProviders, Pipe, viewChild, TemplateRef } from '@angular/core';
|
|
8
3
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
4
|
+
import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, debounceTime, distinctUntilChanged, tap, take as take$1, startWith, pairwise, Observable, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer, defer, combineLatest, pipe, scan as scan$1, concatMap } from 'rxjs';
|
|
5
|
+
import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
|
|
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
|
+
import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
|
|
9
8
|
import { HttpClient } from '@angular/common/http';
|
|
10
9
|
import { isStandardSchemaMarker } from '@ng-forge/dynamic-forms/schema';
|
|
11
10
|
import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Converts Observables to Promises for compatibility with Angular Signal Forms' submit().
|
|
16
|
-
*
|
|
17
|
-
* Errors are NOT caught here — they propagate so that submit() can reject its Promise,
|
|
18
|
-
* allowing the caller's catchError to log and keep the submission stream alive.
|
|
13
|
+
* Type guard for WrapperTypeDefinition.
|
|
19
14
|
*
|
|
20
|
-
*
|
|
21
|
-
* @returns A wrapped function that returns a Promise
|
|
15
|
+
* Discriminates via `wrapperName` — field types use `name`, wrapper types use `wrapperName`.
|
|
22
16
|
*/
|
|
23
|
-
function
|
|
24
|
-
return
|
|
25
|
-
const result = action(formTree);
|
|
26
|
-
// If the action returns an Observable, convert it to a Promise
|
|
27
|
-
if (isObservable(result)) {
|
|
28
|
-
await firstValueFrom(result);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
await Promise.resolve(result);
|
|
32
|
-
};
|
|
17
|
+
function isWrapperTypeDefinition(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && 'wrapperName' in value;
|
|
33
19
|
}
|
|
34
20
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* This utility encapsulates the submission handling logic:
|
|
38
|
-
* - Listens for submit events from the event bus
|
|
39
|
-
* - If a submission.action is configured, wraps it and uses Angular Signal Forms' submit()
|
|
40
|
-
* - Handles both Promise and Observable returns from the action
|
|
41
|
-
* - Uses exhaustMap to ignore new submissions while one is in-flight (first-submit-wins)
|
|
42
|
-
*
|
|
43
|
-
* The returned Observable should be subscribed to with takeUntilDestroyed() in the component.
|
|
21
|
+
* Injection token for the wrapper type registry.
|
|
44
22
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```typescript
|
|
50
|
-
* // In component constructor
|
|
51
|
-
* createSubmissionHandler({
|
|
52
|
-
* eventBus: this.eventBus,
|
|
53
|
-
* configSignal: this.config,
|
|
54
|
-
* formSignal: this.form,
|
|
55
|
-
* })
|
|
56
|
-
* .pipe(takeUntilDestroyed())
|
|
57
|
-
* .subscribe();
|
|
58
|
-
* ```
|
|
23
|
+
* Provides access to the map of registered wrapper types. The registry is
|
|
24
|
+
* populated via `provideDynamicForm()` and used by `ContainerFieldComponent` to
|
|
25
|
+
* resolve wrapper types to their component implementations.
|
|
59
26
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// switchMap would unsubscribe the Observable wrapper but cannot cancel the
|
|
65
|
-
// underlying Promise, causing both side effects to execute.
|
|
66
|
-
return eventBus.on('submit').pipe(exhaustMap(() => {
|
|
67
|
-
const submissionConfig = configSignal().submission;
|
|
68
|
-
// If no submission action is configured, let the submitted output handle it
|
|
69
|
-
// This maintains backward compatibility for users handling submission manually
|
|
70
|
-
if (!submissionConfig?.action) {
|
|
71
|
-
return EMPTY;
|
|
72
|
-
}
|
|
73
|
-
// Guard: match the (submitted) output's safety contract — reject submission
|
|
74
|
-
// when the form is invalid or has pending async validators.
|
|
75
|
-
if (!validSignal()) {
|
|
76
|
-
logger.debug('Submission action skipped: form is not valid (invalid or pending async validators)');
|
|
77
|
-
return EMPTY;
|
|
78
|
-
}
|
|
79
|
-
// Type assertion needed: submission.action accepts the form tree but its signature
|
|
80
|
-
// is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.
|
|
81
|
-
const wrappedAction = wrapSubmissionAction(submissionConfig.action);
|
|
82
|
-
// Use Angular Signal Forms' native submit() function
|
|
83
|
-
// This automatically:
|
|
84
|
-
// - Sets form.submitting() to true during execution
|
|
85
|
-
// - Applies server errors to form fields on completion
|
|
86
|
-
// - Sets form.submitting() to false when done
|
|
87
|
-
// catchError keeps the exhaustMap stream alive after action failure —
|
|
88
|
-
// without it, an unhandled error would terminate all future submissions.
|
|
89
|
-
return from(submit(formSignal(), wrappedAction)).pipe(catchError((error) => {
|
|
90
|
-
logger.error('Submission action failed:', error);
|
|
91
|
-
return EMPTY;
|
|
92
|
-
}));
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
|
|
27
|
+
const WRAPPER_REGISTRY = new InjectionToken('WRAPPER_REGISTRY', {
|
|
28
|
+
providedIn: 'root',
|
|
29
|
+
factory: () => new Map(),
|
|
30
|
+
});
|
|
96
31
|
/**
|
|
97
|
-
*
|
|
32
|
+
* Component cache for loaded wrapper components.
|
|
98
33
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* This token is configured via `withEventFormValue()` feature function.
|
|
103
|
-
* Per-form overrides can be set via `options.emitFormValueOnEvents` in the form config.
|
|
34
|
+
* Caches resolved wrapper component classes for instant re-resolution.
|
|
35
|
+
* SSR-safe because it's DI-scoped, not module-scoped.
|
|
104
36
|
*
|
|
105
|
-
*
|
|
37
|
+
* NOTE: The cache grows across the application lifetime. It's bounded by the
|
|
38
|
+
* number of wrapper types registered (typically small), so this is not a leak;
|
|
39
|
+
* the COMPONENT_CACHE for field types has the same shape.
|
|
106
40
|
*/
|
|
107
|
-
const
|
|
41
|
+
const WRAPPER_COMPONENT_CACHE = new InjectionToken('WRAPPER_COMPONENT_CACHE', {
|
|
108
42
|
providedIn: 'root',
|
|
109
|
-
factory: () =>
|
|
43
|
+
factory: () => new Map(),
|
|
110
44
|
});
|
|
111
|
-
|
|
112
45
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
46
|
+
* Pre-computed reverse index: `fieldType → WrapperConfig[]` for every
|
|
47
|
+
* registered `WrapperTypeDefinition.types` entry. Built once in the
|
|
48
|
+
* `provideDynamicForm(...)` factory so `resolveWrappers` can look up
|
|
49
|
+
* auto-associations in O(1) per field instead of scanning the full
|
|
50
|
+
* wrapper registry on every render.
|
|
117
51
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.formValue = formValue;
|
|
123
|
-
this.rootForm = rootForm;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
52
|
+
const WRAPPER_AUTO_ASSOCIATIONS = new InjectionToken('WRAPPER_AUTO_ASSOCIATIONS', {
|
|
53
|
+
providedIn: 'root',
|
|
54
|
+
factory: () => new Map(),
|
|
55
|
+
});
|
|
126
56
|
|
|
127
57
|
/**
|
|
128
58
|
* Base error class for all Dynamic Forms errors.
|
|
@@ -139,6 +69,18 @@ class DynamicFormError extends Error {
|
|
|
139
69
|
}
|
|
140
70
|
}
|
|
141
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Injection token for form-level default wrappers.
|
|
74
|
+
*
|
|
75
|
+
* Provides a Signal of the `defaultWrappers` array from FormConfig. Consumed by
|
|
76
|
+
* `DfFieldOutlet` / `ContainerFieldComponent` to merge into each field's
|
|
77
|
+
* effective wrapper chain (lowest priority after auto-associations, higher
|
|
78
|
+
* than field-level `wrappers`).
|
|
79
|
+
*
|
|
80
|
+
* Provided once at the DynamicForm level and inherited via Angular's
|
|
81
|
+
* hierarchical injector.
|
|
82
|
+
*/
|
|
83
|
+
const DEFAULT_WRAPPERS = new InjectionToken('DEFAULT_WRAPPERS');
|
|
142
84
|
/**
|
|
143
85
|
* Injection token for providing field signal context to mappers and components.
|
|
144
86
|
*
|
|
@@ -345,6 +287,201 @@ function createArrayItemIdGenerator() {
|
|
|
345
287
|
return () => `item-${counter++}`;
|
|
346
288
|
}
|
|
347
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Module-level cache keyed by component class. `reflectComponentType` returns
|
|
292
|
+
* immutable metadata per class, so we probe it once and reuse the resulting
|
|
293
|
+
* `Set<templateName>` for every `setInputIfDeclared` call afterwards.
|
|
294
|
+
*
|
|
295
|
+
* We cache `templateName` (the public input name, aka alias) rather than
|
|
296
|
+
* `propName` (the class field name) because `ComponentRef.setInput()` keys by
|
|
297
|
+
* the public name — that's also the name wrapper configs use. An aliased
|
|
298
|
+
* input declared as `input(..., { alias: 'header' })` with class field
|
|
299
|
+
* `headerText` must be addressed as `'header'`; caching `propName` would
|
|
300
|
+
* leave the alias out of the declared set and silently drop config values.
|
|
301
|
+
*
|
|
302
|
+
* SSR-safe: the WeakMap is keyed by the component class object — classes are
|
|
303
|
+
* created per Angular application bootstrap and GC'd with it, so this does not
|
|
304
|
+
* leak state between server renders.
|
|
305
|
+
*/
|
|
306
|
+
const inputNamesCache = new WeakMap();
|
|
307
|
+
function getDeclaredInputs(componentType) {
|
|
308
|
+
let inputs = inputNamesCache.get(componentType);
|
|
309
|
+
if (!inputs) {
|
|
310
|
+
const meta = reflectComponentType(componentType);
|
|
311
|
+
inputs = new Set(meta?.inputs.map((i) => i.templateName) ?? []);
|
|
312
|
+
inputNamesCache.set(componentType, inputs);
|
|
313
|
+
}
|
|
314
|
+
return inputs;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Set an input on a ComponentRef only when the target component actually declares it.
|
|
318
|
+
*
|
|
319
|
+
* Angular's `ComponentRef.setInput()` throws NG0303 when the input is missing.
|
|
320
|
+
* For config keys driven by user data (e.g. a wrapper config containing a prop
|
|
321
|
+
* the wrapper doesn't care about) that would surface as a noisy runtime error.
|
|
322
|
+
* We probe the component's metadata via `reflectComponentType` (public API),
|
|
323
|
+
* cached per component class — called once per input key per emission, so
|
|
324
|
+
* avoiding the reflection scan on each call is meaningful under heavy typing.
|
|
325
|
+
*/
|
|
326
|
+
function setInputIfDeclared(ref, inputName, value) {
|
|
327
|
+
const declared = getDeclaredInputs(ref.componentType);
|
|
328
|
+
if (declared.has(inputName)) {
|
|
329
|
+
ref.setInput(inputName, value);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Narrows an ES-module-with-default-export from a bare component class.
|
|
334
|
+
* Lazy component loaders may return either shape depending on how the user
|
|
335
|
+
* wrote the import expression (`import('./x')` vs `import('./x').then(m => m.Foo)`).
|
|
336
|
+
*/
|
|
337
|
+
function hasDefaultExport(value) {
|
|
338
|
+
return typeof value === 'object' && value !== null && 'default' in value && !!value.default;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Pick the component class out of whatever a lazy loader returned.
|
|
342
|
+
*/
|
|
343
|
+
function resolveDefaultExport(result) {
|
|
344
|
+
return hasDefaultExport(result) ? result.default : result;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a wrapper type name to its component class, with DI-scoped caching.
|
|
348
|
+
*/
|
|
349
|
+
async function loadWrapperComponent(type, registry, cache) {
|
|
350
|
+
const cached = cache.get(type);
|
|
351
|
+
if (cached)
|
|
352
|
+
return cached;
|
|
353
|
+
const definition = registry.get(type);
|
|
354
|
+
if (!definition)
|
|
355
|
+
return undefined;
|
|
356
|
+
const component = resolveDefaultExport(await definition.loadComponent());
|
|
357
|
+
if (component)
|
|
358
|
+
cache.set(type, component);
|
|
359
|
+
return component;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Load every wrapper for a chain, all-or-nothing. A single failed load
|
|
363
|
+
* aborts the whole chain (emits `[]`) so the field renders bare rather
|
|
364
|
+
* than in a partially-wrapped, visually-misleading state. Each failure
|
|
365
|
+
* is logged; field-type resolution still throws — this isn't silent.
|
|
366
|
+
*/
|
|
367
|
+
function loadWrapperComponents(configs, registry, cache, logger) {
|
|
368
|
+
if (configs.length === 0)
|
|
369
|
+
return of([]);
|
|
370
|
+
return forkJoin(configs.map((config) => from(loadWrapperComponent(config.type, registry, cache)).pipe(catchError(() => of(undefined)), map((component) => {
|
|
371
|
+
if (!component) {
|
|
372
|
+
logger.error(`Wrapper type '${config.type}' could not be loaded. Ensure it is registered via provideDynamicForm().`);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return { config, component };
|
|
376
|
+
})))).pipe(map((results) => (results.some((r) => r === null) ? [] : results)));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Recursively create each wrapper component, threading the next one (or the
|
|
380
|
+
* innermost content) into its `#fieldComponent` slot.
|
|
381
|
+
*
|
|
382
|
+
* Each wrapper receives:
|
|
383
|
+
* - Each of its config properties (minus `type`) as an individual Angular input
|
|
384
|
+
* - `fieldInputs` as a single input, if provided
|
|
385
|
+
*
|
|
386
|
+
* Returns the list of wrapper `ComponentRef`s — callers retain them for later
|
|
387
|
+
* cleanup and for re-setting `fieldInputs` when the mapper signal emits.
|
|
388
|
+
*/
|
|
389
|
+
function renderWrapperChain(options) {
|
|
390
|
+
const refs = [];
|
|
391
|
+
renderStep(options.outerContainer, options.loadedWrappers, options, refs);
|
|
392
|
+
return refs;
|
|
393
|
+
}
|
|
394
|
+
function renderStep(slot, remaining, options, refs) {
|
|
395
|
+
if (remaining.length === 0) {
|
|
396
|
+
options.renderInnermost(slot);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const [wrapper, ...rest] = remaining;
|
|
400
|
+
// `slot.injector` is the element injector seen from inside the current slot —
|
|
401
|
+
// for nested slots that includes the outer wrapper's component, enabling
|
|
402
|
+
// `inject(OuterWrapper)`. When a field injector is provided we merge it in
|
|
403
|
+
// front so tokens like `ARRAY_CONTEXT` also resolve.
|
|
404
|
+
const wrapperInjector = options.fieldInjector ? createWrapperAwareInjector(options.fieldInjector, slot.injector) : slot.injector;
|
|
405
|
+
const ref = slot.createComponent(wrapper.component, {
|
|
406
|
+
environmentInjector: options.environmentInjector,
|
|
407
|
+
injector: wrapperInjector,
|
|
408
|
+
});
|
|
409
|
+
refs.push(ref);
|
|
410
|
+
for (const [key, value] of Object.entries(wrapper.config)) {
|
|
411
|
+
// `type` is the registry discriminant; `fieldInputs` is set explicitly
|
|
412
|
+
// below. Don't let a stray config key override either.
|
|
413
|
+
if (key === 'type' || key === 'fieldInputs')
|
|
414
|
+
continue;
|
|
415
|
+
setInputIfDeclared(ref, key, value);
|
|
416
|
+
}
|
|
417
|
+
if (options.fieldInputs !== undefined) {
|
|
418
|
+
setInputIfDeclared(ref, 'fieldInputs', options.fieldInputs);
|
|
419
|
+
}
|
|
420
|
+
// Required to resolve `viewChild.required('fieldComponent')` before we
|
|
421
|
+
// recurse into the slot. O(chain length) CD passes per rebuild — acceptable
|
|
422
|
+
// for typical chains (≤3); refactor to a template-based render if it bites.
|
|
423
|
+
ref.changeDetectorRef.detectChanges();
|
|
424
|
+
const inner = resolveInnerSlot(ref);
|
|
425
|
+
if (!inner) {
|
|
426
|
+
options.logger.error(`Wrapper component for type '${wrapper.config.type}' does not provide a 'fieldComponent' ViewContainerRef. ` +
|
|
427
|
+
`Ensure the wrapper component has a viewChild('fieldComponent', { read: ViewContainerRef }) query ` +
|
|
428
|
+
`and that #fieldComponent is not inside a conditional (@if, @defer).`);
|
|
429
|
+
// Unwind: destroy every wrapper built so far, including this one, so the
|
|
430
|
+
// caller doesn't end up with a partial chain left in the DOM.
|
|
431
|
+
while (refs.length)
|
|
432
|
+
refs.pop().destroy();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
renderStep(inner, rest, options, refs);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Read a wrapper's `#fieldComponent` ViewContainerRef. `viewChild.required`
|
|
439
|
+
* throws when the query can't resolve (missing template ref, or ref sitting
|
|
440
|
+
* inside `@if` / `@defer` / `@for`); any throw from this pure signal read
|
|
441
|
+
* means the slot is unusable, so we return undefined and let the caller log
|
|
442
|
+
* the actionable "wrapper missing fieldComponent" message.
|
|
443
|
+
*/
|
|
444
|
+
function resolveInnerSlot(ref) {
|
|
445
|
+
const contract = ref.instance;
|
|
446
|
+
try {
|
|
447
|
+
return contract.fieldComponent?.();
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Merged injector: check the field-level injector first (for tokens like
|
|
455
|
+
* `ARRAY_CONTEXT` / `FIELD_SIGNAL_CONTEXT`), fall back to the element-chain
|
|
456
|
+
* injector (so nested wrappers can still `inject(OuterWrapper)`).
|
|
457
|
+
*
|
|
458
|
+
* Precedence: a field-level token shadows a same-named token provided by an
|
|
459
|
+
* outer wrapper. That matches "more specific context wins" but is worth
|
|
460
|
+
* keeping in mind if a wrapper exports service tokens.
|
|
461
|
+
*/
|
|
462
|
+
function createWrapperAwareInjector(fieldInjector, elementInjector) {
|
|
463
|
+
if (fieldInjector === elementInjector)
|
|
464
|
+
return fieldInjector;
|
|
465
|
+
return new WrapperAwareInjector(fieldInjector, elementInjector);
|
|
466
|
+
}
|
|
467
|
+
/** Sentinel — distinguishes "token not found" from a legitimate `undefined` provider value. */
|
|
468
|
+
const WRAPPER_NOT_FOUND = {};
|
|
469
|
+
class WrapperAwareInjector extends Injector {
|
|
470
|
+
fieldInjector;
|
|
471
|
+
elementInjector;
|
|
472
|
+
constructor(fieldInjector, elementInjector) {
|
|
473
|
+
super();
|
|
474
|
+
this.fieldInjector = fieldInjector;
|
|
475
|
+
this.elementInjector = elementInjector;
|
|
476
|
+
}
|
|
477
|
+
get(token, notFoundValue, options) {
|
|
478
|
+
const field = this.fieldInjector.get(token, WRAPPER_NOT_FOUND, options);
|
|
479
|
+
if (field !== WRAPPER_NOT_FOUND)
|
|
480
|
+
return field;
|
|
481
|
+
return this.elementInjector.get(token, notFoundValue, options);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
348
485
|
/**
|
|
349
486
|
* Injection token for the dynamic forms logger.
|
|
350
487
|
*
|
|
@@ -353,6 +490,567 @@ function createArrayItemIdGenerator() {
|
|
|
353
490
|
*/
|
|
354
491
|
const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
|
|
355
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
|
+
/**
|
|
538
|
+
* Owns the wrapper-chain lifecycle for a field or container: async component
|
|
539
|
+
* loading, switchMap-based cancellation of stale loads, flicker-tolerant
|
|
540
|
+
* rebuilds (only structural changes tear down), and `fieldInputs` push-through.
|
|
541
|
+
* Must be called from a DI injection context.
|
|
542
|
+
*/
|
|
543
|
+
function createWrapperChainController(opts) {
|
|
544
|
+
const deps = injectChainDeps();
|
|
545
|
+
const state = createStateSignal(opts);
|
|
546
|
+
const mounted = { value: null };
|
|
547
|
+
let refs = [];
|
|
548
|
+
buildEmissionStream(state, deps)
|
|
549
|
+
.pipe(takeUntilDestroyed(deps.destroyRef))
|
|
550
|
+
.subscribe((emission) => {
|
|
551
|
+
// Wrap the whole render path — a throw here (bad wrapper template,
|
|
552
|
+
// caller's renderInnermost blew up, etc.) would otherwise terminate
|
|
553
|
+
// the subscription and silently freeze subsequent chain updates.
|
|
554
|
+
try {
|
|
555
|
+
refs = applyEmission(emission, { opts, deps, mounted, refs });
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
deps.logger.error('Wrapper chain render failed; tearing down partial state.', err);
|
|
559
|
+
opts.vcr().clear();
|
|
560
|
+
refs = [];
|
|
561
|
+
mounted.value = null;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
pushFieldInputsOnChange(opts, () => refs);
|
|
565
|
+
deps.destroyRef.onDestroy(() => opts.vcr().clear());
|
|
566
|
+
}
|
|
567
|
+
function injectChainDeps() {
|
|
568
|
+
return {
|
|
569
|
+
registry: inject(WRAPPER_REGISTRY),
|
|
570
|
+
cache: inject(WRAPPER_COMPONENT_CACHE),
|
|
571
|
+
logger: inject(DynamicFormLogger),
|
|
572
|
+
destroyRef: inject(DestroyRef),
|
|
573
|
+
environmentInjector: inject(EnvironmentInjector),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function createStateSignal(opts) {
|
|
577
|
+
return computed(() => ({
|
|
578
|
+
open: opts.gate?.() ?? true,
|
|
579
|
+
wrappers: opts.wrappers(),
|
|
580
|
+
rebuildKey: opts.rebuildKey?.(),
|
|
581
|
+
}), { equal: (a, b) => a.open === b.open && a.rebuildKey === b.rebuildKey && isSameWrapperChain(a.wrappers, b.wrappers) });
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Resolve a `ChainState` into a `ChainEmission`. `switchMap` cancellation
|
|
585
|
+
* guarantees in-flight async loads are discarded when a newer state arrives.
|
|
586
|
+
*/
|
|
587
|
+
function buildEmissionStream(state, deps) {
|
|
588
|
+
return toObservable(state).pipe(switchMap((s) => resolveLoadedWrappers(s, deps)));
|
|
589
|
+
}
|
|
590
|
+
function resolveLoadedWrappers(state, deps) {
|
|
591
|
+
if (!state.open) {
|
|
592
|
+
return of({ state, loaded: null });
|
|
593
|
+
}
|
|
594
|
+
if (state.wrappers.every((w) => deps.cache.has(w.type))) {
|
|
595
|
+
// Sync fast-path — the cache only holds SUCCESSFUL loads, so "every cached"
|
|
596
|
+
// means every wrapper was previously resolved cleanly. No need to re-log.
|
|
597
|
+
const loaded = state.wrappers.map((config) => ({ config, component: deps.cache.get(config.type) }));
|
|
598
|
+
return of({ state, loaded });
|
|
599
|
+
}
|
|
600
|
+
return loadWrapperComponents(state.wrappers, deps.registry, deps.cache, deps.logger).pipe(map((loaded) => ({ state, loaded })));
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Diff what's mounted against the next state and act:
|
|
604
|
+
* - Gate-only flicker → no-op (preserve the mounted chain)
|
|
605
|
+
* - Structural change → beforeRebuild → vcr.clear → render fresh
|
|
606
|
+
* - Idempotent re-emission → no-op
|
|
607
|
+
*
|
|
608
|
+
* Returns the (possibly new) wrapper refs so the caller can track them for
|
|
609
|
+
* `fieldInputs` push-through.
|
|
610
|
+
*/
|
|
611
|
+
function applyEmission({ state, loaded }, ctx) {
|
|
612
|
+
const { opts, deps, mounted, refs } = ctx;
|
|
613
|
+
const vcr = opts.vcr();
|
|
614
|
+
const structurallyChanged = isStructurallyDifferent(mounted.value, state);
|
|
615
|
+
// Gate-only flicker after first mount — keep the chain alive so the user's
|
|
616
|
+
// focus / caret / scroll survive. The caller's rawInputs effect continues
|
|
617
|
+
// to push live mapper outputs through the still-mounted innermost.
|
|
618
|
+
if (!structurallyChanged && !state.open)
|
|
619
|
+
return refs;
|
|
620
|
+
// Real structural change — tear down. Angular cascades destroy through
|
|
621
|
+
// every nested ComponentRef, so walking `refs` manually is redundant.
|
|
622
|
+
// `beforeRebuild` gives the caller a chance to detach views it wants to
|
|
623
|
+
// preserve (e.g. the innermost field when only wrappers changed).
|
|
624
|
+
if (structurallyChanged && mounted.value !== null) {
|
|
625
|
+
opts.beforeRebuild?.();
|
|
626
|
+
vcr.clear();
|
|
627
|
+
mounted.value = null;
|
|
628
|
+
}
|
|
629
|
+
if (!state.open || loaded === null)
|
|
630
|
+
return [];
|
|
631
|
+
// Same structure + already mounted — idempotent re-emission, nothing to do.
|
|
632
|
+
if (mounted.value !== null)
|
|
633
|
+
return refs;
|
|
634
|
+
const newRefs = renderWrapperChain({
|
|
635
|
+
outerContainer: vcr,
|
|
636
|
+
loadedWrappers: loaded,
|
|
637
|
+
environmentInjector: deps.environmentInjector,
|
|
638
|
+
logger: deps.logger,
|
|
639
|
+
fieldInputs: opts.fieldInputs?.(),
|
|
640
|
+
fieldInjector: opts.fieldInjector?.(),
|
|
641
|
+
renderInnermost: opts.renderInnermost,
|
|
642
|
+
});
|
|
643
|
+
mounted.value = { wrappers: state.wrappers, rebuildKey: state.rebuildKey };
|
|
644
|
+
return newRefs;
|
|
645
|
+
}
|
|
646
|
+
function isStructurallyDifferent(mounted, next) {
|
|
647
|
+
if (mounted === null)
|
|
648
|
+
return true;
|
|
649
|
+
if (mounted.rebuildKey !== next.rebuildKey)
|
|
650
|
+
return true;
|
|
651
|
+
return !isSameWrapperChain(mounted.wrappers, next.wrappers);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Push the latest `fieldInputs` bag to every mounted wrapper whenever it
|
|
655
|
+
* changes — without rebuilding the chain. No-op when the chain is empty
|
|
656
|
+
* (gated off, or nothing rendered yet).
|
|
657
|
+
*/
|
|
658
|
+
function pushFieldInputsOnChange(opts, getRefs) {
|
|
659
|
+
if (!opts.fieldInputs)
|
|
660
|
+
return;
|
|
661
|
+
explicitEffect([opts.fieldInputs], ([fi]) => {
|
|
662
|
+
for (const ref of getRefs())
|
|
663
|
+
setInputIfDeclared(ref, 'fieldInputs', fi);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Get the length of an array FieldTree.
|
|
669
|
+
*/
|
|
670
|
+
function getArrayLength(arrayFieldTree) {
|
|
671
|
+
const value = arrayFieldTree().value();
|
|
672
|
+
return Array.isArray(value) ? value.length : 0;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Build a `ReadonlyFieldTree` by extracting the whitelisted read signals from a
|
|
676
|
+
* Signal Forms `FieldTree`. Returns a fresh plain object — no casting, no proxying,
|
|
677
|
+
* so consumers only see the narrow surface and Angular's `WritableSignal` capability
|
|
678
|
+
* on `value` is hidden.
|
|
679
|
+
*/
|
|
680
|
+
function toReadonlyFieldTree(field) {
|
|
681
|
+
const state = field();
|
|
682
|
+
return {
|
|
683
|
+
value: state.value,
|
|
684
|
+
valid: state.valid,
|
|
685
|
+
invalid: state.invalid,
|
|
686
|
+
touched: state.touched,
|
|
687
|
+
dirty: state.dirty,
|
|
688
|
+
required: state.required,
|
|
689
|
+
disabled: state.disabled,
|
|
690
|
+
hidden: state.hidden,
|
|
691
|
+
errors: state.errors,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* DI-scoped cache for `ReadonlyFieldTree` views, keyed on the source FieldTree
|
|
696
|
+
* identity. A fresh WeakMap per root injector keeps renders isolated under SSR —
|
|
697
|
+
* Angular creates a new root injector per request, so there's no shared state
|
|
698
|
+
* between server renders (same pattern as `COMPONENT_CACHE`).
|
|
699
|
+
*
|
|
700
|
+
* @internal
|
|
701
|
+
*/
|
|
702
|
+
const READONLY_FIELD_TREE_CACHE = new InjectionToken('READONLY_FIELD_TREE_CACHE', {
|
|
703
|
+
providedIn: 'root',
|
|
704
|
+
factory: () => new WeakMap(),
|
|
705
|
+
});
|
|
706
|
+
/**
|
|
707
|
+
* Cached variant of {@link toReadonlyFieldTree}. Each FieldTree instance is a
|
|
708
|
+
* stable reference for the lifetime of its form, so one `ReadonlyFieldTree`
|
|
709
|
+
* view per tree is enough. Callers on hot paths (`pushInputs` on every mapper
|
|
710
|
+
* emission) avoid re-allocating the nine-property view object.
|
|
711
|
+
*
|
|
712
|
+
* @internal
|
|
713
|
+
*/
|
|
714
|
+
function toReadonlyFieldTreeCached(cache, field) {
|
|
715
|
+
const key = field;
|
|
716
|
+
const cached = cache.get(key);
|
|
717
|
+
if (cached)
|
|
718
|
+
return cached;
|
|
719
|
+
const view = toReadonlyFieldTree(field);
|
|
720
|
+
cache.set(key, view);
|
|
721
|
+
return view;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Structural directive that renders a `ResolvedField` with its effective
|
|
726
|
+
* wrapper chain.
|
|
727
|
+
*
|
|
728
|
+
* Replaces `*ngComponentOutlet` in field-rendering templates. When the field
|
|
729
|
+
* has no wrappers, the component is created directly at the outlet position
|
|
730
|
+
* (no extra DOM nesting). When wrappers apply, they chain outermost-first and
|
|
731
|
+
* the field renders in the innermost slot.
|
|
732
|
+
*
|
|
733
|
+
* Effective wrappers are merged from (outermost → innermost):
|
|
734
|
+
* 1. `WrapperTypeDefinition.types` auto-associations for the field's `type`
|
|
735
|
+
* 2. `FormConfig.defaultWrappers`
|
|
736
|
+
* 3. Field-level `FieldDef.wrappers` (`null` = explicit opt-out; `[]` does not
|
|
737
|
+
* opt out — auto + defaults still apply)
|
|
738
|
+
*
|
|
739
|
+
* Wrapper config keys (minus `type`) are pushed as individual Angular inputs;
|
|
740
|
+
* every wrapper also receives `fieldInputs` — a `WrapperFieldInputs` bag that
|
|
741
|
+
* includes the field's mapper outputs and a `ReadonlyFieldTree` view of its
|
|
742
|
+
* form state.
|
|
743
|
+
*
|
|
744
|
+
* Rendering is gated by `field.renderReady()` — the directive waits until
|
|
745
|
+
* the mapper produces the required inputs before instantiating the component.
|
|
746
|
+
* Once rendered, a transient `renderReady → false` does *not* tear the chain
|
|
747
|
+
* down; the controller keeps the mounted components alive and ignores the
|
|
748
|
+
* flicker. Only a structural change (wrappers or component class) triggers
|
|
749
|
+
* a rebuild.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```html
|
|
753
|
+
* \@for (field of resolvedFields(); track field.key) {
|
|
754
|
+
* <ng-container *dfFieldOutlet="field; environmentInjector: envInjector" />
|
|
755
|
+
* }
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
class DfFieldOutlet {
|
|
759
|
+
// Named to match the structural directive microsyntax directly
|
|
760
|
+
// (`*dfFieldOutlet="field; environmentInjector: env"`) so no aliasing is needed.
|
|
761
|
+
dfFieldOutlet = input.required(...(ngDevMode ? [{ debugName: "dfFieldOutlet" }] : /* istanbul ignore next */ []));
|
|
762
|
+
dfFieldOutletEnvironmentInjector = input(undefined, ...(ngDevMode ? [{ debugName: "dfFieldOutletEnvironmentInjector" }] : /* istanbul ignore next */ []));
|
|
763
|
+
vcrRef = inject(ViewContainerRef);
|
|
764
|
+
vcr = signal(this.vcrRef).asReadonly();
|
|
765
|
+
destroyRef = inject(DestroyRef);
|
|
766
|
+
wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);
|
|
767
|
+
defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });
|
|
768
|
+
readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);
|
|
769
|
+
fieldRef;
|
|
770
|
+
/**
|
|
771
|
+
* VCR the `fieldRef`'s host view is currently inserted into (either the outer
|
|
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;
|
|
779
|
+
/**
|
|
780
|
+
* Last rawInputs reference pushed to the innermost field. Used to dedupe the
|
|
781
|
+
* rawInputs effect when the same snapshot was already applied as part of the
|
|
782
|
+
* initial render — keeps per-keystroke input updates O(changed-keys) instead
|
|
783
|
+
* of re-walking the whole input bag.
|
|
784
|
+
*/
|
|
785
|
+
lastPushedInputs;
|
|
786
|
+
componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
|
|
787
|
+
renderReady = computed(() => this.dfFieldOutlet().renderReady(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
|
|
788
|
+
rawInputs = computed(() => this.dfFieldOutlet().inputs(), ...(ngDevMode ? [{ debugName: "rawInputs" }] : /* istanbul ignore next */ []));
|
|
789
|
+
/**
|
|
790
|
+
* Effective wrapper chain. Element-wise identity comparison keeps the signal
|
|
791
|
+
* stable across `ResolvedField` reference changes that don't actually change
|
|
792
|
+
* the chain — avoids rebuilds on reconciled fields.
|
|
793
|
+
*/
|
|
794
|
+
wrappers = computed(() => resolveWrappers(this.dfFieldOutlet().fieldDef, this.defaultWrappersSignal?.(), this.wrapperAutoAssociations), { ...(ngDevMode ? { debugName: "wrappers" } : /* istanbul ignore next */ {}), equal: isSameWrapperChain });
|
|
795
|
+
/**
|
|
796
|
+
* `fieldInputs` bag handed to every wrapper in the chain. Memoised on
|
|
797
|
+
* `rawInputs` identity so repeated emissions with the same underlying object
|
|
798
|
+
* return the same view and don't cascade OnPush re-evaluations.
|
|
799
|
+
*/
|
|
800
|
+
fieldInputs = computed(() => this.buildFieldInputs(this.rawInputs()), ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
|
|
801
|
+
defaultEnvInjector = inject(EnvironmentInjector);
|
|
802
|
+
/** Environment injector for the innermost field component — `[environmentInjector]` input takes precedence over the directive's own DI. */
|
|
803
|
+
fieldEnvInjector = computed(() => this.dfFieldOutletEnvironmentInjector() ?? this.defaultEnvInjector, ...(ngDevMode ? [{ debugName: "fieldEnvInjector" }] : /* istanbul ignore next */ []));
|
|
804
|
+
/** Field-level injector (FIELD_SIGNAL_CONTEXT, ARRAY_CONTEXT, …). Threaded to the controller so wrappers can inject it too. */
|
|
805
|
+
fieldInjector = computed(() => this.dfFieldOutlet().injector, ...(ngDevMode ? [{ debugName: "fieldInjector" }] : /* istanbul ignore next */ []));
|
|
806
|
+
constructor() {
|
|
807
|
+
createWrapperChainController({
|
|
808
|
+
vcr: this.vcr,
|
|
809
|
+
wrappers: this.wrappers,
|
|
810
|
+
gate: this.renderReady,
|
|
811
|
+
rebuildKey: this.componentIdentity,
|
|
812
|
+
fieldInputs: this.fieldInputs,
|
|
813
|
+
fieldInjector: this.fieldInjector,
|
|
814
|
+
beforeRebuild: () => this.detachFieldRef(),
|
|
815
|
+
renderInnermost: (slot) => {
|
|
816
|
+
const resolved = this.dfFieldOutlet();
|
|
817
|
+
if (this.fieldRef && this.fieldRef.componentType === resolved.component) {
|
|
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());
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
// Push rawInputs to the innermost field. Safe mid-rebuild — renderInnermost
|
|
848
|
+
// re-reads rawInputs synchronously on mount, so skipping here loses nothing.
|
|
849
|
+
explicitEffect([this.rawInputs], ([rawInputs]) => {
|
|
850
|
+
if (!this.fieldRef)
|
|
851
|
+
return;
|
|
852
|
+
if (this.lastPushedInputs === rawInputs)
|
|
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
|
+
};
|
|
929
|
+
}
|
|
930
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DfFieldOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
931
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", 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
|
+
}
|
|
933
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DfFieldOutlet, decorators: [{
|
|
934
|
+
type: Directive,
|
|
935
|
+
args: [{
|
|
936
|
+
selector: '[dfFieldOutlet]',
|
|
937
|
+
}]
|
|
938
|
+
}], ctorParameters: () => [], propDecorators: { dfFieldOutlet: [{ type: i0.Input, args: [{ isSignal: true, alias: "dfFieldOutlet", required: true }] }], dfFieldOutletEnvironmentInjector: [{ type: i0.Input, args: [{ isSignal: true, alias: "dfFieldOutletEnvironmentInjector", required: false }] }] } });
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Wraps a submission action to handle both Promise and Observable returns.
|
|
942
|
+
* Converts Observables to Promises for compatibility with Angular Signal Forms' submit().
|
|
943
|
+
*
|
|
944
|
+
* Errors are NOT caught here — they propagate so that submit() can reject its Promise,
|
|
945
|
+
* allowing the caller's catchError to log and keep the submission stream alive.
|
|
946
|
+
*
|
|
947
|
+
* @param action - The submission action function
|
|
948
|
+
* @returns A wrapped function that returns a Promise
|
|
949
|
+
*/
|
|
950
|
+
function wrapSubmissionAction(action) {
|
|
951
|
+
return async (formTree) => {
|
|
952
|
+
const result = action(formTree);
|
|
953
|
+
// If the action returns an Observable, convert it to a Promise
|
|
954
|
+
if (isObservable(result)) {
|
|
955
|
+
await firstValueFrom(result);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
await Promise.resolve(result);
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Creates an Observable that handles form submission with optional submission action.
|
|
963
|
+
*
|
|
964
|
+
* This utility encapsulates the submission handling logic:
|
|
965
|
+
* - Listens for submit events from the event bus
|
|
966
|
+
* - If a submission.action is configured, wraps it and uses Angular Signal Forms' submit()
|
|
967
|
+
* - Handles both Promise and Observable returns from the action
|
|
968
|
+
* - Uses exhaustMap to ignore new submissions while one is in-flight (first-submit-wins)
|
|
969
|
+
*
|
|
970
|
+
* The returned Observable should be subscribed to with takeUntilDestroyed() in the component.
|
|
971
|
+
*
|
|
972
|
+
* @param options - Configuration options for the submission handler
|
|
973
|
+
* @returns Observable that processes submissions (emits when submission completes)
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```typescript
|
|
977
|
+
* // In component constructor
|
|
978
|
+
* createSubmissionHandler({
|
|
979
|
+
* eventBus: this.eventBus,
|
|
980
|
+
* configSignal: this.config,
|
|
981
|
+
* formSignal: this.form,
|
|
982
|
+
* })
|
|
983
|
+
* .pipe(takeUntilDestroyed())
|
|
984
|
+
* .subscribe();
|
|
985
|
+
* ```
|
|
986
|
+
*/
|
|
987
|
+
function createSubmissionHandler(options) {
|
|
988
|
+
const { eventBus, configSignal, formSignal, validSignal, logger } = options;
|
|
989
|
+
// exhaustMap ensures first-submit-wins: a second submit event while the first
|
|
990
|
+
// is in-flight is silently dropped rather than cancelling the running Promise.
|
|
991
|
+
// switchMap would unsubscribe the Observable wrapper but cannot cancel the
|
|
992
|
+
// underlying Promise, causing both side effects to execute.
|
|
993
|
+
return eventBus.on('submit').pipe(exhaustMap(() => {
|
|
994
|
+
const submissionConfig = configSignal().submission;
|
|
995
|
+
// If no submission action is configured, let the submitted output handle it
|
|
996
|
+
// This maintains backward compatibility for users handling submission manually
|
|
997
|
+
if (!submissionConfig?.action) {
|
|
998
|
+
return EMPTY;
|
|
999
|
+
}
|
|
1000
|
+
// Guard: match the (submitted) output's safety contract — reject submission
|
|
1001
|
+
// when the form is invalid or has pending async validators.
|
|
1002
|
+
if (!validSignal()) {
|
|
1003
|
+
logger.debug('Submission action skipped: form is not valid (invalid or pending async validators)');
|
|
1004
|
+
return EMPTY;
|
|
1005
|
+
}
|
|
1006
|
+
// Type assertion needed: submission.action accepts the form tree but its signature
|
|
1007
|
+
// is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.
|
|
1008
|
+
const wrappedAction = wrapSubmissionAction(submissionConfig.action);
|
|
1009
|
+
// Use Angular Signal Forms' native submit() function
|
|
1010
|
+
// This automatically:
|
|
1011
|
+
// - Sets form.submitting() to true during execution
|
|
1012
|
+
// - Applies server errors to form fields on completion
|
|
1013
|
+
// - Sets form.submitting() to false when done
|
|
1014
|
+
// catchError keeps the exhaustMap stream alive after action failure —
|
|
1015
|
+
// without it, an unhandled error would terminate all future submissions.
|
|
1016
|
+
return from(submit(formSignal(), wrappedAction)).pipe(catchError((error) => {
|
|
1017
|
+
logger.error('Submission action failed:', error);
|
|
1018
|
+
return EMPTY;
|
|
1019
|
+
}));
|
|
1020
|
+
}));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Injection token for globally enabling form value emission on events.
|
|
1025
|
+
*
|
|
1026
|
+
* When set to `true`, all events dispatched through the EventBus will include
|
|
1027
|
+
* the current form value in the `formValue` property.
|
|
1028
|
+
*
|
|
1029
|
+
* This token is configured via `withEventFormValue()` feature function.
|
|
1030
|
+
* Per-form overrides can be set via `options.emitFormValueOnEvents` in the form config.
|
|
1031
|
+
*
|
|
1032
|
+
* @internal
|
|
1033
|
+
*/
|
|
1034
|
+
const EMIT_FORM_VALUE_ON_EVENTS = new InjectionToken('EMIT_FORM_VALUE_ON_EVENTS', {
|
|
1035
|
+
providedIn: 'root',
|
|
1036
|
+
factory: () => false,
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Registry service that provides access to the root form and its values.
|
|
1041
|
+
*
|
|
1042
|
+
* Constructed via factory in `provideDynamicFormDI` with signals from
|
|
1043
|
+
* `FormStateManager` — no DI injection, no Maps, no lifecycle.
|
|
1044
|
+
*/
|
|
1045
|
+
class RootFormRegistryService {
|
|
1046
|
+
formValue;
|
|
1047
|
+
rootForm;
|
|
1048
|
+
constructor(formValue, rootForm) {
|
|
1049
|
+
this.formValue = formValue;
|
|
1050
|
+
this.rootForm = rootForm;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
356
1054
|
/**
|
|
357
1055
|
* No-operation logger implementation.
|
|
358
1056
|
* All methods are no-ops - used in production by default.
|
|
@@ -532,10 +1230,10 @@ class EventBus {
|
|
|
532
1230
|
}
|
|
533
1231
|
return this.pipeline$.pipe(filter((event) => event.type === eventType));
|
|
534
1232
|
}
|
|
535
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
536
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1233
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1234
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventBus });
|
|
537
1235
|
}
|
|
538
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1236
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventBus, decorators: [{
|
|
539
1237
|
type: Injectable
|
|
540
1238
|
}] });
|
|
541
1239
|
|
|
@@ -588,7 +1286,7 @@ const INITIALIZATION_TIMEOUT_MS = new InjectionToken('INITIALIZATION_TIMEOUT_MS'
|
|
|
588
1286
|
* ```
|
|
589
1287
|
*/
|
|
590
1288
|
function createInitializationTracker(eventBus, expectedCount) {
|
|
591
|
-
return eventBus.on('component-initialized').pipe(scan((count) => count + 1, 0), map((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
|
|
1289
|
+
return eventBus.on('component-initialized').pipe(scan((count) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
|
|
592
1290
|
}
|
|
593
1291
|
/**
|
|
594
1292
|
* Creates an observable that tracks component initialization progress with detailed status.
|
|
@@ -613,7 +1311,7 @@ function createInitializationTracker(eventBus, expectedCount) {
|
|
|
613
1311
|
* ```
|
|
614
1312
|
*/
|
|
615
1313
|
function createDetailedInitializationTracker(eventBus, expectedCount) {
|
|
616
|
-
return eventBus.on('component-initialized').pipe(scan((count) => count + 1, 0), map((currentCount) => ({
|
|
1314
|
+
return eventBus.on('component-initialized').pipe(scan((count) => count + 1, 0), map$1((currentCount) => ({
|
|
617
1315
|
currentCount,
|
|
618
1316
|
expectedCount,
|
|
619
1317
|
isComplete: currentCount >= expectedCount,
|
|
@@ -649,11 +1347,11 @@ function setupInitializationTracking(options) {
|
|
|
649
1347
|
const { eventBus, totalComponentsCount, injector, componentId } = options;
|
|
650
1348
|
const timeoutMs = injector.get(INITIALIZATION_TIMEOUT_MS);
|
|
651
1349
|
const logger = injector.get(DynamicFormLogger);
|
|
652
|
-
return toObservable(totalComponentsCount, { injector }).pipe(take(1), switchMap((count) => {
|
|
1350
|
+
return toObservable(totalComponentsCount, { injector }).pipe(take(1), switchMap$1((count) => {
|
|
653
1351
|
let tracking$;
|
|
654
1352
|
if (count === 1) {
|
|
655
1353
|
// Only dynamic-form component, emit immediately when it initializes
|
|
656
|
-
tracking$ = eventBus.on('component-initialized').pipe(filter$1((event) => event.componentType === 'dynamic-form' && event.componentId === componentId), map(() => true), take(1));
|
|
1354
|
+
tracking$ = eventBus.on('component-initialized').pipe(filter$1((event) => event.componentType === 'dynamic-form' && event.componentId === componentId), map$1(() => true), take(1));
|
|
657
1355
|
}
|
|
658
1356
|
else {
|
|
659
1357
|
tracking$ = createInitializationTracker(eventBus, count);
|
|
@@ -787,6 +1485,14 @@ function isSimplifiedArrayField(field) {
|
|
|
787
1485
|
return field.type === 'array' && 'template' in field;
|
|
788
1486
|
}
|
|
789
1487
|
|
|
1488
|
+
/**
|
|
1489
|
+
* Type guard for ContainerField with proper type narrowing
|
|
1490
|
+
*/
|
|
1491
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
|
|
1492
|
+
function isContainerTypedField(field) {
|
|
1493
|
+
return field.type === 'container' && 'fields' in field && 'wrappers' in field;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
790
1496
|
/**
|
|
791
1497
|
* Type guard to check if a field definition has child fields.
|
|
792
1498
|
* This is a looser check that works with FieldDef<unknown> without requiring RegisteredFieldTypes.
|
|
@@ -795,7 +1501,7 @@ function hasChildFields(field) {
|
|
|
795
1501
|
return 'fields' in field && field.fields != null;
|
|
796
1502
|
}
|
|
797
1503
|
/** Container field type names */
|
|
798
|
-
const CONTAINER_TYPES$1 = new Set(['page', 'row', 'group', 'array']);
|
|
1504
|
+
const CONTAINER_TYPES$1 = new Set(['page', 'row', 'group', 'array', 'container']);
|
|
799
1505
|
function isContainerField(field) {
|
|
800
1506
|
return CONTAINER_TYPES$1.has(field.type);
|
|
801
1507
|
}
|
|
@@ -1240,12 +1946,12 @@ function assertNever(value) {
|
|
|
1240
1946
|
* - Example: `noSpaces: (ctx) => ctx.value().includes(' ') ? { kind: 'noSpaces' } : null`
|
|
1241
1947
|
*
|
|
1242
1948
|
* 3. **Async Validators** - For asynchronous validation (debouncing, database lookups, etc.)
|
|
1243
|
-
* - Used in: validators array on fields with type '
|
|
1949
|
+
* - Used in: validators array on fields with type 'async'
|
|
1244
1950
|
* - Return type: Observable<ValidationError | ValidationError[] | null>
|
|
1245
1951
|
* - Example: `checkUsername: (ctx) => userService.checkAvailability(ctx.value())`
|
|
1246
1952
|
*
|
|
1247
1953
|
* 4. **HTTP Validators** - For HTTP-based validation with automatic request cancellation
|
|
1248
|
-
* - Used in: validators array on fields with type '
|
|
1954
|
+
* - Used in: validators array on fields with type 'http'
|
|
1249
1955
|
* - Configuration object with url, method, mapResponse, etc.
|
|
1250
1956
|
* - Example: `{ url: '/api/check', method: 'GET', mapResponse: ... }`
|
|
1251
1957
|
*
|
|
@@ -1292,8 +1998,6 @@ class FunctionRegistryService {
|
|
|
1292
1998
|
customFunctions = new Map();
|
|
1293
1999
|
customFunctionScopes = new Map();
|
|
1294
2000
|
derivationFunctions = new Map();
|
|
1295
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
1296
|
-
propertyDerivationFunctions = new Map();
|
|
1297
2001
|
asyncDerivationFunctions = new Map();
|
|
1298
2002
|
asyncConditionFunctions = new Map();
|
|
1299
2003
|
validators = new Map();
|
|
@@ -1395,66 +2099,14 @@ class FunctionRegistryService {
|
|
|
1395
2099
|
*
|
|
1396
2100
|
* @param derivations - Object mapping function names to derivation functions
|
|
1397
2101
|
*/
|
|
1398
|
-
setDerivationFunctions(derivations) {
|
|
1399
|
-
this.setRegistryIfChanged(this.derivationFunctions, derivations);
|
|
1400
|
-
}
|
|
1401
|
-
/**
|
|
1402
|
-
* Clear all derivation functions
|
|
1403
|
-
*/
|
|
1404
|
-
clearDerivationFunctions() {
|
|
1405
|
-
this.derivationFunctions.clear();
|
|
1406
|
-
}
|
|
1407
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1408
|
-
// Property Derivation Functions
|
|
1409
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
1410
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1411
|
-
/**
|
|
1412
|
-
* Register a property derivation function.
|
|
1413
|
-
*
|
|
1414
|
-
* Property derivation functions compute derived values for field properties
|
|
1415
|
-
* (like `minDate`, `options`, `label`) and are called when a
|
|
1416
|
-
* `PropertyDerivationLogicConfig` references them by `functionName`.
|
|
1417
|
-
*
|
|
1418
|
-
* @param name - Unique identifier for the function
|
|
1419
|
-
* @param fn - Function that receives EvaluationContext and returns the derived property value
|
|
1420
|
-
*/
|
|
1421
|
-
registerPropertyDerivationFunction(name, fn) {
|
|
1422
|
-
this.propertyDerivationFunctions.set(name, fn);
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* Get a property derivation function by name
|
|
1426
|
-
*/
|
|
1427
|
-
getPropertyDerivationFunction(name) {
|
|
1428
|
-
return this.propertyDerivationFunctions.get(name) ?? this.derivationFunctions.get(name);
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* Get all property derivation functions as an object.
|
|
1432
|
-
*
|
|
1433
|
-
* Merges both `derivationFunctions` and `propertyDerivationFunctions` maps,
|
|
1434
|
-
* with `propertyDerivationFunctions` taking precedence. This allows users
|
|
1435
|
-
* to migrate functions from `propertyDerivations` to `derivations` without
|
|
1436
|
-
* breaking property derivation lookups.
|
|
1437
|
-
*/
|
|
1438
|
-
getPropertyDerivationFunctions() {
|
|
1439
|
-
return {
|
|
1440
|
-
...Object.fromEntries(this.derivationFunctions),
|
|
1441
|
-
...Object.fromEntries(this.propertyDerivationFunctions),
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
/**
|
|
1445
|
-
* Set property derivation functions from a config object.
|
|
1446
|
-
* Only updates functions if their references have changed.
|
|
1447
|
-
*
|
|
1448
|
-
* @param functions - Object mapping function names to property derivation functions
|
|
1449
|
-
*/
|
|
1450
|
-
setPropertyDerivationFunctions(functions) {
|
|
1451
|
-
this.setRegistryIfChanged(this.propertyDerivationFunctions, functions);
|
|
2102
|
+
setDerivationFunctions(derivations) {
|
|
2103
|
+
this.setRegistryIfChanged(this.derivationFunctions, derivations);
|
|
1452
2104
|
}
|
|
1453
2105
|
/**
|
|
1454
|
-
* Clear all
|
|
2106
|
+
* Clear all derivation functions
|
|
1455
2107
|
*/
|
|
1456
|
-
|
|
1457
|
-
this.
|
|
2108
|
+
clearDerivationFunctions() {
|
|
2109
|
+
this.derivationFunctions.clear();
|
|
1458
2110
|
}
|
|
1459
2111
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1460
2112
|
// Async Derivation Functions
|
|
@@ -1757,17 +2409,16 @@ class FunctionRegistryService {
|
|
|
1757
2409
|
clearAll() {
|
|
1758
2410
|
this.clearCustomFunctions();
|
|
1759
2411
|
this.clearDerivationFunctions();
|
|
1760
|
-
this.clearPropertyDerivationFunctions();
|
|
1761
2412
|
this.clearAsyncDerivationFunctions();
|
|
1762
2413
|
this.clearAsyncConditionFunctions();
|
|
1763
2414
|
this.clearValidators();
|
|
1764
2415
|
this.clearAsyncValidators();
|
|
1765
2416
|
this.clearHttpValidators();
|
|
1766
2417
|
}
|
|
1767
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1768
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
2418
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2419
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FunctionRegistryService });
|
|
1769
2420
|
}
|
|
1770
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2421
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FunctionRegistryService, decorators: [{
|
|
1771
2422
|
type: Injectable
|
|
1772
2423
|
}] });
|
|
1773
2424
|
|
|
@@ -1806,10 +2457,10 @@ class SchemaRegistryService {
|
|
|
1806
2457
|
}
|
|
1807
2458
|
return schema;
|
|
1808
2459
|
}
|
|
1809
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1810
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
2460
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2461
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SchemaRegistryService });
|
|
1811
2462
|
}
|
|
1812
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2463
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SchemaRegistryService, decorators: [{
|
|
1813
2464
|
type: Injectable
|
|
1814
2465
|
}] });
|
|
1815
2466
|
|
|
@@ -2390,10 +3041,10 @@ class FieldContextRegistryService {
|
|
|
2390
3041
|
},
|
|
2391
3042
|
};
|
|
2392
3043
|
}
|
|
2393
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2394
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3044
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3045
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FieldContextRegistryService });
|
|
2395
3046
|
}
|
|
2396
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3047
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FieldContextRegistryService, decorators: [{
|
|
2397
3048
|
type: Injectable
|
|
2398
3049
|
}] });
|
|
2399
3050
|
|
|
@@ -2721,24 +3372,6 @@ function countArrayPlaceholders(path) {
|
|
|
2721
3372
|
return path.split('.').filter((part) => part === '$').length;
|
|
2722
3373
|
}
|
|
2723
3374
|
|
|
2724
|
-
/**
|
|
2725
|
-
* Emits a deprecation warning once per unique key in dev mode only.
|
|
2726
|
-
* Subsequent calls with the same `id` are silently ignored.
|
|
2727
|
-
*
|
|
2728
|
-
* @param logger - The DynamicFormLogger instance for output
|
|
2729
|
-
* @param tracker - DI-scoped tracker to deduplicate warnings
|
|
2730
|
-
* @param id - Unique identifier for this deprecation (e.g., 'type:propertyDerivation')
|
|
2731
|
-
* @param message - Human-readable deprecation message
|
|
2732
|
-
*
|
|
2733
|
-
* @public
|
|
2734
|
-
*/
|
|
2735
|
-
function warnDeprecated(logger, tracker, id, message) {
|
|
2736
|
-
if (isDevMode() && !tracker.warnedKeys.has(id)) {
|
|
2737
|
-
tracker.warnedKeys.add(id);
|
|
2738
|
-
logger.warn(`[Dynamic Forms] DEPRECATED: ${message}`);
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
3375
|
/**
|
|
2743
3376
|
* Token types for expression parsing
|
|
2744
3377
|
*/
|
|
@@ -3595,16 +4228,22 @@ function evaluateCondition(expression, context) {
|
|
|
3595
4228
|
switch (expression.type) {
|
|
3596
4229
|
case 'fieldValue':
|
|
3597
4230
|
return evaluateFieldValueCondition(expression, context);
|
|
3598
|
-
case 'formValue':
|
|
3599
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
3600
|
-
if (context.deprecationTracker) {
|
|
3601
|
-
warnDeprecated(context.logger, context.deprecationTracker, 'condition:formValue', "Condition type 'formValue' is deprecated. Use 'fieldValue' with a specific fieldPath, or 'javascript' for complex form-level comparisons.");
|
|
3602
|
-
}
|
|
3603
|
-
return evaluateFormValueCondition(expression, context);
|
|
3604
4231
|
case 'javascript':
|
|
3605
4232
|
return evaluateJavaScriptExpression(expression, context);
|
|
3606
|
-
case 'custom':
|
|
3607
|
-
|
|
4233
|
+
case 'custom': {
|
|
4234
|
+
const customFn = context.customFunctions?.[expression.functionName];
|
|
4235
|
+
if (!customFn) {
|
|
4236
|
+
context.logger.error('Custom function not found:', expression.functionName);
|
|
4237
|
+
return false;
|
|
4238
|
+
}
|
|
4239
|
+
try {
|
|
4240
|
+
return !!customFn(context);
|
|
4241
|
+
}
|
|
4242
|
+
catch (error) {
|
|
4243
|
+
context.logger.error('Error executing custom function:', expression.functionName, error);
|
|
4244
|
+
return false;
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
3608
4247
|
case 'and':
|
|
3609
4248
|
return evaluateAndCondition(expression, context);
|
|
3610
4249
|
case 'or':
|
|
@@ -3637,12 +4276,9 @@ function evaluateFieldValueCondition(expression, context) {
|
|
|
3637
4276
|
: undefined;
|
|
3638
4277
|
return compareValues(fieldValue, expression.value, expression.operator);
|
|
3639
4278
|
}
|
|
3640
|
-
function evaluateFormValueCondition(expression, context) {
|
|
3641
|
-
return compareValues(context.formValue, expression.value, expression.operator);
|
|
3642
|
-
}
|
|
3643
4279
|
function evaluateJavaScriptExpression(expression, context) {
|
|
3644
4280
|
try {
|
|
3645
|
-
// Use secure AST-based expression parser instead of
|
|
4281
|
+
// Use secure AST-based expression parser instead of dynamic code execution
|
|
3646
4282
|
const result = ExpressionParser.evaluate(expression.expression, context);
|
|
3647
4283
|
return !!result;
|
|
3648
4284
|
}
|
|
@@ -3651,25 +4287,6 @@ function evaluateJavaScriptExpression(expression, context) {
|
|
|
3651
4287
|
return false;
|
|
3652
4288
|
}
|
|
3653
4289
|
}
|
|
3654
|
-
function evaluateCustomFunction(expression, context) {
|
|
3655
|
-
const fnName = 'functionName' in expression ? expression.functionName : expression.expression;
|
|
3656
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
3657
|
-
if (!('functionName' in expression) && context.deprecationTracker) {
|
|
3658
|
-
warnDeprecated(context.logger, context.deprecationTracker, 'condition:custom:expression', "CustomCondition 'expression' field is deprecated. Use 'functionName' instead.");
|
|
3659
|
-
}
|
|
3660
|
-
const customFn = context.customFunctions?.[fnName];
|
|
3661
|
-
if (!customFn) {
|
|
3662
|
-
context.logger.error('Custom function not found:', fnName);
|
|
3663
|
-
return false;
|
|
3664
|
-
}
|
|
3665
|
-
try {
|
|
3666
|
-
return !!customFn(context);
|
|
3667
|
-
}
|
|
3668
|
-
catch (error) {
|
|
3669
|
-
context.logger.error('Error executing custom function:', fnName, error);
|
|
3670
|
-
return false;
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
4290
|
function evaluateAndCondition(expression, context) {
|
|
3674
4291
|
if (expression.conditions.length === 0)
|
|
3675
4292
|
return false;
|
|
@@ -4578,21 +5195,6 @@ function isStateLogicConfig(config) {
|
|
|
4578
5195
|
function isDerivationLogicConfig(config) {
|
|
4579
5196
|
return config.type === 'derivation';
|
|
4580
5197
|
}
|
|
4581
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
4582
|
-
/**
|
|
4583
|
-
* Type guard to check if a logic config is a PropertyDerivationLogicConfig.
|
|
4584
|
-
*
|
|
4585
|
-
* @deprecated Use `isDerivationLogicConfig(config) && hasTargetProperty(config)` instead.
|
|
4586
|
-
* Will be removed in a future minor version.
|
|
4587
|
-
*
|
|
4588
|
-
* @param config - The logic config to check
|
|
4589
|
-
* @returns true if the config is for property derivation
|
|
4590
|
-
*
|
|
4591
|
-
* @public
|
|
4592
|
-
*/
|
|
4593
|
-
function isPropertyDerivationLogicConfig(config) {
|
|
4594
|
-
return config.type === 'propertyDerivation';
|
|
4595
|
-
}
|
|
4596
5198
|
/**
|
|
4597
5199
|
* Type guard to check if a derivation config targets a property rather than the field value.
|
|
4598
5200
|
*
|
|
@@ -4651,18 +5253,13 @@ function isCrossFieldExpression(expr, context) {
|
|
|
4651
5253
|
case 'fieldValue':
|
|
4652
5254
|
// fieldPath means it references another field's value
|
|
4653
5255
|
return !!expr.fieldPath;
|
|
4654
|
-
case 'formValue':
|
|
4655
|
-
// formValue type always accesses the form state
|
|
4656
|
-
return true;
|
|
4657
5256
|
case 'javascript':
|
|
4658
5257
|
// Check for formValue.* patterns in the expression string
|
|
4659
5258
|
return FORM_VALUE_ACCESS_PATTERN.test(expr.expression || '');
|
|
4660
5259
|
case 'custom': {
|
|
4661
5260
|
// For custom functions, determine scope from registry or default to cross-field.
|
|
4662
|
-
// Support both new API (functionName) and deprecated API (expression).
|
|
4663
|
-
//
|
|
4664
5261
|
// Look up function scope from registry if context is provided
|
|
4665
|
-
const functionName =
|
|
5262
|
+
const functionName = expr.functionName;
|
|
4666
5263
|
if (context?.getFunctionScope && functionName) {
|
|
4667
5264
|
const scope = context.getFunctionScope(functionName);
|
|
4668
5265
|
if (scope === 'field') {
|
|
@@ -4708,10 +5305,6 @@ function extractExpressionDependencies(expr) {
|
|
|
4708
5305
|
deps.add(expr.fieldPath.split('.')[0]);
|
|
4709
5306
|
}
|
|
4710
5307
|
break;
|
|
4711
|
-
case 'formValue':
|
|
4712
|
-
// Full form access - depends on everything
|
|
4713
|
-
deps.add('*');
|
|
4714
|
-
break;
|
|
4715
5308
|
case 'javascript':
|
|
4716
5309
|
// Extract from formValue.* patterns in expression
|
|
4717
5310
|
extractFromExpressionString(expr.expression || '', deps);
|
|
@@ -4826,7 +5419,7 @@ function isCrossFieldBuiltInValidator(config) {
|
|
|
4826
5419
|
* because they require field-level registration.
|
|
4827
5420
|
*/
|
|
4828
5421
|
function isResourceBasedValidator(config) {
|
|
4829
|
-
return config.type === '
|
|
5422
|
+
return config.type === 'async' || config.type === 'http';
|
|
4830
5423
|
}
|
|
4831
5424
|
/**
|
|
4832
5425
|
* Detects if a validator's `when` condition is cross-field.
|
|
@@ -4857,23 +5450,6 @@ function hasCrossFieldWhenCondition(config, context) {
|
|
|
4857
5450
|
function isCrossFieldStateLogic(config, context) {
|
|
4858
5451
|
return isCrossFieldExpression(config.condition, context);
|
|
4859
5452
|
}
|
|
4860
|
-
/**
|
|
4861
|
-
* Detects if a LogicConfig has a cross-field condition.
|
|
4862
|
-
*
|
|
4863
|
-
* @param config The logic configuration to check
|
|
4864
|
-
* @param context Optional context providing function scope lookup
|
|
4865
|
-
* @returns true if the logic condition references other fields
|
|
4866
|
-
*
|
|
4867
|
-
* @deprecated Use `isCrossFieldStateLogic` for state logic configs.
|
|
4868
|
-
* Derivation logic is handled by the derivation system.
|
|
4869
|
-
*/
|
|
4870
|
-
function isCrossFieldLogic(config, context) {
|
|
4871
|
-
// Only check state logic configs
|
|
4872
|
-
if (!isStateLogicConfig(config)) {
|
|
4873
|
-
return false;
|
|
4874
|
-
}
|
|
4875
|
-
return isCrossFieldExpression(config.condition, context);
|
|
4876
|
-
}
|
|
4877
5453
|
// ============================================================================
|
|
4878
5454
|
// Schema Detection
|
|
4879
5455
|
// ============================================================================
|
|
@@ -5696,7 +6272,7 @@ function createHttpDerivationStream(entry, formValue$, context) {
|
|
|
5696
6272
|
// Capture after the guard — TS narrows this to `string` here
|
|
5697
6273
|
const responseExpression = entry.responseExpression;
|
|
5698
6274
|
const debounceMs = entry.debounceMs ?? DEFAULT_HTTP_DEBOUNCE_MS;
|
|
5699
|
-
return formValue$.pipe(startWith(null), pairwise(), map
|
|
6275
|
+
return formValue$.pipe(startWith(null), pairwise(), map(([previous, current]) => ({
|
|
5700
6276
|
current: current,
|
|
5701
6277
|
changedFields: getChangedKeys(previous, current),
|
|
5702
6278
|
})),
|
|
@@ -5705,7 +6281,7 @@ function createHttpDerivationStream(entry, formValue$, context) {
|
|
|
5705
6281
|
if (changedFields.size === 0)
|
|
5706
6282
|
return false;
|
|
5707
6283
|
return entry.dependsOn.some((dep) => changedFields.has(dep));
|
|
5708
|
-
}), debounceTime(debounceMs), switchMap
|
|
6284
|
+
}), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
|
|
5709
6285
|
return new Observable((subscriber) => {
|
|
5710
6286
|
const formAccessor = untracked(() => context.form());
|
|
5711
6287
|
// Check stopOnUserOverride — skip if the user has manually edited the target field
|
|
@@ -5890,7 +6466,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
5890
6466
|
// first emission (null vs actual form value), so every dependsOn field matches.
|
|
5891
6467
|
// This intentionally fires the async derivation on initial form load — same
|
|
5892
6468
|
// pattern as http-derivation-stream.ts.
|
|
5893
|
-
return formValue$.pipe(startWith(null), pairwise(), map
|
|
6469
|
+
return formValue$.pipe(startWith(null), pairwise(), map(([previous, current]) => ({
|
|
5894
6470
|
current: current,
|
|
5895
6471
|
changedFields: getChangedKeys(previous, current),
|
|
5896
6472
|
})),
|
|
@@ -5899,7 +6475,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
5899
6475
|
if (changedFields.size === 0)
|
|
5900
6476
|
return false;
|
|
5901
6477
|
return entry.dependsOn.some((dep) => changedFields.has(dep));
|
|
5902
|
-
}), debounceTime(debounceMs), switchMap
|
|
6478
|
+
}), debounceTime(debounceMs), switchMap(({ current, changedFields }) => {
|
|
5903
6479
|
return new Observable((subscriber) => {
|
|
5904
6480
|
const formAccessor = untracked(() => context.form());
|
|
5905
6481
|
// Check stopOnUserOverride — skip if the user has manually edited the target field
|
|
@@ -6083,7 +6659,7 @@ class DerivationOrchestrator {
|
|
|
6083
6659
|
this.warnAboutWildcardDependencies(collection.entries, fields.length);
|
|
6084
6660
|
this.warnAboutMisconfiguredReEngagement(collection.entries);
|
|
6085
6661
|
return collection;
|
|
6086
|
-
}, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : []));
|
|
6662
|
+
}, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : /* istanbul ignore next */ []));
|
|
6087
6663
|
this.setupOnChangeStream();
|
|
6088
6664
|
this.setupDebouncedStream();
|
|
6089
6665
|
this.setupHttpStreams();
|
|
@@ -6101,7 +6677,7 @@ class DerivationOrchestrator {
|
|
|
6101
6677
|
auditTime(0),
|
|
6102
6678
|
// startWith + pairwise: Track previous and current values to detect changed fields.
|
|
6103
6679
|
// This enables reEngageOnDependencyChange for onChange derivations.
|
|
6104
|
-
startWith(null), pairwise(), filter((pair) => pair[1] !== null), map
|
|
6680
|
+
startWith(null), pairwise(), filter((pair) => pair[1] !== null), map(([previous, current]) => ({
|
|
6105
6681
|
collection: current[0],
|
|
6106
6682
|
formAccessor: current[2],
|
|
6107
6683
|
changedFields: getChangedKeys(previous?.[1] ?? null, current[1]),
|
|
@@ -6129,14 +6705,14 @@ class DerivationOrchestrator {
|
|
|
6129
6705
|
debounceTime(DEFAULT_DEBOUNCE_MS),
|
|
6130
6706
|
// startWith + pairwise: Track previous and current values to detect changes.
|
|
6131
6707
|
// startWith(null) ensures pairwise has an initial value to pair with.
|
|
6132
|
-
startWith(null), pairwise(), filter((pair) => pair[1] !== null), map
|
|
6708
|
+
startWith(null), pairwise(), filter((pair) => pair[1] !== null), map(([previous, current]) => ({
|
|
6133
6709
|
current,
|
|
6134
6710
|
changedFields: getChangedKeys(previous, current),
|
|
6135
6711
|
})), filter(({ changedFields }) => changedFields.size > 0),
|
|
6136
6712
|
// switchMap: For debounced derivations, it's OK to cancel pending work
|
|
6137
6713
|
// if new changes come in - we want the latest debounced values.
|
|
6138
6714
|
// (Unlike onChange which uses exhaustMap to prevent cancellation)
|
|
6139
|
-
switchMap
|
|
6715
|
+
switchMap(({ changedFields }) => {
|
|
6140
6716
|
const collection = untracked(() => this.derivationCollection());
|
|
6141
6717
|
const formAccessor = untracked(() => this.config.form());
|
|
6142
6718
|
if (!collection || !formAccessor)
|
|
@@ -6157,11 +6733,11 @@ class DerivationOrchestrator {
|
|
|
6157
6733
|
createPeriodStream(debounceMs, collection, formAccessor, changedFields) {
|
|
6158
6734
|
const additionalWait = Math.max(0, debounceMs - DEFAULT_DEBOUNCE_MS);
|
|
6159
6735
|
if (additionalWait === 0) {
|
|
6160
|
-
return of(null).pipe(map
|
|
6736
|
+
return of(null).pipe(map(() => {
|
|
6161
6737
|
this.applyDebouncedEntriesForPeriod(debounceMs, collection, formAccessor, changedFields);
|
|
6162
6738
|
}));
|
|
6163
6739
|
}
|
|
6164
|
-
return timer(additionalWait).pipe(map
|
|
6740
|
+
return timer(additionalWait).pipe(map(() => {
|
|
6165
6741
|
const currentCollection = untracked(() => this.derivationCollection());
|
|
6166
6742
|
const currentFormAccessor = untracked(() => this.config.form());
|
|
6167
6743
|
if (currentCollection && currentFormAccessor) {
|
|
@@ -6658,9 +7234,9 @@ function computePropertyValue(entry, evalContext, applicatorContext) {
|
|
|
6658
7234
|
}
|
|
6659
7235
|
// Custom function
|
|
6660
7236
|
if (entry.functionName) {
|
|
6661
|
-
const fn = applicatorContext.
|
|
7237
|
+
const fn = applicatorContext.derivationFunctions?.[entry.functionName];
|
|
6662
7238
|
if (!fn) {
|
|
6663
|
-
throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.derivations
|
|
7239
|
+
throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
|
|
6664
7240
|
}
|
|
6665
7241
|
return fn(evalContext);
|
|
6666
7242
|
}
|
|
@@ -6707,7 +7283,7 @@ function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
|
|
|
6707
7283
|
/**
|
|
6708
7284
|
* Collects all property derivation entries from field definitions.
|
|
6709
7285
|
*
|
|
6710
|
-
* Traverses the field definition tree and extracts `type: '
|
|
7286
|
+
* Traverses the field definition tree and extracts `type: 'derivation'` with `targetProperty`
|
|
6711
7287
|
* entries from each field's `logic` array.
|
|
6712
7288
|
*
|
|
6713
7289
|
* No topological sort is needed because property derivations don't chain —
|
|
@@ -6767,46 +7343,13 @@ function collectFromField$1(field, entries, context) {
|
|
|
6767
7343
|
const validationField = field;
|
|
6768
7344
|
if (validationField.logic) {
|
|
6769
7345
|
for (const logicConfig of validationField.logic) {
|
|
6770
|
-
if (
|
|
6771
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
6772
|
-
// Deprecated path — emit warning, then process normally
|
|
6773
|
-
warnDeprecated(context.logger, context.tracker, 'type:propertyDerivation', "Logic type 'propertyDerivation' is deprecated. Use type: 'derivation' with targetProperty instead.");
|
|
6774
|
-
const entry = createPropertyDerivationEntry(fieldKey, logicConfig, context);
|
|
6775
|
-
entries.push(entry);
|
|
6776
|
-
}
|
|
6777
|
-
else if (isDerivationLogicConfig(logicConfig) && hasTargetProperty(logicConfig)) {
|
|
6778
|
-
// New unified path: type: 'derivation' + targetProperty → property derivation pipeline
|
|
7346
|
+
if (isDerivationLogicConfig(logicConfig) && hasTargetProperty(logicConfig)) {
|
|
6779
7347
|
const entry = createPropertyDerivationEntryFromDerivation(fieldKey, logicConfig, context);
|
|
6780
7348
|
entries.push(entry);
|
|
6781
7349
|
}
|
|
6782
7350
|
}
|
|
6783
7351
|
}
|
|
6784
7352
|
}
|
|
6785
|
-
/**
|
|
6786
|
-
* Creates a property derivation entry from a `PropertyDerivationLogicConfig`.
|
|
6787
|
-
*
|
|
6788
|
-
* @internal
|
|
6789
|
-
*/
|
|
6790
|
-
function createPropertyDerivationEntry(fieldKey, config, context) {
|
|
6791
|
-
const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
|
|
6792
|
-
const dependsOn = extractDependencies(config);
|
|
6793
|
-
const condition = config.condition ?? true;
|
|
6794
|
-
const trigger = config.trigger ?? 'onChange';
|
|
6795
|
-
const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
|
|
6796
|
-
return {
|
|
6797
|
-
fieldKey: effectiveFieldKey,
|
|
6798
|
-
targetProperty: config.targetProperty,
|
|
6799
|
-
dependsOn,
|
|
6800
|
-
condition,
|
|
6801
|
-
value: config.value,
|
|
6802
|
-
expression: config.expression,
|
|
6803
|
-
functionName: config.functionName,
|
|
6804
|
-
trigger,
|
|
6805
|
-
debounceMs,
|
|
6806
|
-
debugName: config.debugName,
|
|
6807
|
-
originalConfig: config,
|
|
6808
|
-
};
|
|
6809
|
-
}
|
|
6810
7353
|
/**
|
|
6811
7354
|
* Creates a property derivation entry from a `DerivationLogicConfig` that has `targetProperty`.
|
|
6812
7355
|
*
|
|
@@ -6899,7 +7442,7 @@ class PropertyDerivationOrchestrator {
|
|
|
6899
7442
|
return null;
|
|
6900
7443
|
}
|
|
6901
7444
|
return collection;
|
|
6902
|
-
}, ...(ngDevMode ? [{ debugName: "propertyDerivationCollection" }] : []));
|
|
7445
|
+
}, ...(ngDevMode ? [{ debugName: "propertyDerivationCollection" }] : /* istanbul ignore next */ []));
|
|
6903
7446
|
// Side effects react to collection changes: clear the store, register fields, warn.
|
|
6904
7447
|
// schemaFields is included in the dependency array to ensure the field count is always
|
|
6905
7448
|
// in sync with the collection (avoids reading a stale value via untracked).
|
|
@@ -6929,10 +7472,10 @@ class PropertyDerivationOrchestrator {
|
|
|
6929
7472
|
}
|
|
6930
7473
|
setupDebouncedStream() {
|
|
6931
7474
|
toObservable(this.config.formValue, { injector: this.injector })
|
|
6932
|
-
.pipe(debounceTime(DEFAULT_DEBOUNCE_MS), startWith(null), pairwise(), filter((pair) => pair[1] !== null), map
|
|
7475
|
+
.pipe(debounceTime(DEFAULT_DEBOUNCE_MS), startWith(null), pairwise(), filter((pair) => pair[1] !== null), map(([previous, current]) => ({
|
|
6933
7476
|
current,
|
|
6934
7477
|
changedFields: getChangedKeys(previous, current),
|
|
6935
|
-
})), filter(({ changedFields }) => changedFields.size > 0), switchMap
|
|
7478
|
+
})), filter(({ changedFields }) => changedFields.size > 0), switchMap(({ changedFields }) => {
|
|
6936
7479
|
const collection = untracked(() => this.propertyDerivationCollection());
|
|
6937
7480
|
if (!collection)
|
|
6938
7481
|
return of(null);
|
|
@@ -6948,7 +7491,7 @@ class PropertyDerivationOrchestrator {
|
|
|
6948
7491
|
}
|
|
6949
7492
|
createPeriodStream(debounceMs, collection, changedFields) {
|
|
6950
7493
|
const additionalWait = Math.max(0, debounceMs - DEFAULT_DEBOUNCE_MS);
|
|
6951
|
-
return timer(additionalWait).pipe(map
|
|
7494
|
+
return timer(additionalWait).pipe(map(() => {
|
|
6952
7495
|
// For periods beyond DEFAULT_DEBOUNCE_MS, re-read collection as it may have changed during the wait.
|
|
6953
7496
|
// For zero-wait periods, use the original collection directly since no time has elapsed.
|
|
6954
7497
|
const currentCollection = additionalWait > 0 ? (untracked(() => this.propertyDerivationCollection()) ?? collection) : collection;
|
|
@@ -6959,7 +7502,7 @@ class PropertyDerivationOrchestrator {
|
|
|
6959
7502
|
const applicatorContext = {
|
|
6960
7503
|
formValue: this.config.formValue,
|
|
6961
7504
|
store: this.config.store,
|
|
6962
|
-
|
|
7505
|
+
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
6963
7506
|
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
6964
7507
|
externalData: this.resolveExternalData(),
|
|
6965
7508
|
logger: this.logger,
|
|
@@ -6975,7 +7518,7 @@ class PropertyDerivationOrchestrator {
|
|
|
6975
7518
|
const applicatorContext = {
|
|
6976
7519
|
formValue: this.config.formValue,
|
|
6977
7520
|
store: this.config.store,
|
|
6978
|
-
|
|
7521
|
+
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
6979
7522
|
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
6980
7523
|
externalData: this.resolveExternalData(),
|
|
6981
7524
|
logger: this.logger,
|
|
@@ -7297,6 +7840,40 @@ class ShiftArrayItemEvent {
|
|
|
7297
7840
|
}
|
|
7298
7841
|
}
|
|
7299
7842
|
|
|
7843
|
+
/**
|
|
7844
|
+
* Event dispatched to move an existing item from one index to another within an array field.
|
|
7845
|
+
*
|
|
7846
|
+
* This is an atomic reorder operation — the item at `fromIndex` is removed and
|
|
7847
|
+
* reinserted at `toIndex`. No template is required because the existing item
|
|
7848
|
+
* (resolved component, form value, and stored template) is preserved.
|
|
7849
|
+
*
|
|
7850
|
+
* @example
|
|
7851
|
+
* ```typescript
|
|
7852
|
+
* // Use the builder API (recommended)
|
|
7853
|
+
* eventBus.dispatch(arrayEvent('contacts').move(0, 2));
|
|
7854
|
+
*
|
|
7855
|
+
* // Or instantiate directly
|
|
7856
|
+
* eventBus.dispatch(new MoveArrayItemEvent('contacts', 0, 2));
|
|
7857
|
+
* ```
|
|
7858
|
+
*/
|
|
7859
|
+
class MoveArrayItemEvent {
|
|
7860
|
+
arrayKey;
|
|
7861
|
+
fromIndex;
|
|
7862
|
+
toIndex;
|
|
7863
|
+
type = 'move-array-item';
|
|
7864
|
+
constructor(
|
|
7865
|
+
/** The key of the array field containing the item to move */
|
|
7866
|
+
arrayKey,
|
|
7867
|
+
/** The current index of the item to move */
|
|
7868
|
+
fromIndex,
|
|
7869
|
+
/** The target index to move the item to */
|
|
7870
|
+
toIndex) {
|
|
7871
|
+
this.arrayKey = arrayKey;
|
|
7872
|
+
this.fromIndex = fromIndex;
|
|
7873
|
+
this.toIndex = toIndex;
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
|
|
7300
7877
|
/**
|
|
7301
7878
|
* Event dispatched to remove an item at a SPECIFIC INDEX from an array field.
|
|
7302
7879
|
*
|
|
@@ -7369,17 +7946,17 @@ class PageOrchestratorComponent {
|
|
|
7369
7946
|
/**
|
|
7370
7947
|
* Array of page field definitions to render
|
|
7371
7948
|
*/
|
|
7372
|
-
pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : []));
|
|
7949
|
+
pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : /* istanbul ignore next */ []));
|
|
7373
7950
|
/**
|
|
7374
7951
|
* Root form instance from parent DynamicForm.
|
|
7375
7952
|
* Uses FieldTree<unknown> to accept any form type.
|
|
7376
7953
|
*/
|
|
7377
|
-
form = input.required(...(ngDevMode ? [{ debugName: "form" }] : []));
|
|
7954
|
+
form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
|
|
7378
7955
|
/**
|
|
7379
7956
|
* Field signal context for child fields
|
|
7380
7957
|
*/
|
|
7381
7958
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FieldSignalContext is contravariant in TModel, using any allows accepting any form type
|
|
7382
|
-
fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
|
|
7959
|
+
fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : /* istanbul ignore next */ []));
|
|
7383
7960
|
/**
|
|
7384
7961
|
* Computed signal that tracks which pages are hidden.
|
|
7385
7962
|
* Returns an array of booleans where true means the page is hidden.
|
|
@@ -7388,7 +7965,7 @@ class PageOrchestratorComponent {
|
|
|
7388
7965
|
pageHiddenStates = computed(() => {
|
|
7389
7966
|
const pages = this.pageFields();
|
|
7390
7967
|
return pages.map((page) => this.evaluatePageHidden(page));
|
|
7391
|
-
}, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : []));
|
|
7968
|
+
}, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : /* istanbul ignore next */ []));
|
|
7392
7969
|
/**
|
|
7393
7970
|
* Computed signal that returns indices of visible (non-hidden) pages.
|
|
7394
7971
|
* This is used for navigation to skip hidden pages.
|
|
@@ -7399,7 +7976,7 @@ class PageOrchestratorComponent {
|
|
|
7399
7976
|
.map((hidden, index) => ({ index, hidden }))
|
|
7400
7977
|
.filter((item) => !item.hidden)
|
|
7401
7978
|
.map((item) => item.index);
|
|
7402
|
-
}, ...(ngDevMode ? [{ debugName: "visiblePageIndices" }] : []));
|
|
7979
|
+
}, ...(ngDevMode ? [{ debugName: "visiblePageIndices" }] : /* istanbul ignore next */ []));
|
|
7403
7980
|
/**
|
|
7404
7981
|
* Internal signal for current page index that tracks with page fields.
|
|
7405
7982
|
* This tracks the actual page index, not the visible page index.
|
|
@@ -7414,7 +7991,7 @@ class PageOrchestratorComponent {
|
|
|
7414
7991
|
return 0;
|
|
7415
7992
|
// Start on page 0 (will be adjusted if page 0 is hidden by initial navigation)
|
|
7416
7993
|
return 0;
|
|
7417
|
-
}, ...(ngDevMode ? [{ debugName: "currentPageIndex" }] : []));
|
|
7994
|
+
}, ...(ngDevMode ? [{ debugName: "currentPageIndex" }] : /* istanbul ignore next */ []));
|
|
7418
7995
|
/**
|
|
7419
7996
|
* Computed state for the orchestrator
|
|
7420
7997
|
*/
|
|
@@ -7433,7 +8010,7 @@ class PageOrchestratorComponent {
|
|
|
7433
8010
|
isLastPage: isLastVisiblePage,
|
|
7434
8011
|
navigationDisabled: false,
|
|
7435
8012
|
};
|
|
7436
|
-
}, ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
8013
|
+
}, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
7437
8014
|
/**
|
|
7438
8015
|
* Signal indicating whether all fields on the current page are valid.
|
|
7439
8016
|
*
|
|
@@ -7467,7 +8044,7 @@ class PageOrchestratorComponent {
|
|
|
7467
8044
|
}
|
|
7468
8045
|
}
|
|
7469
8046
|
return true;
|
|
7470
|
-
}, ...(ngDevMode ? [{ debugName: "currentPageValid" }] : []));
|
|
8047
|
+
}, ...(ngDevMode ? [{ debugName: "currentPageValid" }] : /* istanbul ignore next */ []));
|
|
7471
8048
|
/**
|
|
7472
8049
|
* Extended field signal context that includes currentPageValid.
|
|
7473
8050
|
*
|
|
@@ -7478,7 +8055,7 @@ class PageOrchestratorComponent {
|
|
|
7478
8055
|
extendedFieldSignalContext = computed(() => ({
|
|
7479
8056
|
...this.fieldSignalContext(),
|
|
7480
8057
|
currentPageValid: this.currentPageValid,
|
|
7481
|
-
}), ...(ngDevMode ? [{ debugName: "extendedFieldSignalContext" }] : []));
|
|
8058
|
+
}), ...(ngDevMode ? [{ debugName: "extendedFieldSignalContext" }] : /* istanbul ignore next */ []));
|
|
7482
8059
|
constructor() {
|
|
7483
8060
|
// Setup event listeners for navigation
|
|
7484
8061
|
this.setupEventListeners();
|
|
@@ -7685,8 +8262,8 @@ class PageOrchestratorComponent {
|
|
|
7685
8262
|
}
|
|
7686
8263
|
return false;
|
|
7687
8264
|
}
|
|
7688
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
7689
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
8265
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8266
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", 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: [
|
|
7690
8267
|
{
|
|
7691
8268
|
provide: FIELD_SIGNAL_CONTEXT,
|
|
7692
8269
|
useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
|
|
@@ -7716,9 +8293,9 @@ class PageOrchestratorComponent {
|
|
|
7716
8293
|
}
|
|
7717
8294
|
}
|
|
7718
8295
|
}
|
|
7719
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
8296
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs').then(m => m.default)]] });
|
|
7720
8297
|
}
|
|
7721
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.
|
|
8298
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
7722
8299
|
type: Component,
|
|
7723
8300
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
7724
8301
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -7870,7 +8447,7 @@ function validateSimplifiedTemplate(field) {
|
|
|
7870
8447
|
* Expands a simplified array field into a full ArrayField + optional add button.
|
|
7871
8448
|
*/
|
|
7872
8449
|
function expandSimplifiedArray(field) {
|
|
7873
|
-
const { template, value = [], addButton, removeButton, key, logic, minLength, maxLength } = field;
|
|
8450
|
+
const { template, value = [], addButton, removeButton, key, logic, minLength, maxLength, wrappers, skipAutoWrappers, skipDefaultWrappers, } = field;
|
|
7874
8451
|
const isObjectTemplate = Array.isArray(template);
|
|
7875
8452
|
const values = value;
|
|
7876
8453
|
// Validate template before expansion
|
|
@@ -7907,6 +8484,15 @@ function expandSimplifiedArray(field) {
|
|
|
7907
8484
|
if (maxLength !== undefined) {
|
|
7908
8485
|
arrayFieldObj['maxLength'] = maxLength;
|
|
7909
8486
|
}
|
|
8487
|
+
if (wrappers !== undefined) {
|
|
8488
|
+
arrayFieldObj['wrappers'] = wrappers;
|
|
8489
|
+
}
|
|
8490
|
+
if (skipAutoWrappers !== undefined) {
|
|
8491
|
+
arrayFieldObj['skipAutoWrappers'] = skipAutoWrappers;
|
|
8492
|
+
}
|
|
8493
|
+
if (skipDefaultWrappers !== undefined) {
|
|
8494
|
+
arrayFieldObj['skipDefaultWrappers'] = skipDefaultWrappers;
|
|
8495
|
+
}
|
|
7910
8496
|
// Safe cast: we're constructing a valid ArrayField shape with key, type, and fields
|
|
7911
8497
|
const arrayField = arrayFieldObj;
|
|
7912
8498
|
// Store normalization metadata via Symbol property instead of a runtime property
|
|
@@ -7989,7 +8575,7 @@ function buildRemoveButton(config) {
|
|
|
7989
8575
|
* Type guard to check if a field is a container with fields
|
|
7990
8576
|
*/
|
|
7991
8577
|
function isContainerWithFields(field) {
|
|
7992
|
-
return ((field.type === 'row' || field.type === 'group' || field.type === 'page') &&
|
|
8578
|
+
return ((field.type === 'row' || field.type === 'group' || field.type === 'page' || field.type === 'container') &&
|
|
7993
8579
|
'fields' in field &&
|
|
7994
8580
|
Array.isArray(field.fields));
|
|
7995
8581
|
}
|
|
@@ -8244,6 +8830,83 @@ class HttpConditionFunctionCacheService {
|
|
|
8244
8830
|
httpConditionFunctionCache = new Map();
|
|
8245
8831
|
}
|
|
8246
8832
|
|
|
8833
|
+
/**
|
|
8834
|
+
* Wraps a `Resource` so that the previous resolved value is preserved while the resource
|
|
8835
|
+
* is loading or reloading. This prevents UI flicker when params change and a new request
|
|
8836
|
+
* is in-flight — the consumer sees the stale value with a `loading`/`reloading` status
|
|
8837
|
+
* instead of `undefined`.
|
|
8838
|
+
*
|
|
8839
|
+
* Uses Angular's `resource.snapshot` + `resourceFromSnapshots` composition pattern.
|
|
8840
|
+
*
|
|
8841
|
+
* @example
|
|
8842
|
+
* ```typescript
|
|
8843
|
+
* const user = withPreviousValue(
|
|
8844
|
+
* resource({ params: () => userId(), loader: ({ params }) => fetchUser(params) })
|
|
8845
|
+
* );
|
|
8846
|
+
* // user.value() keeps the old user data during loading transitions
|
|
8847
|
+
* ```
|
|
8848
|
+
*
|
|
8849
|
+
* @experimental Uses Angular's experimental resource composition APIs.
|
|
8850
|
+
*/
|
|
8851
|
+
function withPreviousValue(input) {
|
|
8852
|
+
const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
|
|
8853
|
+
computation: (snapshot, previous) => {
|
|
8854
|
+
// During loading/reloading, keep the previous resolved value if available.
|
|
8855
|
+
// Don't preserve values from error states — those aren't useful to show.
|
|
8856
|
+
if ((snapshot.status === 'loading' || snapshot.status === 'reloading') &&
|
|
8857
|
+
previous !== undefined &&
|
|
8858
|
+
previous.value.status !== 'error') {
|
|
8859
|
+
return { status: snapshot.status, value: previous.value.value };
|
|
8860
|
+
}
|
|
8861
|
+
return snapshot;
|
|
8862
|
+
} });
|
|
8863
|
+
return resourceFromSnapshots(derived);
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8866
|
+
/**
|
|
8867
|
+
* Type guard to check if a value is an array of Signals.
|
|
8868
|
+
* @internal
|
|
8869
|
+
*/
|
|
8870
|
+
function isSignalArray(value) {
|
|
8871
|
+
return Array.isArray(value) && value.every((item) => isSignal(item));
|
|
8872
|
+
}
|
|
8873
|
+
/**
|
|
8874
|
+
* Type guard to check if a value is a record (object) of Signals.
|
|
8875
|
+
* @internal
|
|
8876
|
+
*/
|
|
8877
|
+
function isSignalRecord(value) {
|
|
8878
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
|
|
8879
|
+
}
|
|
8880
|
+
// Implementation
|
|
8881
|
+
function derivedFromDeferred(source, operator, options) {
|
|
8882
|
+
const deferred$ = defer(() => {
|
|
8883
|
+
// Handle single signal
|
|
8884
|
+
if (isSignal(source)) {
|
|
8885
|
+
return toObservable(source, { injector: options.injector });
|
|
8886
|
+
}
|
|
8887
|
+
// Handle array of signals
|
|
8888
|
+
if (isSignalArray(source)) {
|
|
8889
|
+
const observables = source.map((s) => toObservable(s, { injector: options.injector }));
|
|
8890
|
+
return combineLatest(observables);
|
|
8891
|
+
}
|
|
8892
|
+
// Handle object of signals
|
|
8893
|
+
if (isSignalRecord(source)) {
|
|
8894
|
+
const keys = Object.keys(source);
|
|
8895
|
+
const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
|
|
8896
|
+
return combineLatest(observables, (...values) => {
|
|
8897
|
+
const result = {};
|
|
8898
|
+
keys.forEach((key, index) => {
|
|
8899
|
+
result[key] = values[index];
|
|
8900
|
+
});
|
|
8901
|
+
return result;
|
|
8902
|
+
});
|
|
8903
|
+
}
|
|
8904
|
+
// Fallback - should not happen with proper typing
|
|
8905
|
+
throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
|
|
8906
|
+
}).pipe(operator);
|
|
8907
|
+
return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
|
|
8908
|
+
}
|
|
8909
|
+
|
|
8247
8910
|
/**
|
|
8248
8911
|
* Extracts a boolean from an HTTP response using an optional expression.
|
|
8249
8912
|
* When `responseExpression` is provided, evaluates it with `{ response }` scope.
|
|
@@ -8269,10 +8932,10 @@ function extractBoolean(response, responseExpression, pendingValue, logger) {
|
|
|
8269
8932
|
/**
|
|
8270
8933
|
* Creates a logic function for an HTTP condition.
|
|
8271
8934
|
*
|
|
8272
|
-
* Uses
|
|
8273
|
-
*
|
|
8274
|
-
*
|
|
8275
|
-
*
|
|
8935
|
+
* Uses Angular's `rxResource()` API with snapshot composition for async resolution:
|
|
8936
|
+
* the LogicFn updates a `resolvedRequest` signal, a debounced version feeds into
|
|
8937
|
+
* an `rxResource()` that manages the HTTP lifecycle, and `withPreviousValue()` preserves
|
|
8938
|
+
* the last resolved boolean during re-fetching to prevent UI flicker.
|
|
8276
8939
|
*
|
|
8277
8940
|
* Must be called in injection context (same as `createLogicFunction`).
|
|
8278
8941
|
*/
|
|
@@ -8303,30 +8966,45 @@ function createHttpConditionLogicFunction(condition) {
|
|
|
8303
8966
|
const contextKey = safeReadPathKeys(ctx).join('.');
|
|
8304
8967
|
let signalPair = perFunctionSignalStore.get(contextKey);
|
|
8305
8968
|
if (!signalPair) {
|
|
8306
|
-
const resolvedRequest = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedRequest" }] : []));
|
|
8307
|
-
// Wrap in untracked() to avoid NG0602:
|
|
8969
|
+
const resolvedRequest = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedRequest" }] : /* istanbul ignore next */ []));
|
|
8970
|
+
// Wrap in untracked() to avoid NG0602: resource() internally creates effects,
|
|
8308
8971
|
// which cannot be created inside a reactive context (computed). The LogicFn runs
|
|
8309
8972
|
// inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
|
|
8310
|
-
// clear the active reactive consumer before creating the
|
|
8311
|
-
const
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8973
|
+
// clear the active reactive consumer before creating the resource pipeline.
|
|
8974
|
+
const resultResource = untracked(() => {
|
|
8975
|
+
// Create a debounced, stabilized version of the request signal.
|
|
8976
|
+
// This prevents rapid re-fetches when form values change quickly.
|
|
8977
|
+
const debouncedRequest = derivedFromDeferred(resolvedRequest, pipe(debounceTime(debounceMs), distinctUntilChanged((prev, curr) => stableStringify(prev) === stableStringify(curr))), { initialValue: undefined, injector });
|
|
8978
|
+
// rxResource() manages the HTTP lifecycle natively with Observables:
|
|
8979
|
+
// auto-cancellation via unsubscription, loading/resolved/error status tracking,
|
|
8980
|
+
// and signal-based reactivity.
|
|
8981
|
+
const httpResource = rxResource({
|
|
8982
|
+
params: () => debouncedRequest() ?? undefined,
|
|
8983
|
+
// When params() returns undefined, rxResource() enters idle state and skips the stream.
|
|
8984
|
+
stream: ({ params: request }) => {
|
|
8985
|
+
const method = request.method ?? 'GET';
|
|
8986
|
+
const options = {};
|
|
8987
|
+
if (request.body)
|
|
8988
|
+
options['body'] = request.body;
|
|
8989
|
+
if (request.headers)
|
|
8990
|
+
options['headers'] = request.headers;
|
|
8991
|
+
return httpClient.request(method, request.url, options).pipe(map((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)), tap((value) => {
|
|
8992
|
+
const requestKey = stableStringify(request);
|
|
8993
|
+
cache.set(requestKey, value, cacheDurationMs);
|
|
8994
|
+
}), catchError((error) => {
|
|
8995
|
+
logger.warn('HTTP condition request failed:', error);
|
|
8996
|
+
return of(pendingValue);
|
|
8997
|
+
}));
|
|
8998
|
+
},
|
|
8999
|
+
defaultValue: pendingValue,
|
|
9000
|
+
injector,
|
|
9001
|
+
});
|
|
9002
|
+
// Snapshot composition: preserve the previous boolean result during re-fetching.
|
|
9003
|
+
// Without this, the condition would briefly flash to pendingValue when params change,
|
|
9004
|
+
// causing visible UI flicker (e.g., a conditionally visible field briefly disappearing).
|
|
9005
|
+
return withPreviousValue(httpResource);
|
|
8328
9006
|
});
|
|
8329
|
-
signalPair = { resolvedRequest,
|
|
9007
|
+
signalPair = { resolvedRequest, resultResource };
|
|
8330
9008
|
perFunctionSignalStore.set(contextKey, signalPair);
|
|
8331
9009
|
}
|
|
8332
9010
|
// Build reactive evaluation context (creates signal dependencies on form values)
|
|
@@ -8334,7 +9012,7 @@ function createHttpConditionLogicFunction(condition) {
|
|
|
8334
9012
|
// Returns null when a path param is undefined — skip the request and return pending value
|
|
8335
9013
|
const resolved = resolveHttpRequest(condition.http, evaluationContext);
|
|
8336
9014
|
if (!resolved) {
|
|
8337
|
-
return signalPair.
|
|
9015
|
+
return signalPair.resultResource.value();
|
|
8338
9016
|
}
|
|
8339
9017
|
const requestKey = stableStringify(resolved);
|
|
8340
9018
|
// Check response cache first
|
|
@@ -8342,15 +9020,15 @@ function createHttpConditionLogicFunction(condition) {
|
|
|
8342
9020
|
if (cachedResult !== undefined) {
|
|
8343
9021
|
return cachedResult;
|
|
8344
9022
|
}
|
|
8345
|
-
// Update the resolved request signal (triggers
|
|
8346
|
-
const { resolvedRequest,
|
|
9023
|
+
// Update the resolved request signal (triggers resource if request changed)
|
|
9024
|
+
const { resolvedRequest, resultResource } = signalPair;
|
|
8347
9025
|
untracked(() => {
|
|
8348
9026
|
const current = resolvedRequest();
|
|
8349
9027
|
if (stableStringify(current) !== requestKey) {
|
|
8350
9028
|
resolvedRequest.set(resolved);
|
|
8351
9029
|
}
|
|
8352
9030
|
});
|
|
8353
|
-
return
|
|
9031
|
+
return resultResource.value();
|
|
8354
9032
|
};
|
|
8355
9033
|
cacheService.httpConditionFunctionCache.set(cacheKey, fn);
|
|
8356
9034
|
return fn;
|
|
@@ -8374,10 +9052,11 @@ class AsyncConditionFunctionCacheService {
|
|
|
8374
9052
|
/**
|
|
8375
9053
|
* Creates a logic function for an async condition.
|
|
8376
9054
|
*
|
|
8377
|
-
* Uses
|
|
8378
|
-
*
|
|
8379
|
-
*
|
|
8380
|
-
*
|
|
9055
|
+
* Uses Angular's `rxResource()` API with snapshot composition for async resolution:
|
|
9056
|
+
* the LogicFn updates a `trigger` signal (serialized evaluation context), a debounced
|
|
9057
|
+
* version feeds into an `rxResource()` that manages the async function lifecycle, and
|
|
9058
|
+
* `withPreviousValue()` preserves the last resolved boolean during re-evaluation
|
|
9059
|
+
* to prevent UI flicker.
|
|
8381
9060
|
*
|
|
8382
9061
|
* Must be called in injection context (same as `createLogicFunction`).
|
|
8383
9062
|
*/
|
|
@@ -8401,33 +9080,53 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
8401
9080
|
const contextKey = safeReadPathKeys(ctx).join('.');
|
|
8402
9081
|
let signalPair = perFunctionSignalStore.get(contextKey);
|
|
8403
9082
|
if (!signalPair) {
|
|
8404
|
-
const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
|
|
8405
|
-
|
|
9083
|
+
const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
9084
|
+
const ctxByTrigger = new Map();
|
|
9085
|
+
// Wrap in untracked() to avoid NG0602: resource() internally creates effects,
|
|
8406
9086
|
// which cannot be created inside a reactive context (computed). The LogicFn runs
|
|
8407
9087
|
// inside Angular Signal Forms' BooleanOrLogic.compute (a computed), so we must
|
|
8408
|
-
// clear the active reactive consumer before creating the
|
|
8409
|
-
const
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
9088
|
+
// clear the active reactive consumer before creating the resource pipeline.
|
|
9089
|
+
const resultResource = untracked(() => {
|
|
9090
|
+
// Debounce the trigger to batch rapid form value changes.
|
|
9091
|
+
const debouncedTrigger = derivedFromDeferred(trigger, pipe(debounceTime(debounceMs), distinctUntilChanged()), {
|
|
9092
|
+
initialValue: undefined,
|
|
9093
|
+
injector,
|
|
9094
|
+
});
|
|
9095
|
+
// rxResource() manages the async function lifecycle natively with Observables:
|
|
9096
|
+
// auto-cancellation via unsubscription and signal-based reactivity.
|
|
9097
|
+
const asyncResource = rxResource({
|
|
9098
|
+
params: () => debouncedTrigger() ?? undefined,
|
|
9099
|
+
stream: ({ params: triggerKey }) => {
|
|
9100
|
+
// Look up the async function at call time (fresh reference)
|
|
9101
|
+
const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
|
|
9102
|
+
if (!asyncFn) {
|
|
9103
|
+
logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
|
|
9104
|
+
`Register it in customFnConfig.asyncConditions.`);
|
|
9105
|
+
return of(pendingValue);
|
|
9106
|
+
}
|
|
9107
|
+
// Retrieve the FieldContext that was captured when this trigger key was set.
|
|
9108
|
+
// This avoids a race condition where a mutable ref could be overwritten by
|
|
9109
|
+
// a later LogicFn call before the loader finishes executing.
|
|
9110
|
+
const capturedCtx = ctxByTrigger.get(triggerKey);
|
|
9111
|
+
if (!capturedCtx)
|
|
9112
|
+
return of(pendingValue);
|
|
9113
|
+
const evaluationContext = untracked(() => fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions()));
|
|
9114
|
+
// asyncFn may return Promise<boolean> or Observable<boolean>.
|
|
9115
|
+
// from() normalizes both to Observable.
|
|
9116
|
+
return from(asyncFn(evaluationContext)).pipe(map((result) => !!result), catchError((error) => {
|
|
9117
|
+
logger.warn('Async Condition - function failed:', error);
|
|
9118
|
+
return of(pendingValue);
|
|
9119
|
+
}));
|
|
9120
|
+
},
|
|
9121
|
+
defaultValue: pendingValue,
|
|
9122
|
+
injector,
|
|
9123
|
+
});
|
|
9124
|
+
// Snapshot composition: preserve the previous boolean result during re-evaluation.
|
|
9125
|
+
// Without this, the condition would briefly flash to pendingValue when the async
|
|
9126
|
+
// function is re-invoked, causing visible UI flicker.
|
|
9127
|
+
return withPreviousValue(asyncResource);
|
|
8429
9128
|
});
|
|
8430
|
-
signalPair = { trigger,
|
|
9129
|
+
signalPair = { trigger, resultResource, ctxByTrigger };
|
|
8431
9130
|
perFunctionSignalStore.set(contextKey, signalPair);
|
|
8432
9131
|
}
|
|
8433
9132
|
// Build reactive evaluation context (creates signal dependencies on form values)
|
|
@@ -8438,15 +9137,18 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
8438
9137
|
fieldValue: evaluationContext.fieldValue,
|
|
8439
9138
|
externalData: evaluationContext.externalData,
|
|
8440
9139
|
});
|
|
8441
|
-
//
|
|
8442
|
-
|
|
9140
|
+
// Capture the FieldContext for this trigger key so the loader can retrieve it.
|
|
9141
|
+
// Only keep the latest — previous entries are stale once the trigger changes.
|
|
9142
|
+
const { trigger: triggerSignal, resultResource, ctxByTrigger } = signalPair;
|
|
9143
|
+
ctxByTrigger.clear();
|
|
9144
|
+
ctxByTrigger.set(contextSnapshot, ctx);
|
|
8443
9145
|
untracked(() => {
|
|
8444
9146
|
const current = triggerSignal();
|
|
8445
9147
|
if (current !== contextSnapshot) {
|
|
8446
9148
|
triggerSignal.set(contextSnapshot);
|
|
8447
9149
|
}
|
|
8448
9150
|
});
|
|
8449
|
-
return
|
|
9151
|
+
return resultResource.value();
|
|
8450
9152
|
};
|
|
8451
9153
|
cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
|
|
8452
9154
|
return fn;
|
|
@@ -8574,7 +9276,7 @@ function createDebouncedLogicFunction(expression, debounceMs) {
|
|
|
8574
9276
|
let signalPair = cacheService.debouncedSignalStore.get(contextKey);
|
|
8575
9277
|
if (!signalPair) {
|
|
8576
9278
|
// Create a signal to hold the immediate evaluation result
|
|
8577
|
-
const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : []));
|
|
9279
|
+
const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : /* istanbul ignore next */ []));
|
|
8578
9280
|
// Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),
|
|
8579
9281
|
// which cannot be created inside a reactive context (computed). The LogicFn runs
|
|
8580
9282
|
// inside Angular Signal Forms' BooleanOrLogic.compute (a computed).
|
|
@@ -8758,22 +9460,6 @@ function applyValidator(config, fieldPath) {
|
|
|
8758
9460
|
case 'async':
|
|
8759
9461
|
applyAsyncValidator(config, path);
|
|
8760
9462
|
break;
|
|
8761
|
-
case 'customAsync': {
|
|
8762
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
8763
|
-
const logger = inject(DynamicFormLogger);
|
|
8764
|
-
const tracker = inject(DEPRECATION_WARNING_TRACKER);
|
|
8765
|
-
warnDeprecated(logger, tracker, 'type:customAsync', "Validator type 'customAsync' is deprecated. Use type: 'async' instead.");
|
|
8766
|
-
applyAsyncValidator(config, path);
|
|
8767
|
-
break;
|
|
8768
|
-
}
|
|
8769
|
-
case 'customHttp': {
|
|
8770
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
8771
|
-
const logger = inject(DynamicFormLogger);
|
|
8772
|
-
const tracker = inject(DEPRECATION_WARNING_TRACKER);
|
|
8773
|
-
warnDeprecated(logger, tracker, 'type:customHttp', "Validator type 'customHttp' is deprecated. Use type: 'http' with functionName instead.");
|
|
8774
|
-
applyFunctionHttpValidator(config, path);
|
|
8775
|
-
break;
|
|
8776
|
-
}
|
|
8777
9463
|
case 'http':
|
|
8778
9464
|
applyUnifiedHttpValidator(config, path);
|
|
8779
9465
|
break;
|
|
@@ -8979,8 +9665,8 @@ function getConfigDebounceMs(config) {
|
|
|
8979
9665
|
return undefined;
|
|
8980
9666
|
}
|
|
8981
9667
|
function applyLogic(config, fieldPath) {
|
|
8982
|
-
// Value derivations
|
|
8983
|
-
if (isDerivationLogicConfig(config)
|
|
9668
|
+
// Value derivations (including property derivations via targetProperty) are handled by their orchestrators — skip them.
|
|
9669
|
+
if (isDerivationLogicConfig(config))
|
|
8984
9670
|
return;
|
|
8985
9671
|
// Guard against unrecognized logic types that may be added in the future.
|
|
8986
9672
|
if (!isStateLogicConfig(config)) {
|
|
@@ -9017,6 +9703,10 @@ function applyLogicFn(type, path, logicFn) {
|
|
|
9017
9703
|
case 'required':
|
|
9018
9704
|
required(path, { when: logicFn });
|
|
9019
9705
|
break;
|
|
9706
|
+
default: {
|
|
9707
|
+
const _exhaustive = type;
|
|
9708
|
+
throw new DynamicFormError(`Unhandled state logic type: ${_exhaustive}`);
|
|
9709
|
+
}
|
|
9020
9710
|
}
|
|
9021
9711
|
}
|
|
9022
9712
|
function applyMultipleLogic(configs, fieldPath) {
|
|
@@ -9027,7 +9717,7 @@ function applyMultipleLogic(configs, fieldPath) {
|
|
|
9027
9717
|
* Create a type predicate function from a predicate string.
|
|
9028
9718
|
*
|
|
9029
9719
|
* Uses the secure ExpressionParser with whitelist-based evaluation instead of
|
|
9030
|
-
*
|
|
9720
|
+
* dynamic code execution. This provides security through:
|
|
9031
9721
|
* - AST-based parsing (no code execution)
|
|
9032
9722
|
* - Whitelisted operators only
|
|
9033
9723
|
* - Whitelisted methods only (Array.isArray, typeof, instanceof, etc.)
|
|
@@ -9206,8 +9896,8 @@ function getNumberValidationConfig(fieldDef) {
|
|
|
9206
9896
|
* Cross-field validators are skipped at field level and applied at form level via validateTree.
|
|
9207
9897
|
*/
|
|
9208
9898
|
function mapFieldToForm(fieldDef, fieldPath) {
|
|
9209
|
-
// Layout containers (page, row) - flatten children to current level
|
|
9210
|
-
if (isPageField(fieldDef) || isRowField(fieldDef)) {
|
|
9899
|
+
// Layout containers (page, row, container) - flatten children to current level
|
|
9900
|
+
if (isPageField(fieldDef) || isRowField(fieldDef) || isContainerTypedField(fieldDef)) {
|
|
9211
9901
|
mapContainerChildren(fieldDef.fields, fieldPath);
|
|
9212
9902
|
return;
|
|
9213
9903
|
}
|
|
@@ -9364,8 +10054,8 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
|
|
|
9364
10054
|
const pathRecord = itemPath;
|
|
9365
10055
|
// Map ALL unique template fields from all object items
|
|
9366
10056
|
for (const templateField of allObjectFields) {
|
|
9367
|
-
if (isRowField(templateField) || isPageField(templateField)) {
|
|
9368
|
-
// Row/page templates flatten their children
|
|
10057
|
+
if (isRowField(templateField) || isPageField(templateField) || isContainerTypedField(templateField)) {
|
|
10058
|
+
// Row/page/container templates flatten their children
|
|
9369
10059
|
mapContainerChildren(templateField.fields, itemPath);
|
|
9370
10060
|
}
|
|
9371
10061
|
else if (isGroupField(templateField)) {
|
|
@@ -9404,8 +10094,8 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
|
|
|
9404
10094
|
function collectFieldsFromObjectItem(itemFields, allFields) {
|
|
9405
10095
|
const seenKeys = new Set(allFields.map((f) => f.key).filter(Boolean));
|
|
9406
10096
|
for (const field of itemFields) {
|
|
9407
|
-
// For row/page fields, we need to collect their children's keys
|
|
9408
|
-
if (isRowField(field) || isPageField(field)) {
|
|
10097
|
+
// For row/page/container fields, we need to collect their children's keys
|
|
10098
|
+
if (isRowField(field) || isPageField(field) || isContainerTypedField(field)) {
|
|
9409
10099
|
// Use a synthetic key for container fields to dedupe them
|
|
9410
10100
|
const containerKey = `__container_${field.type}_${JSON.stringify(field.fields?.map((f) => f.key))}`;
|
|
9411
10101
|
if (!seenKeys.has(containerKey)) {
|
|
@@ -9536,17 +10226,24 @@ function getFieldDefaultValue(field, registry) {
|
|
|
9536
10226
|
}
|
|
9537
10227
|
return groupDefaults;
|
|
9538
10228
|
}
|
|
9539
|
-
//
|
|
10229
|
+
// Narrow once: `field` is FieldDef<unknown>, but value/nullable are defined on BaseValueField.
|
|
10230
|
+
// This cast avoids per-access `as` and documents what we're reading.
|
|
10231
|
+
const valueField = field;
|
|
10232
|
+
const isNullable = valueField.nullable === true;
|
|
10233
|
+
// Use explicit value if provided (including null when nullable)
|
|
9540
10234
|
if ('value' in field) {
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
return field.value;
|
|
10235
|
+
if (valueField.value !== null && valueField.value !== undefined) {
|
|
10236
|
+
return valueField.value;
|
|
9544
10237
|
}
|
|
9545
|
-
//
|
|
9546
|
-
if (
|
|
9547
|
-
return
|
|
10238
|
+
// Preserve explicit null for nullable fields
|
|
10239
|
+
if (valueField.value === null && isNullable) {
|
|
10240
|
+
return null;
|
|
9548
10241
|
}
|
|
9549
|
-
//
|
|
10242
|
+
// Non-nullable null and undefined both fall through to type-specific defaults below
|
|
10243
|
+
}
|
|
10244
|
+
// Nullable fields with no explicit value default to null
|
|
10245
|
+
if (isNullable) {
|
|
10246
|
+
return null;
|
|
9550
10247
|
}
|
|
9551
10248
|
// Type-specific defaults when no value is specified
|
|
9552
10249
|
if (field.type === 'checkbox') {
|
|
@@ -9578,8 +10275,8 @@ function getFieldDefaultValue(field, registry) {
|
|
|
9578
10275
|
// Groups wrap their value under the group key
|
|
9579
10276
|
itemValue[templateField.key] = fieldValue;
|
|
9580
10277
|
}
|
|
9581
|
-
else if (templateField.type === 'row') {
|
|
9582
|
-
// Rows flatten their fields directly
|
|
10278
|
+
else if (templateField.type === 'row' || templateField.type === 'container') {
|
|
10279
|
+
// Rows and containers flatten their fields directly
|
|
9583
10280
|
if (fieldValue && typeof fieldValue === 'object') {
|
|
9584
10281
|
itemValue = { ...itemValue, ...fieldValue };
|
|
9585
10282
|
}
|
|
@@ -10268,18 +10965,20 @@ function flattenFields(fields, registry, options = {}) {
|
|
|
10268
10965
|
// Step 1: Determine how this field type should handle its value in the form
|
|
10269
10966
|
// valueHandling can be: 'include', 'exclude', or 'flatten'
|
|
10270
10967
|
const valueHandling = getFieldValueHandling(field.type, registry);
|
|
10271
|
-
// Step 2: Check if this is a row field that should be preserved for DOM rendering
|
|
10968
|
+
// Step 2: Check if this is a row or container field that should be preserved for DOM rendering
|
|
10272
10969
|
// Row fields need to render their container element for grid layouts to work
|
|
10273
|
-
|
|
10970
|
+
// Container fields need to render their container for the wrapper chain
|
|
10971
|
+
if (options.preserveRows && (isRowField(field) || isContainerTypedField(field))) {
|
|
10274
10972
|
if (field.fields) {
|
|
10275
10973
|
// Recursively flatten children while preserving row structure
|
|
10276
10974
|
const flattenedChildren = flattenFields(normalizeFieldsArray(field.fields), registry, options);
|
|
10277
|
-
// Keep the row field in the result with its flattened children
|
|
10278
|
-
// This allows the
|
|
10975
|
+
// Keep the row/container field in the result with its flattened children
|
|
10976
|
+
// This allows the container component to render while children are flattened
|
|
10977
|
+
const autoPrefix = isRowField(field) ? 'auto_row' : 'auto_container';
|
|
10279
10978
|
result.push({
|
|
10280
10979
|
...field,
|
|
10281
10980
|
fields: flattenedChildren,
|
|
10282
|
-
key: field.key ||
|
|
10981
|
+
key: field.key || `${autoPrefix}_${autoKeyCounter++}`,
|
|
10283
10982
|
});
|
|
10284
10983
|
}
|
|
10285
10984
|
}
|
|
@@ -10353,17 +11052,25 @@ function flattenFields(fields, registry, options = {}) {
|
|
|
10353
11052
|
* @returns Object containing memoized processing functions
|
|
10354
11053
|
*/
|
|
10355
11054
|
function createContainerFieldProcessors() {
|
|
11055
|
+
const createFieldsResolver = (preserveRows = false) => (fields, registry) => {
|
|
11056
|
+
let key = '';
|
|
11057
|
+
for (const f of fields) {
|
|
11058
|
+
key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
|
|
11059
|
+
}
|
|
11060
|
+
return key + registry.size + '|' + preserveRows;
|
|
11061
|
+
};
|
|
10356
11062
|
const memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
|
|
10357
11063
|
// registry.size is a valid cache key proxy because the field registry is populated
|
|
10358
11064
|
// once at bootstrap and never mutated at runtime. If the registry were mutable,
|
|
10359
11065
|
// we would need a content-based hash instead.
|
|
10360
|
-
resolver: (
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
11066
|
+
resolver: createFieldsResolver(),
|
|
11067
|
+
maxSize: 10,
|
|
11068
|
+
});
|
|
11069
|
+
const memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields([...fields], registry, { preserveRows: true }), {
|
|
11070
|
+
// registry.size is a valid cache key proxy because the field registry is populated
|
|
11071
|
+
// once at bootstrap and never mutated at runtime. If the registry were mutable,
|
|
11072
|
+
// we would need a content-based hash instead.
|
|
11073
|
+
resolver: createFieldsResolver(true),
|
|
10367
11074
|
maxSize: 10,
|
|
10368
11075
|
});
|
|
10369
11076
|
const memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
|
|
@@ -10385,7 +11092,7 @@ function createContainerFieldProcessors() {
|
|
|
10385
11092
|
},
|
|
10386
11093
|
maxSize: 10,
|
|
10387
11094
|
});
|
|
10388
|
-
return { memoizedFlattenFields, memoizedKeyBy, memoizedDefaultValues };
|
|
11095
|
+
return { memoizedFlattenFields, memoizedFlattenFieldsForRendering, memoizedKeyBy, memoizedDefaultValues };
|
|
10389
11096
|
}
|
|
10390
11097
|
/**
|
|
10391
11098
|
* Shared container field processors injection token.
|
|
@@ -10407,50 +11114,6 @@ const CONTAINER_FIELD_PROCESSORS = new InjectionToken('CONTAINER_FIELD_PROCESSOR
|
|
|
10407
11114
|
factory: () => createContainerFieldProcessors(),
|
|
10408
11115
|
});
|
|
10409
11116
|
|
|
10410
|
-
/**
|
|
10411
|
-
* Type guard to check if a value is an array of Signals.
|
|
10412
|
-
* @internal
|
|
10413
|
-
*/
|
|
10414
|
-
function isSignalArray(value) {
|
|
10415
|
-
return Array.isArray(value) && value.every((item) => isSignal(item));
|
|
10416
|
-
}
|
|
10417
|
-
/**
|
|
10418
|
-
* Type guard to check if a value is a record (object) of Signals.
|
|
10419
|
-
* @internal
|
|
10420
|
-
*/
|
|
10421
|
-
function isSignalRecord(value) {
|
|
10422
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
|
|
10423
|
-
}
|
|
10424
|
-
// Implementation
|
|
10425
|
-
function derivedFromDeferred(source, operator, options) {
|
|
10426
|
-
const deferred$ = defer(() => {
|
|
10427
|
-
// Handle single signal
|
|
10428
|
-
if (isSignal(source)) {
|
|
10429
|
-
return toObservable(source, { injector: options.injector });
|
|
10430
|
-
}
|
|
10431
|
-
// Handle array of signals
|
|
10432
|
-
if (isSignalArray(source)) {
|
|
10433
|
-
const observables = source.map((s) => toObservable(s, { injector: options.injector }));
|
|
10434
|
-
return combineLatest(observables);
|
|
10435
|
-
}
|
|
10436
|
-
// Handle object of signals
|
|
10437
|
-
if (isSignalRecord(source)) {
|
|
10438
|
-
const keys = Object.keys(source);
|
|
10439
|
-
const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
|
|
10440
|
-
return combineLatest(observables, (...values) => {
|
|
10441
|
-
const result = {};
|
|
10442
|
-
keys.forEach((key, index) => {
|
|
10443
|
-
result[key] = values[index];
|
|
10444
|
-
});
|
|
10445
|
-
return result;
|
|
10446
|
-
});
|
|
10447
|
-
}
|
|
10448
|
-
// Fallback - should not happen with proper typing
|
|
10449
|
-
throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
|
|
10450
|
-
}).pipe(operator);
|
|
10451
|
-
return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
|
|
10452
|
-
}
|
|
10453
|
-
|
|
10454
11117
|
/**
|
|
10455
11118
|
* Generates CSS class string for responsive grid layout based on field column configuration.
|
|
10456
11119
|
*
|
|
@@ -10887,6 +11550,17 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
10887
11550
|
});
|
|
10888
11551
|
}
|
|
10889
11552
|
|
|
11553
|
+
function createRenderReadySignal(inputs, definition) {
|
|
11554
|
+
const explicitRenderReadyWhen = definition?.renderReadyWhen;
|
|
11555
|
+
const requiredInputs = definition?.mapper && explicitRenderReadyWhen === undefined ? ['field'] : (explicitRenderReadyWhen ?? []);
|
|
11556
|
+
if (requiredInputs.length === 0) {
|
|
11557
|
+
return computed(() => true);
|
|
11558
|
+
}
|
|
11559
|
+
return computed(() => {
|
|
11560
|
+
const currentInputs = inputs();
|
|
11561
|
+
return requiredInputs.every((inputName) => currentInputs[inputName] !== undefined);
|
|
11562
|
+
});
|
|
11563
|
+
}
|
|
10890
11564
|
/**
|
|
10891
11565
|
* Resolves a single field definition to a ResolvedField using RxJS.
|
|
10892
11566
|
* Loads the component asynchronously and maps inputs in the injection context.
|
|
@@ -10900,7 +11574,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
10900
11574
|
* @returns Observable that emits ResolvedField or undefined (for componentless fields or on error)
|
|
10901
11575
|
*/
|
|
10902
11576
|
function resolveField(fieldDef, context) {
|
|
10903
|
-
return from(context.loadTypeComponent(fieldDef.type)).pipe(map
|
|
11577
|
+
return from(context.loadTypeComponent(fieldDef.type)).pipe(map((component) => {
|
|
10904
11578
|
// Check if component is destroyed before proceeding
|
|
10905
11579
|
if (context.destroyRef.destroyed) {
|
|
10906
11580
|
return undefined;
|
|
@@ -10911,15 +11585,18 @@ function resolveField(fieldDef, context) {
|
|
|
10911
11585
|
}
|
|
10912
11586
|
// Run mapper in injection context
|
|
10913
11587
|
const inputs = runInInjectionContext(context.injector, () => mapFieldToInputs(fieldDef, context.registry));
|
|
11588
|
+
const definition = context.registry.get(fieldDef.type);
|
|
10914
11589
|
// Fields with components should always have inputs (componentless fields are handled above)
|
|
10915
11590
|
if (!inputs) {
|
|
10916
11591
|
return undefined;
|
|
10917
11592
|
}
|
|
10918
11593
|
return {
|
|
10919
11594
|
key: fieldDef.key,
|
|
11595
|
+
fieldDef,
|
|
10920
11596
|
component,
|
|
10921
11597
|
injector: context.injector,
|
|
10922
11598
|
inputs,
|
|
11599
|
+
renderReady: createRenderReadySignal(inputs, definition),
|
|
10923
11600
|
};
|
|
10924
11601
|
}), catchError((error) => {
|
|
10925
11602
|
// Only call onError if component is not destroyed to avoid accessing cleaned-up state
|
|
@@ -10945,14 +11622,17 @@ function resolveFieldSync(fieldDef, context) {
|
|
|
10945
11622
|
return undefined;
|
|
10946
11623
|
}
|
|
10947
11624
|
const inputs = runInInjectionContext(context.injector, () => mapFieldToInputs(fieldDef, context.registry));
|
|
11625
|
+
const definition = context.registry.get(fieldDef.type);
|
|
10948
11626
|
if (!inputs) {
|
|
10949
11627
|
return undefined;
|
|
10950
11628
|
}
|
|
10951
11629
|
return {
|
|
10952
11630
|
key: fieldDef.key,
|
|
11631
|
+
fieldDef,
|
|
10953
11632
|
component,
|
|
10954
11633
|
injector: context.injector,
|
|
10955
11634
|
inputs,
|
|
11635
|
+
renderReady: createRenderReadySignal(inputs, definition),
|
|
10956
11636
|
};
|
|
10957
11637
|
}
|
|
10958
11638
|
/**
|
|
@@ -10980,13 +11660,13 @@ function reconcileFields(prev, curr) {
|
|
|
10980
11660
|
* Used by container components (page, group) to resolve their child fields.
|
|
10981
11661
|
*/
|
|
10982
11662
|
function createFieldResolutionPipe(getContext) {
|
|
10983
|
-
return pipe(switchMap
|
|
11663
|
+
return pipe(switchMap((fields) => {
|
|
10984
11664
|
if (!fields || fields.length === 0) {
|
|
10985
11665
|
return of([]);
|
|
10986
11666
|
}
|
|
10987
11667
|
const context = getContext();
|
|
10988
11668
|
return forkJoin(fields.map((f) => resolveField(f, context)));
|
|
10989
|
-
}), map
|
|
11669
|
+
}), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, []));
|
|
10990
11670
|
}
|
|
10991
11671
|
|
|
10992
11672
|
/**
|
|
@@ -11251,13 +11931,8 @@ function injectFieldRegistry() {
|
|
|
11251
11931
|
if (cached) {
|
|
11252
11932
|
return cached;
|
|
11253
11933
|
}
|
|
11254
|
-
// Handle async loading
|
|
11255
11934
|
try {
|
|
11256
|
-
const
|
|
11257
|
-
// Handle ES module imports - extract default export if needed
|
|
11258
|
-
const moduleResult = result;
|
|
11259
|
-
const component = (typeof moduleResult === 'object' && 'default' in moduleResult && moduleResult.default) || result;
|
|
11260
|
-
// Populate cache for future sync lookups
|
|
11935
|
+
const component = resolveDefaultExport(await fieldType.loadComponent());
|
|
11261
11936
|
if (component) {
|
|
11262
11937
|
componentCache.set(name, component);
|
|
11263
11938
|
}
|
|
@@ -11802,7 +12477,7 @@ class FormStateMachine {
|
|
|
11802
12477
|
}
|
|
11803
12478
|
constructor(config) {
|
|
11804
12479
|
this.config = config;
|
|
11805
|
-
this._state = signal(createUninitializedState(), ...(ngDevMode ? [{ debugName: "_state" }] : []));
|
|
12480
|
+
this._state = signal(createUninitializedState(), ...(ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []));
|
|
11806
12481
|
this.state = this._state.asReadonly();
|
|
11807
12482
|
this.state$ = toObservable(this._state, { injector: config.injector });
|
|
11808
12483
|
this.setupActionProcessing();
|
|
@@ -12076,7 +12751,6 @@ class FormStateManager {
|
|
|
12076
12751
|
injector = inject(Injector);
|
|
12077
12752
|
destroyRef = inject(DestroyRef);
|
|
12078
12753
|
logger = inject(DynamicFormLogger);
|
|
12079
|
-
deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });
|
|
12080
12754
|
eventBus = inject(EventBus);
|
|
12081
12755
|
functionRegistry = inject(FunctionRegistryService);
|
|
12082
12756
|
schemaRegistry = inject(SchemaRegistryService);
|
|
@@ -12102,27 +12776,14 @@ class FormStateManager {
|
|
|
12102
12776
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12103
12777
|
// Memoized Functions
|
|
12104
12778
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12105
|
-
fieldProcessors = inject(CONTAINER_FIELD_PROCESSORS);
|
|
12106
|
-
|
|
12107
|
-
// registry.size is a valid cache key proxy because the field registry is populated
|
|
12108
|
-
// once at bootstrap and never mutated at runtime. If the registry were mutable,
|
|
12109
|
-
// we would need a content-based hash instead.
|
|
12110
|
-
resolver: (fields, registry) => {
|
|
12111
|
-
let key = '';
|
|
12112
|
-
for (const f of fields) {
|
|
12113
|
-
key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
|
|
12114
|
-
}
|
|
12115
|
-
return key + registry.size;
|
|
12116
|
-
},
|
|
12117
|
-
maxSize: 10,
|
|
12118
|
-
});
|
|
12119
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
12779
|
+
fieldProcessors = inject(CONTAINER_FIELD_PROCESSORS);
|
|
12780
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12120
12781
|
// Internal State Signals
|
|
12121
12782
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12122
12783
|
/**
|
|
12123
12784
|
* Field loading errors accumulated during resolution.
|
|
12124
12785
|
*/
|
|
12125
|
-
fieldLoadingErrors = signal([], ...(ngDevMode ? [{ debugName: "fieldLoadingErrors" }] : []));
|
|
12786
|
+
fieldLoadingErrors = signal([], ...(ngDevMode ? [{ debugName: "fieldLoadingErrors" }] : /* istanbul ignore next */ []));
|
|
12126
12787
|
/**
|
|
12127
12788
|
* Class-level form cache for the "hold until settled" pattern.
|
|
12128
12789
|
* MUST be class-level — a local variable in the computed would reset on re-evaluation.
|
|
@@ -12153,7 +12814,7 @@ class FormStateManager {
|
|
|
12153
12814
|
if (state.type === LifecycleState.Initializing)
|
|
12154
12815
|
return state.config;
|
|
12155
12816
|
return undefined;
|
|
12156
|
-
}, ...(ngDevMode ? [{ debugName: "activeConfig" }] : []));
|
|
12817
|
+
}, ...(ngDevMode ? [{ debugName: "activeConfig" }] : /* istanbul ignore next */ []));
|
|
12157
12818
|
/**
|
|
12158
12819
|
* Current render phase: 'render' = showing form, 'teardown' = hiding old components.
|
|
12159
12820
|
*/
|
|
@@ -12162,9 +12823,9 @@ class FormStateManager {
|
|
|
12162
12823
|
if (isTransitioningState(state) && state.phase === Phase.Teardown)
|
|
12163
12824
|
return Phase.Teardown;
|
|
12164
12825
|
return 'render';
|
|
12165
|
-
}, ...(ngDevMode ? [{ debugName: "renderPhase" }] : []));
|
|
12826
|
+
}, ...(ngDevMode ? [{ debugName: "renderPhase" }] : /* istanbul ignore next */ []));
|
|
12166
12827
|
/** Whether to render the form template. */
|
|
12167
|
-
shouldRender = computed(() => this.activeConfig() !== undefined, ...(ngDevMode ? [{ debugName: "shouldRender" }] : []));
|
|
12828
|
+
shouldRender = computed(() => this.activeConfig() !== undefined, ...(ngDevMode ? [{ debugName: "shouldRender" }] : /* istanbul ignore next */ []));
|
|
12168
12829
|
/**
|
|
12169
12830
|
* Computed form mode detection with validation.
|
|
12170
12831
|
*/
|
|
@@ -12174,14 +12835,14 @@ class FormStateManager {
|
|
|
12174
12835
|
return { mode: 'non-paged', isValid: true, errors: [] };
|
|
12175
12836
|
}
|
|
12176
12837
|
return detectFormMode(config.fields || []);
|
|
12177
|
-
}, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : []));
|
|
12838
|
+
}, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : /* istanbul ignore next */ []));
|
|
12178
12839
|
/** Validation result for the current form configuration. */
|
|
12179
12840
|
formConfigValidation = computed(() => {
|
|
12180
12841
|
const config = this.activeConfig();
|
|
12181
12842
|
if (!config)
|
|
12182
12843
|
return { isValid: true, errors: [], warnings: [] };
|
|
12183
12844
|
return FormModeValidator.validateFormConfiguration(config.fields || []);
|
|
12184
|
-
}, ...(ngDevMode ? [{ debugName: "formConfigValidation" }] : []));
|
|
12845
|
+
}, ...(ngDevMode ? [{ debugName: "formConfigValidation" }] : /* istanbul ignore next */ []));
|
|
12185
12846
|
/**
|
|
12186
12847
|
* Effective form options (merged from config and input).
|
|
12187
12848
|
*/
|
|
@@ -12190,7 +12851,7 @@ class FormStateManager {
|
|
|
12190
12851
|
const configOptions = config?.options || {};
|
|
12191
12852
|
const inputOptions = this.deps.formOptions() ?? undefined;
|
|
12192
12853
|
return { ...configOptions, ...inputOptions };
|
|
12193
|
-
}, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : []));
|
|
12854
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : /* istanbul ignore next */ []));
|
|
12194
12855
|
/**
|
|
12195
12856
|
* Page field definitions (for paged forms).
|
|
12196
12857
|
*/
|
|
@@ -12201,11 +12862,11 @@ class FormStateManager {
|
|
|
12201
12862
|
return config.fields.filter(isPageField);
|
|
12202
12863
|
}
|
|
12203
12864
|
return [];
|
|
12204
|
-
}, ...(ngDevMode ? [{ debugName: "pageFieldDefinitions" }] : []));
|
|
12865
|
+
}, ...(ngDevMode ? [{ debugName: "pageFieldDefinitions" }] : /* istanbul ignore next */ []));
|
|
12205
12866
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12206
12867
|
// Computed Signals - Form Setup
|
|
12207
12868
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12208
|
-
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
|
|
12869
|
+
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
|
|
12209
12870
|
/** Computed form setup — reads from the state machine, computed from config on bootstrap. */
|
|
12210
12871
|
formSetup = computed(() => {
|
|
12211
12872
|
const registry = this.rawFieldRegistry();
|
|
@@ -12236,18 +12897,18 @@ class FormStateManager {
|
|
|
12236
12897
|
return this.createFormSetupFromConfig(config.fields, modeDetection.mode, registry);
|
|
12237
12898
|
}
|
|
12238
12899
|
return this.createEmptyFormSetup(registry);
|
|
12239
|
-
}, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
|
|
12900
|
+
}, ...(ngDevMode ? [{ debugName: "formSetup" }] : /* istanbul ignore next */ []));
|
|
12240
12901
|
/**
|
|
12241
12902
|
* Default values computed from field definitions.
|
|
12242
12903
|
*/
|
|
12243
|
-
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
12904
|
+
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
|
|
12244
12905
|
/** Valid field keys — memoized separately so the Set isn't rebuilt on every keystroke. */
|
|
12245
12906
|
validKeys = computed(() => {
|
|
12246
12907
|
const schemaFields = this.formSetup().schemaFields;
|
|
12247
12908
|
if (!schemaFields || schemaFields.length === 0)
|
|
12248
12909
|
return undefined;
|
|
12249
12910
|
return new Set(schemaFields.map((f) => f.key).filter((key) => key !== undefined));
|
|
12250
|
-
}, ...(ngDevMode ? [{ debugName: "validKeys" }] : []));
|
|
12911
|
+
}, ...(ngDevMode ? [{ debugName: "validKeys" }] : /* istanbul ignore next */ []));
|
|
12251
12912
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12252
12913
|
// Computed Signals - Entity & Form
|
|
12253
12914
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -12307,7 +12968,7 @@ class FormStateManager {
|
|
|
12307
12968
|
return createFormLevelSchema(config.schema);
|
|
12308
12969
|
}
|
|
12309
12970
|
return undefined;
|
|
12310
|
-
}), ...(ngDevMode ? [{ debugName: "formSchema" }] : []));
|
|
12971
|
+
}), ...(ngDevMode ? [{ debugName: "formSchema" }] : /* istanbul ignore next */ []));
|
|
12311
12972
|
/**
|
|
12312
12973
|
* The Angular Signal Form instance.
|
|
12313
12974
|
*/
|
|
@@ -12315,7 +12976,7 @@ class FormStateManager {
|
|
|
12315
12976
|
const schema = this.formSchema();
|
|
12316
12977
|
const injector = this.injector;
|
|
12317
12978
|
return untracked(() => (schema ? form(this.entity, schema, { injector }) : form(this.entity, { injector })));
|
|
12318
|
-
}, ...(ngDevMode ? [{ debugName: "form" }] : []));
|
|
12979
|
+
}, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
|
|
12319
12980
|
/** Whether resolvedFields has caught up with fieldsSource (set in constructor). */
|
|
12320
12981
|
isFieldPipelineSettled;
|
|
12321
12982
|
/**
|
|
@@ -12342,14 +13003,14 @@ class FormStateManager {
|
|
|
12342
13003
|
return currentForm;
|
|
12343
13004
|
},
|
|
12344
13005
|
};
|
|
12345
|
-
}, ...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
|
|
13006
|
+
}, ...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : /* istanbul ignore next */ []));
|
|
12346
13007
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12347
13008
|
// Computed Signals - Form State
|
|
12348
13009
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12349
13010
|
/** Intermediate computed that unwraps the double-signal (form()()) once. */
|
|
12350
|
-
formInstance = computed(() => this.form()(), ...(ngDevMode ? [{ debugName: "formInstance" }] : []));
|
|
13011
|
+
formInstance = computed(() => this.form()(), ...(ngDevMode ? [{ debugName: "formInstance" }] : /* istanbul ignore next */ []));
|
|
12351
13012
|
/** Current form values (reactive). */
|
|
12352
|
-
formValue = computed(() => this.formInstance().value(), ...(ngDevMode ? [{ debugName: "formValue" }] : []));
|
|
13013
|
+
formValue = computed(() => this.formInstance().value(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
12353
13014
|
/**
|
|
12354
13015
|
* Form values filtered by value exclusion rules.
|
|
12355
13016
|
*
|
|
@@ -12379,25 +13040,25 @@ class FormStateManager {
|
|
|
12379
13040
|
// FieldTree doesn't expose this index signature, so we use a helper cast.
|
|
12380
13041
|
const fieldTreeRecord = asFieldTreeRecord(formTree);
|
|
12381
13042
|
return filterFormValue(rawValue, setup.schemaFields, fieldTreeRecord, setup.registry, this.valueExclusionDefaults, formOptions);
|
|
12382
|
-
}, ...(ngDevMode ? [{ debugName: "filteredFormValue" }] : []));
|
|
13043
|
+
}, ...(ngDevMode ? [{ debugName: "filteredFormValue" }] : /* istanbul ignore next */ []));
|
|
12383
13044
|
/** Whether the form is currently valid. */
|
|
12384
|
-
valid = computed(() => this.formInstance().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
|
|
13045
|
+
valid = computed(() => this.formInstance().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
|
|
12385
13046
|
/** Whether the form is currently invalid. */
|
|
12386
|
-
invalid = computed(() => this.formInstance().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
13047
|
+
invalid = computed(() => this.formInstance().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
12387
13048
|
/** Whether any form field has been modified. */
|
|
12388
|
-
dirty = computed(() => this.formInstance().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
|
|
13049
|
+
dirty = computed(() => this.formInstance().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
12389
13050
|
/** Whether any form field has been touched (blurred). */
|
|
12390
|
-
touched = computed(() => this.formInstance().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
13051
|
+
touched = computed(() => this.formInstance().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
12391
13052
|
/** Current validation errors from all fields. */
|
|
12392
|
-
errors = computed(() => this.formInstance().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
13053
|
+
errors = computed(() => this.formInstance().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
12393
13054
|
/** Whether the form is disabled (from options or form state). */
|
|
12394
13055
|
disabled = computed(() => {
|
|
12395
13056
|
const optionsDisabled = this.effectiveFormOptions().disabled;
|
|
12396
13057
|
const formDisabled = this.formInstance().disabled();
|
|
12397
13058
|
return optionsDisabled ?? formDisabled;
|
|
12398
|
-
}, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
13059
|
+
}, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
12399
13060
|
/** Whether the form is currently submitting. */
|
|
12400
|
-
submitting = computed(() => this.formInstance().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
|
|
13061
|
+
submitting = computed(() => this.formInstance().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
|
|
12401
13062
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12402
13063
|
// Field Resolution
|
|
12403
13064
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -12420,7 +13081,7 @@ class FormStateManager {
|
|
|
12420
13081
|
}
|
|
12421
13082
|
}
|
|
12422
13083
|
return this.formSetup().fields;
|
|
12423
|
-
}, { ...(ngDevMode ? { debugName: "fieldsSource" } : {}), equal: (a, b) => {
|
|
13084
|
+
}, { ...(ngDevMode ? { debugName: "fieldsSource" } : /* istanbul ignore next */ {}), equal: (a, b) => {
|
|
12424
13085
|
if (a === b)
|
|
12425
13086
|
return true;
|
|
12426
13087
|
if (a.length !== b.length)
|
|
@@ -12439,7 +13100,7 @@ class FormStateManager {
|
|
|
12439
13100
|
fieldInjector = computed(() => Injector.create({
|
|
12440
13101
|
parent: this.injector,
|
|
12441
13102
|
providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: this.fieldSignalContext() }],
|
|
12442
|
-
}), ...(ngDevMode ? [{ debugName: "fieldInjector" }] : []));
|
|
13103
|
+
}), ...(ngDevMode ? [{ debugName: "fieldInjector" }] : /* istanbul ignore next */ []));
|
|
12443
13104
|
/** Resolved fields ready for rendering. */
|
|
12444
13105
|
resolvedFields;
|
|
12445
13106
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -12453,13 +13114,13 @@ class FormStateManager {
|
|
|
12453
13114
|
cleared$;
|
|
12454
13115
|
/** Emits once when the state machine reaches 'ready'. Useful for tests. */
|
|
12455
13116
|
get ready$() {
|
|
12456
|
-
return this.machine.state$.pipe(filter(isReadyState), take$1(1), map
|
|
13117
|
+
return this.machine.state$.pipe(filter(isReadyState), take$1(1), map(() => undefined));
|
|
12457
13118
|
}
|
|
12458
13119
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12459
13120
|
// Constructor
|
|
12460
13121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12461
13122
|
constructor() {
|
|
12462
|
-
this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap
|
|
13123
|
+
this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
|
|
12463
13124
|
if (!fields || fields.length === 0) {
|
|
12464
13125
|
return of([]);
|
|
12465
13126
|
}
|
|
@@ -12508,18 +13169,18 @@ class FormStateManager {
|
|
|
12508
13169
|
if (asyncObs.length === 0) {
|
|
12509
13170
|
return of(results);
|
|
12510
13171
|
}
|
|
12511
|
-
return forkJoin(asyncObs).pipe(map
|
|
13172
|
+
return forkJoin(asyncObs).pipe(map((asyncResults) => {
|
|
12512
13173
|
for (let j = 0; j < asyncResults.length; j++) {
|
|
12513
13174
|
results[asyncIndexes[j]] = asyncResults[j];
|
|
12514
13175
|
}
|
|
12515
13176
|
return results;
|
|
12516
13177
|
}));
|
|
12517
|
-
}), map
|
|
13178
|
+
}), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
|
|
12518
13179
|
// Length comparison works because reconcileFields preserves ordering and
|
|
12519
13180
|
// fieldsSource changes atomically during transitions (lengths won't match until settled).
|
|
12520
13181
|
this.isFieldPipelineSettled = computed(() => {
|
|
12521
13182
|
return this.fieldsSource().length === this.resolvedFields().length;
|
|
12522
|
-
}, ...(ngDevMode ? [{ debugName: "isFieldPipelineSettled" }] : []));
|
|
13183
|
+
}, ...(ngDevMode ? [{ debugName: "isFieldPipelineSettled" }] : /* istanbul ignore next */ []));
|
|
12523
13184
|
this.submitted$ = this.eventBus.on('submit').pipe(filter(() => {
|
|
12524
13185
|
if (!this.valid()) {
|
|
12525
13186
|
this.logger.debug('Form submitted while invalid, not emitting to (submitted) output');
|
|
@@ -12529,7 +13190,7 @@ class FormStateManager {
|
|
|
12529
13190
|
}),
|
|
12530
13191
|
// switchMap is intentional here: mapping is synchronous (reads signals, no async Promise).
|
|
12531
13192
|
// The exhaustMap concern in createSubmissionHandler does not apply to this path.
|
|
12532
|
-
switchMap
|
|
13193
|
+
switchMap(() => {
|
|
12533
13194
|
const config = this.activeConfig();
|
|
12534
13195
|
const submissionConfig = config?.submission;
|
|
12535
13196
|
if (submissionConfig?.action) {
|
|
@@ -12540,8 +13201,8 @@ class FormStateManager {
|
|
|
12540
13201
|
}
|
|
12541
13202
|
return of(this.filteredFormValue());
|
|
12542
13203
|
}), takeUntilDestroyed(this.destroyRef));
|
|
12543
|
-
this.reset$ = this.eventBus.on('form-reset').pipe(map
|
|
12544
|
-
this.cleared$ = this.eventBus.on('form-clear').pipe(map
|
|
13204
|
+
this.reset$ = this.eventBus.on('form-reset').pipe(map(() => undefined), takeUntilDestroyed(this.destroyRef));
|
|
13205
|
+
this.cleared$ = this.eventBus.on('form-clear').pipe(map(() => undefined), takeUntilDestroyed(this.destroyRef));
|
|
12545
13206
|
// Create the state machine eagerly
|
|
12546
13207
|
this.machine = createFormStateMachine({
|
|
12547
13208
|
injector: this.injector,
|
|
@@ -12667,13 +13328,6 @@ class FormStateManager {
|
|
|
12667
13328
|
if (customFnConfig.derivations) {
|
|
12668
13329
|
this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);
|
|
12669
13330
|
}
|
|
12670
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
12671
|
-
if (customFnConfig.propertyDerivations) {
|
|
12672
|
-
if (this.deprecationTracker) {
|
|
12673
|
-
warnDeprecated(this.logger, this.deprecationTracker, 'customFnConfig:propertyDerivations', "customFnConfig.propertyDerivations is deprecated. Register functions under 'derivations' instead and use type: 'derivation' with targetProperty.");
|
|
12674
|
-
}
|
|
12675
|
-
this.functionRegistry.setPropertyDerivationFunctions(customFnConfig.propertyDerivations);
|
|
12676
|
-
}
|
|
12677
13331
|
if (customFnConfig.asyncDerivations) {
|
|
12678
13332
|
this.functionRegistry.setAsyncDerivationFunctions(customFnConfig.asyncDerivations);
|
|
12679
13333
|
}
|
|
@@ -12682,10 +13336,6 @@ class FormStateManager {
|
|
|
12682
13336
|
}
|
|
12683
13337
|
this.functionRegistry.setValidators(customFnConfig.validators);
|
|
12684
13338
|
this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);
|
|
12685
|
-
// TODO(@ng-forge): remove deprecated code in next minor
|
|
12686
|
-
if (customFnConfig.httpValidators && this.deprecationTracker) {
|
|
12687
|
-
warnDeprecated(this.logger, this.deprecationTracker, 'customFnConfig:httpValidators', "customFnConfig.httpValidators registration path is deprecated. Use type: 'http' with functionName (for function-based) or http + responseMapping (for declarative).");
|
|
12688
|
-
}
|
|
12689
13339
|
this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);
|
|
12690
13340
|
}
|
|
12691
13341
|
createFormSetupFromConfig(fields, mode, registry) {
|
|
@@ -12694,7 +13344,7 @@ class FormStateManager {
|
|
|
12694
13344
|
// into full ArrayField.fields and are reachable during traversal.
|
|
12695
13345
|
validateFormConfig(normalizedFields, registry, this.logger);
|
|
12696
13346
|
const flattenedFields = this.fieldProcessors.memoizedFlattenFields(normalizedFields, registry);
|
|
12697
|
-
const flattenedFieldsForRendering = this.memoizedFlattenFieldsForRendering(normalizedFields, registry);
|
|
13347
|
+
const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(normalizedFields, registry);
|
|
12698
13348
|
const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
|
|
12699
13349
|
const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
|
|
12700
13350
|
const fieldsToRender = mode === 'paged' ? [] : flattenedFieldsForRendering;
|
|
@@ -12722,10 +13372,10 @@ class FormStateManager {
|
|
|
12722
13372
|
ngOnDestroy() {
|
|
12723
13373
|
this.machine.dispatch({ type: Action.Destroy });
|
|
12724
13374
|
}
|
|
12725
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
12726
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
13375
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FormStateManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
13376
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FormStateManager });
|
|
12727
13377
|
}
|
|
12728
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
13378
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FormStateManager, decorators: [{
|
|
12729
13379
|
type: Injectable
|
|
12730
13380
|
}], ctorParameters: () => [] });
|
|
12731
13381
|
|
|
@@ -12996,6 +13646,11 @@ function provideDynamicFormDI() {
|
|
|
12996
13646
|
useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultProps),
|
|
12997
13647
|
deps: [FormStateManager],
|
|
12998
13648
|
},
|
|
13649
|
+
{
|
|
13650
|
+
provide: DEFAULT_WRAPPERS,
|
|
13651
|
+
useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultWrappers),
|
|
13652
|
+
deps: [FormStateManager],
|
|
13653
|
+
},
|
|
12999
13654
|
{
|
|
13000
13655
|
provide: DEFAULT_VALIDATION_MESSAGES,
|
|
13001
13656
|
useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultValidationMessages),
|
|
@@ -13126,10 +13781,10 @@ class EventDispatcher {
|
|
|
13126
13781
|
disconnect() {
|
|
13127
13782
|
this.bus = undefined;
|
|
13128
13783
|
}
|
|
13129
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
13130
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
13784
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
13785
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventDispatcher });
|
|
13131
13786
|
}
|
|
13132
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
13787
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EventDispatcher, decorators: [{
|
|
13133
13788
|
type: Injectable
|
|
13134
13789
|
}] });
|
|
13135
13790
|
|
|
@@ -13147,16 +13802,17 @@ class DynamicForm {
|
|
|
13147
13802
|
// Inputs (must be declared BEFORE deps connection and stateManager injection)
|
|
13148
13803
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13149
13804
|
/** Form configuration defining the structure, validation, and behavior. */
|
|
13150
|
-
config = input.required({ ...(ngDevMode ? { debugName: "config" } : {}), alias: 'dynamic-form' });
|
|
13805
|
+
config = input.required({ ...(ngDevMode ? { debugName: "config" } : /* istanbul ignore next */ {}), alias: 'dynamic-form' });
|
|
13151
13806
|
/** Runtime form options that override config options when provided. */
|
|
13152
|
-
formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : []));
|
|
13807
|
+
formOptions = input(undefined, ...(ngDevMode ? [{ debugName: "formOptions" }] : /* istanbul ignore next */ []));
|
|
13153
13808
|
/** Form values for two-way data binding. */
|
|
13154
|
-
value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
13809
|
+
value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
13155
13810
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13156
13811
|
// Dependencies
|
|
13157
13812
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13158
13813
|
destroyRef = inject(DestroyRef);
|
|
13159
13814
|
injector = inject(Injector);
|
|
13815
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
13160
13816
|
eventBus = inject(EventBus);
|
|
13161
13817
|
logger = inject(DynamicFormLogger);
|
|
13162
13818
|
dispatcher = inject(EventDispatcher, { optional: true });
|
|
@@ -13221,7 +13877,7 @@ class DynamicForm {
|
|
|
13221
13877
|
totalComponentsCount = computed(() => {
|
|
13222
13878
|
const fields = this.stateManager.formSetup()?.fields ?? [];
|
|
13223
13879
|
return countContainersRecursive(fields) + 1;
|
|
13224
|
-
}, ...(ngDevMode ? [{ debugName: "totalComponentsCount" }] : []));
|
|
13880
|
+
}, ...(ngDevMode ? [{ debugName: "totalComponentsCount" }] : /* istanbul ignore next */ []));
|
|
13225
13881
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13226
13882
|
// Initialization
|
|
13227
13883
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -13338,28 +13994,40 @@ class DynamicForm {
|
|
|
13338
13994
|
error: (err) => this.logger.error('Submission handler error', err),
|
|
13339
13995
|
});
|
|
13340
13996
|
}
|
|
13341
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
13342
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
13997
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13998
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", 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 } }, 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: [provideDynamicFormDI()], ngImport: i0, template: `
|
|
13343
13999
|
@if (shouldRender()) {
|
|
13344
|
-
@
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
14000
|
+
@switch (formModeDetection().mode) {
|
|
14001
|
+
@case ('paged') {
|
|
14002
|
+
<div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
|
|
14003
|
+
}
|
|
14004
|
+
@case ('non-paged') {
|
|
14005
|
+
@for (field of resolvedFields(); track field.key) {
|
|
14006
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
14007
|
+
}
|
|
14008
|
+
}
|
|
14009
|
+
@default {
|
|
14010
|
+
never;
|
|
13349
14011
|
}
|
|
13350
14012
|
}
|
|
13351
14013
|
}
|
|
13352
|
-
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--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{--df-grid-row-gap: 0}\n"], dependencies: [{ kind: "directive", type:
|
|
14014
|
+
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--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{--df-grid-row-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 });
|
|
13353
14015
|
}
|
|
13354
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
14016
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicForm, decorators: [{
|
|
13355
14017
|
type: Component,
|
|
13356
|
-
args: [{ selector: 'form[dynamic-form]', imports: [
|
|
14018
|
+
args: [{ selector: 'form[dynamic-form]', imports: [DfFieldOutlet, PageOrchestratorComponent], template: `
|
|
13357
14019
|
@if (shouldRender()) {
|
|
13358
|
-
@
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
14020
|
+
@switch (formModeDetection().mode) {
|
|
14021
|
+
@case ('paged') {
|
|
14022
|
+
<div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
|
|
14023
|
+
}
|
|
14024
|
+
@case ('non-paged') {
|
|
14025
|
+
@for (field of resolvedFields(); track field.key) {
|
|
14026
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
14029
|
+
@default {
|
|
14030
|
+
never;
|
|
13363
14031
|
}
|
|
13364
14032
|
}
|
|
13365
14033
|
}
|
|
@@ -13374,7 +14042,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
13374
14042
|
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--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{--df-grid-row-gap: 0}\n"] }]
|
|
13375
14043
|
}], 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"] }] } });
|
|
13376
14044
|
/**
|
|
13377
|
-
* Recursively counts container fields (page, row, group, array) in a field tree.
|
|
14045
|
+
* Recursively counts container fields (page, row, group, array, wrapper) in a field tree.
|
|
13378
14046
|
* Descends into container children including array item templates to ensure
|
|
13379
14047
|
* nested containers are counted for accurate initialization tracking.
|
|
13380
14048
|
*/
|
|
@@ -13791,24 +14459,28 @@ function groupFieldMapper(fieldDef) {
|
|
|
13791
14459
|
});
|
|
13792
14460
|
}
|
|
13793
14461
|
|
|
14462
|
+
const ROW_WRAPPERS = [{ type: 'row' }];
|
|
13794
14463
|
/**
|
|
13795
|
-
* Maps a row field definition to component inputs.
|
|
14464
|
+
* Maps a row field definition to container component inputs.
|
|
13796
14465
|
*
|
|
13797
|
-
*
|
|
13798
|
-
*
|
|
14466
|
+
* `row` is a virtual field type: it resolves to `ContainerFieldComponent` with a
|
|
14467
|
+
* synthesized `{ type: 'row' }` wrapper, so the user-facing config stays
|
|
14468
|
+
* `{ type: 'row', fields: [...] }` while the runtime uses the container +
|
|
14469
|
+
* wrapper pipeline.
|
|
13799
14470
|
*
|
|
13800
14471
|
* Supports hidden state resolution via `logic` array or static `hidden` property.
|
|
13801
|
-
*
|
|
13802
|
-
* @param fieldDef The row field definition
|
|
13803
|
-
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
13804
14472
|
*/
|
|
13805
14473
|
function rowFieldMapper(fieldDef) {
|
|
13806
14474
|
const rootFormRegistry = inject(RootFormRegistryService);
|
|
13807
14475
|
const className = buildClassName(fieldDef);
|
|
13808
14476
|
return computed(() => {
|
|
14477
|
+
// Rebuilt each emission so logic-driven updates to fieldDef (e.g. `disabled`)
|
|
14478
|
+
// flow through. Shallow spread is safe while RowField has no nested
|
|
14479
|
+
// mutable props; add a deep-clone if that stops being true.
|
|
14480
|
+
const containerField = { ...fieldDef, wrappers: ROW_WRAPPERS };
|
|
13809
14481
|
const inputs = {
|
|
13810
14482
|
key: fieldDef.key,
|
|
13811
|
-
field:
|
|
14483
|
+
field: containerField,
|
|
13812
14484
|
...(className !== undefined && { className }),
|
|
13813
14485
|
};
|
|
13814
14486
|
applyHiddenLogic(inputs, fieldDef, rootFormRegistry);
|
|
@@ -13867,6 +14539,38 @@ function textFieldMapper(fieldDef) {
|
|
|
13867
14539
|
});
|
|
13868
14540
|
}
|
|
13869
14541
|
|
|
14542
|
+
/**
|
|
14543
|
+
* Maps a container field definition to component inputs.
|
|
14544
|
+
*
|
|
14545
|
+
* Container components are layout containers that don't change the form shape.
|
|
14546
|
+
* The container component will inject FIELD_SIGNAL_CONTEXT directly.
|
|
14547
|
+
*
|
|
14548
|
+
* Supports hidden state resolution via `logic` array or static `hidden` property.
|
|
14549
|
+
*
|
|
14550
|
+
* @param fieldDef The container field definition
|
|
14551
|
+
* @returns Signal containing Record of input names to values for ngComponentOutlet
|
|
14552
|
+
*/
|
|
14553
|
+
function containerFieldMapper(fieldDef) {
|
|
14554
|
+
const rootFormRegistry = inject(RootFormRegistryService);
|
|
14555
|
+
const className = buildClassName(fieldDef);
|
|
14556
|
+
return computed(() => {
|
|
14557
|
+
// Nullify wrapper-related props on the field passed to the container
|
|
14558
|
+
// component — DfFieldOutlet handles field-level wrappers at the outlet
|
|
14559
|
+
// level. Passing them here would cause double-wrapping since the
|
|
14560
|
+
// container's internal wrapper chain also resolves from `this.field()`.
|
|
14561
|
+
// We set wrappers to undefined (not stripped) so the object identity
|
|
14562
|
+
// stays close to the original fieldDef shape.
|
|
14563
|
+
const containerField = { ...fieldDef, wrappers: undefined, skipAutoWrappers: undefined, skipDefaultWrappers: undefined };
|
|
14564
|
+
const inputs = {
|
|
14565
|
+
key: fieldDef.key,
|
|
14566
|
+
field: containerField,
|
|
14567
|
+
...(className !== undefined && { className }),
|
|
14568
|
+
};
|
|
14569
|
+
applyHiddenLogic(inputs, fieldDef, rootFormRegistry);
|
|
14570
|
+
return inputs;
|
|
14571
|
+
});
|
|
14572
|
+
}
|
|
14573
|
+
|
|
13870
14574
|
/**
|
|
13871
14575
|
* Built-in field types provided by the dynamic form library.
|
|
13872
14576
|
*
|
|
@@ -13898,6 +14602,14 @@ function textFieldMapper(fieldDef) {
|
|
|
13898
14602
|
* { type: 'page', key: 'step1', fields: [...] }
|
|
13899
14603
|
* ```
|
|
13900
14604
|
*/
|
|
14605
|
+
/**
|
|
14606
|
+
* Base field type definitions for fields that don't use the `field` input.
|
|
14607
|
+
* Used by display-only fields like `text` that render immediately without
|
|
14608
|
+
* waiting for form value integration.
|
|
14609
|
+
*/
|
|
14610
|
+
const DISPLAY_FIELD_TYPES_BASE = {
|
|
14611
|
+
renderReadyWhen: [],
|
|
14612
|
+
};
|
|
13901
14613
|
/**
|
|
13902
14614
|
* Built-in field types provided by the dynamic form library.
|
|
13903
14615
|
*
|
|
@@ -13908,7 +14620,10 @@ function textFieldMapper(fieldDef) {
|
|
|
13908
14620
|
const BUILT_IN_FIELDS = [
|
|
13909
14621
|
{
|
|
13910
14622
|
name: 'row',
|
|
13911
|
-
|
|
14623
|
+
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
14624
|
+
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
14625
|
+
// renders the flex/grid layout via RowWrapperComponent.
|
|
14626
|
+
loadComponent: () => Promise.resolve().then(function () { return containerField_component; }),
|
|
13912
14627
|
mapper: rowFieldMapper,
|
|
13913
14628
|
valueHandling: 'flatten',
|
|
13914
14629
|
},
|
|
@@ -13926,23 +14641,90 @@ const BUILT_IN_FIELDS = [
|
|
|
13926
14641
|
},
|
|
13927
14642
|
{
|
|
13928
14643
|
name: 'page',
|
|
13929
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
14644
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-CEN5u1dd.mjs'),
|
|
13930
14645
|
mapper: pageFieldMapper,
|
|
13931
14646
|
valueHandling: 'flatten',
|
|
13932
14647
|
},
|
|
13933
14648
|
{
|
|
13934
14649
|
name: 'text',
|
|
13935
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
14650
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-9OIByjZZ.mjs'),
|
|
13936
14651
|
mapper: textFieldMapper,
|
|
13937
14652
|
valueHandling: 'exclude',
|
|
14653
|
+
...DISPLAY_FIELD_TYPES_BASE,
|
|
13938
14654
|
},
|
|
13939
14655
|
{
|
|
13940
14656
|
name: 'hidden',
|
|
13941
14657
|
// Componentless field - no loadComponent or mapper needed
|
|
13942
14658
|
valueHandling: 'include',
|
|
13943
14659
|
},
|
|
14660
|
+
{
|
|
14661
|
+
name: 'container',
|
|
14662
|
+
loadComponent: () => Promise.resolve().then(function () { return containerField_component; }),
|
|
14663
|
+
mapper: containerFieldMapper,
|
|
14664
|
+
valueHandling: 'flatten',
|
|
14665
|
+
},
|
|
14666
|
+
];
|
|
14667
|
+
const BUILT_IN_WRAPPERS = [
|
|
14668
|
+
{
|
|
14669
|
+
wrapperName: 'css',
|
|
14670
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-Civn5kLJ.mjs'),
|
|
14671
|
+
},
|
|
14672
|
+
{
|
|
14673
|
+
wrapperName: 'row',
|
|
14674
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs'),
|
|
14675
|
+
},
|
|
13944
14676
|
];
|
|
13945
14677
|
|
|
14678
|
+
/**
|
|
14679
|
+
* Bundle wrapper registrations into a single object that can be passed to
|
|
14680
|
+
* `provideDynamicForm(...)`.
|
|
14681
|
+
*
|
|
14682
|
+
* @example
|
|
14683
|
+
* ```typescript
|
|
14684
|
+
* const appWrappers = createWrappers(
|
|
14685
|
+
* {
|
|
14686
|
+
* wrapperName: 'section',
|
|
14687
|
+
* loadComponent: () => import('./section-wrapper'),
|
|
14688
|
+
* props: wrapperProps<SectionWrapper>(),
|
|
14689
|
+
* },
|
|
14690
|
+
* {
|
|
14691
|
+
* wrapperName: 'highlight',
|
|
14692
|
+
* loadComponent: () => import('./highlight-wrapper'),
|
|
14693
|
+
* types: ['input', 'select'],
|
|
14694
|
+
* props: wrapperProps<HighlightWrapper>(),
|
|
14695
|
+
* },
|
|
14696
|
+
* );
|
|
14697
|
+
*
|
|
14698
|
+
* declare module '@ng-forge/dynamic-forms' {
|
|
14699
|
+
* interface FieldRegistryWrappers extends InferWrapperRegistry<typeof appWrappers> {}
|
|
14700
|
+
* }
|
|
14701
|
+
*
|
|
14702
|
+
* bootstrapApplication(AppComponent, {
|
|
14703
|
+
* providers: [provideDynamicForm(appWrappers)],
|
|
14704
|
+
* });
|
|
14705
|
+
* ```
|
|
14706
|
+
*/
|
|
14707
|
+
function createWrappers(...registrations) {
|
|
14708
|
+
return {
|
|
14709
|
+
ɵkind: 'wrappers',
|
|
14710
|
+
ɵregistrations: registrations,
|
|
14711
|
+
ɵdefinitions: registrations.map(({ wrapperName, loadComponent, types }) => ({
|
|
14712
|
+
wrapperName,
|
|
14713
|
+
loadComponent,
|
|
14714
|
+
types,
|
|
14715
|
+
})),
|
|
14716
|
+
};
|
|
14717
|
+
}
|
|
14718
|
+
/** Type guard for a `WrappersBundle`. */
|
|
14719
|
+
function isWrappersBundle(value) {
|
|
14720
|
+
return (typeof value === 'object' &&
|
|
14721
|
+
value !== null &&
|
|
14722
|
+
'ɵkind' in value &&
|
|
14723
|
+
value.ɵkind === 'wrappers' &&
|
|
14724
|
+
'ɵdefinitions' in value &&
|
|
14725
|
+
Array.isArray(value.ɵdefinitions));
|
|
14726
|
+
}
|
|
14727
|
+
|
|
13946
14728
|
/**
|
|
13947
14729
|
* Provider function to configure the dynamic form system with field types and options.
|
|
13948
14730
|
*
|
|
@@ -13992,19 +14774,14 @@ const BUILT_IN_FIELDS = [
|
|
|
13992
14774
|
* @public
|
|
13993
14775
|
*/
|
|
13994
14776
|
function provideDynamicForm(...items) {
|
|
13995
|
-
// Separate field types
|
|
13996
|
-
const fieldTypes = items.filter((item) => !isDynamicFormFeature(item));
|
|
14777
|
+
// Separate field types, wrapper types, wrapper bundles, and features
|
|
14778
|
+
const fieldTypes = items.filter((item) => !isDynamicFormFeature(item) && !isWrapperTypeDefinition(item) && !isWrappersBundle(item));
|
|
14779
|
+
const wrapperTypes = items.filter(isWrapperTypeDefinition);
|
|
14780
|
+
const wrapperBundles = items.filter(isWrappersBundle);
|
|
13997
14781
|
const features = items.filter(isDynamicFormFeature);
|
|
13998
14782
|
const fields = [...BUILT_IN_FIELDS, ...fieldTypes];
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
fieldTypes.forEach((fieldTypeArray) => {
|
|
14002
|
-
const fieldTypeWithConfig = fieldTypeArray;
|
|
14003
|
-
if (fieldTypeWithConfig.__configProviders) {
|
|
14004
|
-
configProviders.push(...fieldTypeWithConfig.__configProviders);
|
|
14005
|
-
}
|
|
14006
|
-
});
|
|
14007
|
-
// Extract providers from features
|
|
14783
|
+
const wrappers = [...BUILT_IN_WRAPPERS, ...wrapperTypes, ...wrapperBundles.flatMap((bundle) => bundle.ɵdefinitions)];
|
|
14784
|
+
// Extract providers from features (includes config features like material-config, bootstrap-config, etc.)
|
|
14008
14785
|
const featureProviders = [];
|
|
14009
14786
|
const hasLoggerFeature = features.some((feature) => feature.ɵkind === 'logger');
|
|
14010
14787
|
features.forEach((feature) => {
|
|
@@ -14031,7 +14808,41 @@ function provideDynamicForm(...items) {
|
|
|
14031
14808
|
return registry;
|
|
14032
14809
|
},
|
|
14033
14810
|
},
|
|
14034
|
-
|
|
14811
|
+
// Always provide default Wrapper classes
|
|
14812
|
+
{
|
|
14813
|
+
provide: WRAPPER_REGISTRY,
|
|
14814
|
+
useFactory: () => {
|
|
14815
|
+
const logger = inject(DynamicFormLogger);
|
|
14816
|
+
const registry = new Map();
|
|
14817
|
+
// Add custom wrapper types
|
|
14818
|
+
wrappers.forEach((wrapperType) => {
|
|
14819
|
+
if (registry.has(wrapperType.wrapperName)) {
|
|
14820
|
+
logger.warn(`Wrapper type "${wrapperType.wrapperName}" is already registered. Overwriting.`);
|
|
14821
|
+
}
|
|
14822
|
+
registry.set(wrapperType.wrapperName, wrapperType);
|
|
14823
|
+
});
|
|
14824
|
+
return registry;
|
|
14825
|
+
},
|
|
14826
|
+
},
|
|
14827
|
+
// Pre-computed reverse index for auto-association lookup — built once so
|
|
14828
|
+
// resolveWrappers is O(1) per field render instead of scanning every
|
|
14829
|
+
// registered wrapper.
|
|
14830
|
+
{
|
|
14831
|
+
provide: WRAPPER_AUTO_ASSOCIATIONS,
|
|
14832
|
+
useFactory: () => {
|
|
14833
|
+
const autoMap = new Map();
|
|
14834
|
+
for (const wrapperType of wrappers) {
|
|
14835
|
+
if (!wrapperType.types)
|
|
14836
|
+
continue;
|
|
14837
|
+
for (const fieldType of wrapperType.types) {
|
|
14838
|
+
const existing = autoMap.get(fieldType) ?? [];
|
|
14839
|
+
existing.push({ type: wrapperType.wrapperName });
|
|
14840
|
+
autoMap.set(fieldType, existing);
|
|
14841
|
+
}
|
|
14842
|
+
}
|
|
14843
|
+
return autoMap;
|
|
14844
|
+
},
|
|
14845
|
+
},
|
|
14035
14846
|
...featureProviders,
|
|
14036
14847
|
]);
|
|
14037
14848
|
}
|
|
@@ -14166,8 +14977,38 @@ function withValueExclusionDefaults(config) {
|
|
|
14166
14977
|
return createFeature('value-exclusion', [{ provide: VALUE_EXCLUSION_DEFAULTS, useValue: resolved }]);
|
|
14167
14978
|
}
|
|
14168
14979
|
|
|
14980
|
+
/**
|
|
14981
|
+
* Zero-cost type carrier used in wrapper registrations.
|
|
14982
|
+
*
|
|
14983
|
+
* Returns `undefined` at runtime but is typed as `T`, so TypeScript can
|
|
14984
|
+
* thread a wrapper's config type through `createWrappers(...)` into the
|
|
14985
|
+
* `InferWrapperRegistry<typeof ...>` utility without requiring users to
|
|
14986
|
+
* hand-write the augmentation shape.
|
|
14987
|
+
*
|
|
14988
|
+
* @example
|
|
14989
|
+
* ```typescript
|
|
14990
|
+
* const wrappers = createWrappers(
|
|
14991
|
+
* {
|
|
14992
|
+
* wrapperName: 'section',
|
|
14993
|
+
* loadComponent: () => import('./section-wrapper'),
|
|
14994
|
+
* props: wrapperProps<SectionWrapper>(),
|
|
14995
|
+
* },
|
|
14996
|
+
* );
|
|
14997
|
+
*
|
|
14998
|
+
* declare module '@ng-forge/dynamic-forms' {
|
|
14999
|
+
* interface FieldRegistryWrappers extends InferWrapperRegistry<typeof wrappers> {}
|
|
15000
|
+
* }
|
|
15001
|
+
* ```
|
|
15002
|
+
*/
|
|
15003
|
+
function wrapperProps() {
|
|
15004
|
+
return undefined;
|
|
15005
|
+
}
|
|
15006
|
+
|
|
14169
15007
|
function isValueField(field) {
|
|
14170
|
-
|
|
15008
|
+
// `nullable: true` without an explicit `value` is still a value-bearing declaration:
|
|
15009
|
+
// the field resolves to `null` at runtime. Treating it as a value field keeps
|
|
15010
|
+
// downstream narrowing consistent with getFieldDefaultValue's behavior.
|
|
15011
|
+
return 'value' in field || 'nullable' in field;
|
|
14171
15012
|
}
|
|
14172
15013
|
|
|
14173
15014
|
function isCheckedField(field) {
|
|
@@ -14205,10 +15046,13 @@ function isCheckedField(field) {
|
|
|
14205
15046
|
* eventBus.dispatch(arrayEvent('contacts').pop()); // Remove last
|
|
14206
15047
|
* eventBus.dispatch(arrayEvent('contacts').shift()); // Remove first
|
|
14207
15048
|
* eventBus.dispatch(arrayEvent('contacts').removeAt(2)); // Remove at index
|
|
15049
|
+
*
|
|
15050
|
+
* // Reordering items (no template needed, preserves item identity)
|
|
15051
|
+
* eventBus.dispatch(arrayEvent('contacts').move(0, 2)); // Move first to third
|
|
14208
15052
|
* ```
|
|
14209
15053
|
*
|
|
14210
15054
|
* @param arrayKey - The key of the array field to operate on
|
|
14211
|
-
* @returns An object with methods for all
|
|
15055
|
+
* @returns An object with methods for all 7 array operations
|
|
14212
15056
|
*/
|
|
14213
15057
|
function arrayEvent(arrayKey) {
|
|
14214
15058
|
return {
|
|
@@ -14319,6 +15163,22 @@ function arrayEvent(arrayKey) {
|
|
|
14319
15163
|
* ```
|
|
14320
15164
|
*/
|
|
14321
15165
|
removeAt: (index) => new RemoveAtIndexEvent(arrayKey, index),
|
|
15166
|
+
/**
|
|
15167
|
+
* Move an existing item from one position to another within the array.
|
|
15168
|
+
* This is an atomic reorder — the item is NOT destroyed and recreated.
|
|
15169
|
+
* The resolved component, form value, and stored template are preserved.
|
|
15170
|
+
*
|
|
15171
|
+
* @param from - The current index of the item to move
|
|
15172
|
+
* @param to - The target index to move the item to
|
|
15173
|
+
* @returns A MoveArrayItemEvent to dispatch
|
|
15174
|
+
*
|
|
15175
|
+
* @example
|
|
15176
|
+
* ```typescript
|
|
15177
|
+
* // Move item from index 0 to index 2
|
|
15178
|
+
* eventBus.dispatch(arrayEvent('contacts').move(0, 2));
|
|
15179
|
+
* ```
|
|
15180
|
+
*/
|
|
15181
|
+
move: (from, to) => new MoveArrayItemEvent(arrayKey, from, to),
|
|
14322
15182
|
};
|
|
14323
15183
|
}
|
|
14324
15184
|
|
|
@@ -14434,11 +15294,11 @@ function formConfig(config) {
|
|
|
14434
15294
|
/**
|
|
14435
15295
|
* Container field types that do NOT support labels
|
|
14436
15296
|
*/
|
|
14437
|
-
const CONTAINER_TYPES = ['group', 'row', 'array'];
|
|
15297
|
+
const CONTAINER_TYPES = ['group', 'row', 'array', 'container'];
|
|
14438
15298
|
/**
|
|
14439
15299
|
* Container and page field types that only support 'hidden' logic
|
|
14440
15300
|
*/
|
|
14441
|
-
const HIDDEN_ONLY_LOGIC_TYPES = ['group', 'row', 'array', 'page'];
|
|
15301
|
+
const HIDDEN_ONLY_LOGIC_TYPES = ['group', 'row', 'array', 'container', 'page'];
|
|
14442
15302
|
/**
|
|
14443
15303
|
* Field types that support options at the field level
|
|
14444
15304
|
*/
|
|
@@ -14698,14 +15558,13 @@ class DynamicTextPipe {
|
|
|
14698
15558
|
}
|
|
14699
15559
|
return of(value || '');
|
|
14700
15560
|
}
|
|
14701
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
14702
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.
|
|
15561
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
15562
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
14703
15563
|
}
|
|
14704
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15564
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
14705
15565
|
type: Pipe,
|
|
14706
15566
|
args: [{
|
|
14707
15567
|
name: 'dynamicText',
|
|
14708
|
-
standalone: true,
|
|
14709
15568
|
}]
|
|
14710
15569
|
}] });
|
|
14711
15570
|
|
|
@@ -14752,106 +15611,30 @@ function computeContainerHostClasses(containerType, className) {
|
|
|
14752
15611
|
* When resolved fields become non-empty, emits a ComponentInitializedEvent
|
|
14753
15612
|
* via afterNextRender to signal the container is ready.
|
|
14754
15613
|
*
|
|
15614
|
+
* `componentType` may be a static value or a lazy getter — the latter lets
|
|
15615
|
+
* virtual field types (e.g. `row`, which renders via ContainerFieldComponent)
|
|
15616
|
+
* preserve their original type on the emitted event instead of collapsing to
|
|
15617
|
+
* the host component's type.
|
|
15618
|
+
*
|
|
14755
15619
|
* @param resolvedFields - Signal of resolved fields
|
|
14756
15620
|
* @param eventBus - EventBus for dispatching initialization events
|
|
14757
|
-
* @param componentType -
|
|
15621
|
+
* @param componentType - Static type, or a getter evaluated at emission time
|
|
14758
15622
|
* @param fieldKeyFn - Function returning the field's key
|
|
14759
15623
|
* @param injector - Injector for afterNextRender scheduling
|
|
14760
15624
|
*/
|
|
14761
15625
|
function setupContainerInitEffect(resolvedFields, eventBus, componentType, fieldKeyFn, injector) {
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
15626
|
+
const allReady = computed(() => {
|
|
15627
|
+
const fields = resolvedFields();
|
|
15628
|
+
return fields.length > 0 && fields.every((field) => field.renderReady());
|
|
15629
|
+
}, ...(ngDevMode ? [{ debugName: "allReady" }] : /* istanbul ignore next */ []));
|
|
15630
|
+
explicitEffect([allReady], ([ready]) => {
|
|
15631
|
+
if (ready) {
|
|
15632
|
+
const type = typeof componentType === 'function' ? componentType() : componentType;
|
|
15633
|
+
emitComponentInitialized(eventBus, type, fieldKeyFn(), injector);
|
|
14765
15634
|
}
|
|
14766
15635
|
});
|
|
14767
15636
|
}
|
|
14768
15637
|
|
|
14769
|
-
/**
|
|
14770
|
-
* Layout container for horizontal field arrangement.
|
|
14771
|
-
*
|
|
14772
|
-
* Does not create a new form context - fields share the parent's context.
|
|
14773
|
-
* Field values are flattened into the parent form (no nesting under row key).
|
|
14774
|
-
* Purely a visual/layout container with no impact on form structure.
|
|
14775
|
-
*/
|
|
14776
|
-
class RowFieldComponent {
|
|
14777
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14778
|
-
// Dependencies
|
|
14779
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14780
|
-
destroyRef = inject(DestroyRef);
|
|
14781
|
-
fieldRegistry = injectFieldRegistry();
|
|
14782
|
-
injector = inject(Injector);
|
|
14783
|
-
eventBus = inject(EventBus);
|
|
14784
|
-
logger = inject(DynamicFormLogger);
|
|
14785
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14786
|
-
// Inputs
|
|
14787
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14788
|
-
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
14789
|
-
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
|
|
14790
|
-
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
|
|
14791
|
-
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
|
|
14792
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14793
|
-
// Computed Signals
|
|
14794
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14795
|
-
hostClasses = computed(() => computeContainerHostClasses('row', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
14796
|
-
disabled = computed(() => this.field().disabled || false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
14797
|
-
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
|
|
14798
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14799
|
-
// Field Resolution
|
|
14800
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14801
|
-
fieldsSource = computed(() => (this.field().fields || []), ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
|
|
14802
|
-
resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
|
|
14803
|
-
loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
|
|
14804
|
-
registry: this.rawFieldRegistry(),
|
|
14805
|
-
injector: this.injector,
|
|
14806
|
-
destroyRef: this.destroyRef,
|
|
14807
|
-
onError: (fieldDef, error) => {
|
|
14808
|
-
const fieldKey = fieldDef.key || '<no key>';
|
|
14809
|
-
const rowKey = this.field().key || '<no key>';
|
|
14810
|
-
this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
|
|
14811
|
-
`within row '${rowKey}'. Ensure the field type is registered in your field registry.`, error);
|
|
14812
|
-
},
|
|
14813
|
-
})), { initialValue: [], injector: this.injector });
|
|
14814
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14815
|
-
// Constructor
|
|
14816
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14817
|
-
constructor() {
|
|
14818
|
-
this.setupEffects();
|
|
14819
|
-
}
|
|
14820
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14821
|
-
// Private Methods
|
|
14822
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14823
|
-
setupEffects() {
|
|
14824
|
-
setupContainerInitEffect(this.resolvedFields, this.eventBus, 'row', () => this.field().key, this.injector);
|
|
14825
|
-
}
|
|
14826
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14827
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
|
|
14828
|
-
@for (field of resolvedFields(); track field.key) {
|
|
14829
|
-
<ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
|
|
14830
|
-
}
|
|
14831
|
-
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14832
|
-
}
|
|
14833
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RowFieldComponent, decorators: [{
|
|
14834
|
-
type: Component,
|
|
14835
|
-
args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
|
|
14836
|
-
@for (field of resolvedFields(); track field.key) {
|
|
14837
|
-
<ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
|
|
14838
|
-
}
|
|
14839
|
-
`, host: {
|
|
14840
|
-
'[class]': 'hostClasses()',
|
|
14841
|
-
'[class.disabled]': 'disabled()',
|
|
14842
|
-
'[class.df-container-hidden]': 'hidden()',
|
|
14843
|
-
'[attr.aria-hidden]': 'hidden() || null',
|
|
14844
|
-
'[id]': '`${key()}`',
|
|
14845
|
-
'[attr.data-testid]': 'key()',
|
|
14846
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}:host.df-container-hidden{display:none}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"] }]
|
|
14847
|
-
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
|
|
14848
|
-
|
|
14849
|
-
var rowField_component = /*#__PURE__*/Object.freeze({
|
|
14850
|
-
__proto__: null,
|
|
14851
|
-
RowFieldComponent: RowFieldComponent,
|
|
14852
|
-
default: RowFieldComponent
|
|
14853
|
-
});
|
|
14854
|
-
|
|
14855
15638
|
/**
|
|
14856
15639
|
* Container component for rendering nested form groups.
|
|
14857
15640
|
*
|
|
@@ -14867,6 +15650,7 @@ class GroupFieldComponent {
|
|
|
14867
15650
|
fieldRegistry = injectFieldRegistry();
|
|
14868
15651
|
parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
|
|
14869
15652
|
injector = inject(Injector);
|
|
15653
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
14870
15654
|
eventBus = inject(EventBus);
|
|
14871
15655
|
logger = inject(DynamicFormLogger);
|
|
14872
15656
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -14876,24 +15660,26 @@ class GroupFieldComponent {
|
|
|
14876
15660
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14877
15661
|
// Inputs
|
|
14878
15662
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14879
|
-
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
14880
|
-
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
|
|
14881
|
-
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
|
|
14882
|
-
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
|
|
15663
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
15664
|
+
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
15665
|
+
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
|
|
15666
|
+
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
14883
15667
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14884
15668
|
// Computed Signals
|
|
14885
15669
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14886
|
-
hostClasses = computed(() => computeContainerHostClasses('group', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
14887
|
-
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
|
|
15670
|
+
hostClasses = computed(() => computeContainerHostClasses('group', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
|
|
15671
|
+
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
|
|
14888
15672
|
formSetup = computed(() => {
|
|
14889
15673
|
const groupField = this.field();
|
|
14890
15674
|
const registry = this.rawFieldRegistry();
|
|
14891
15675
|
if (groupField.fields && groupField.fields.length > 0) {
|
|
14892
15676
|
const flattenedFields = this.fieldProcessors.memoizedFlattenFields(groupField.fields, registry);
|
|
15677
|
+
const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(groupField.fields, registry);
|
|
14893
15678
|
const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
|
|
14894
15679
|
const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
|
|
14895
15680
|
return {
|
|
14896
|
-
fields:
|
|
15681
|
+
fields: flattenedFieldsForRendering,
|
|
15682
|
+
schemaFields: flattenedFields,
|
|
14897
15683
|
originalFields: groupField.fields,
|
|
14898
15684
|
defaultValues,
|
|
14899
15685
|
registry,
|
|
@@ -14901,12 +15687,13 @@ class GroupFieldComponent {
|
|
|
14901
15687
|
}
|
|
14902
15688
|
return {
|
|
14903
15689
|
fields: [],
|
|
15690
|
+
schemaFields: [],
|
|
14904
15691
|
originalFields: [],
|
|
14905
15692
|
defaultValues: {},
|
|
14906
15693
|
registry,
|
|
14907
15694
|
};
|
|
14908
|
-
}, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
|
|
14909
|
-
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
15695
|
+
}, ...(ngDevMode ? [{ debugName: "formSetup" }] : /* istanbul ignore next */ []));
|
|
15696
|
+
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
|
|
14910
15697
|
/**
|
|
14911
15698
|
* Entity computed from parent value, group key, and defaults.
|
|
14912
15699
|
* Uses deep equality check to prevent unnecessary updates when
|
|
@@ -14925,28 +15712,28 @@ class GroupFieldComponent {
|
|
|
14925
15712
|
form = computed(() => {
|
|
14926
15713
|
return runInInjectionContext(this.injector, () => {
|
|
14927
15714
|
const setup = this.formSetup();
|
|
14928
|
-
if (setup.
|
|
14929
|
-
const schema = createSchemaFromFields(setup.
|
|
15715
|
+
if (setup.schemaFields.length > 0) {
|
|
15716
|
+
const schema = createSchemaFromFields(setup.schemaFields, setup.registry);
|
|
14930
15717
|
return untracked(() => form(this.entity, schema));
|
|
14931
15718
|
}
|
|
14932
15719
|
return untracked(() => form(this.entity));
|
|
14933
15720
|
});
|
|
14934
|
-
}, ...(ngDevMode ? [{ debugName: "form" }] : []));
|
|
15721
|
+
}, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
|
|
14935
15722
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14936
15723
|
// Public State Signals
|
|
14937
15724
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14938
|
-
formValue = computed(() => this.entity(), ...(ngDevMode ? [{ debugName: "formValue" }] : []));
|
|
14939
|
-
valid = computed(() => this.form()().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
|
|
14940
|
-
invalid = computed(() => this.form()().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
14941
|
-
dirty = computed(() => this.form()().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
|
|
14942
|
-
touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
14943
|
-
errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
14944
|
-
disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
15725
|
+
formValue = computed(() => this.entity(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
15726
|
+
valid = computed(() => this.form()().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
|
|
15727
|
+
invalid = computed(() => this.form()().invalid(), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
15728
|
+
dirty = computed(() => this.form()().dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
15729
|
+
touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
15730
|
+
errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
15731
|
+
disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
14945
15732
|
nestedFieldTree = computed(() => {
|
|
14946
15733
|
const parentForm = this.parentFieldSignalContext.form;
|
|
14947
15734
|
const groupKey = this.field().key;
|
|
14948
15735
|
return parentForm[groupKey];
|
|
14949
|
-
}, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
|
|
15736
|
+
}, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : /* istanbul ignore next */ []));
|
|
14950
15737
|
groupFieldSignalContext = (() => {
|
|
14951
15738
|
const nestedFieldTree = this.nestedFieldTree;
|
|
14952
15739
|
return {
|
|
@@ -14971,7 +15758,7 @@ class GroupFieldComponent {
|
|
|
14971
15758
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14972
15759
|
// Field Resolution
|
|
14973
15760
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14974
|
-
fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
|
|
15761
|
+
fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : /* istanbul ignore next */ []));
|
|
14975
15762
|
resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
|
|
14976
15763
|
loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
|
|
14977
15764
|
registry: this.rawFieldRegistry(),
|
|
@@ -15000,18 +15787,18 @@ class GroupFieldComponent {
|
|
|
15000
15787
|
}
|
|
15001
15788
|
}, { defer: true });
|
|
15002
15789
|
}
|
|
15003
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
15004
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
15790
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15791
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
|
|
15005
15792
|
@for (field of resolvedFields(); track field.key) {
|
|
15006
|
-
<ng-container *
|
|
15793
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15007
15794
|
}
|
|
15008
|
-
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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{border:none;margin:0;padding:0;min-width:0;display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}\n"], dependencies: [{ kind: "directive", type:
|
|
15795
|
+
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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{border:none;margin:0;padding:0;min-width:0;display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}\n"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15009
15796
|
}
|
|
15010
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
15797
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
15011
15798
|
type: Component,
|
|
15012
|
-
args: [{ selector: 'fieldset[group-field]', imports: [
|
|
15799
|
+
args: [{ selector: 'fieldset[group-field]', imports: [DfFieldOutlet], template: `
|
|
15013
15800
|
@for (field of resolvedFields(); track field.key) {
|
|
15014
|
-
<ng-container *
|
|
15801
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15015
15802
|
}
|
|
15016
15803
|
`, host: {
|
|
15017
15804
|
'[class]': 'hostClasses()',
|
|
@@ -15112,7 +15899,7 @@ function createArrayItemInjectorAndInputs(options) {
|
|
|
15112
15899
|
return undefined;
|
|
15113
15900
|
}
|
|
15114
15901
|
return arrayFieldTree[index];
|
|
15115
|
-
}, { ...(ngDevMode ? { debugName: "itemFormAccessor" } : {}), equal: isEqual });
|
|
15902
|
+
}, { ...(ngDevMode ? { debugName: "itemFormAccessor" } : /* istanbul ignore next */ {}), equal: isEqual });
|
|
15116
15903
|
const injector = createItemInjector({
|
|
15117
15904
|
itemFormAccessor,
|
|
15118
15905
|
indexSignal,
|
|
@@ -15219,9 +16006,9 @@ function resolveArrayItem(options) {
|
|
|
15219
16006
|
const indexSignal = linkedSignal(() => {
|
|
15220
16007
|
const positionMap = itemPositionMap();
|
|
15221
16008
|
return positionMap.get(itemId) ?? index;
|
|
15222
|
-
}, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
|
|
16009
|
+
}, ...(ngDevMode ? [{ debugName: "indexSignal" }] : /* istanbul ignore next */ []));
|
|
15223
16010
|
// Resolve all templates in parallel
|
|
15224
|
-
const fieldObservables = templates.map((template) => from(loadTypeComponent(template.type)).pipe(map
|
|
16011
|
+
const fieldObservables = templates.map((template) => from(loadTypeComponent(template.type)).pipe(map((component) => {
|
|
15225
16012
|
if (destroyRef.destroyed) {
|
|
15226
16013
|
return undefined;
|
|
15227
16014
|
}
|
|
@@ -15243,9 +16030,12 @@ function resolveArrayItem(options) {
|
|
|
15243
16030
|
return undefined;
|
|
15244
16031
|
}
|
|
15245
16032
|
return {
|
|
16033
|
+
key: template.key,
|
|
16034
|
+
fieldDef: template,
|
|
15246
16035
|
component,
|
|
15247
16036
|
injector,
|
|
15248
16037
|
inputs,
|
|
16038
|
+
renderReady: createRenderReadySignal(inputs, registry.get(template.type)),
|
|
15249
16039
|
};
|
|
15250
16040
|
}), catchError((error) => {
|
|
15251
16041
|
if (!destroyRef.destroyed) {
|
|
@@ -15255,7 +16045,7 @@ function resolveArrayItem(options) {
|
|
|
15255
16045
|
}
|
|
15256
16046
|
return of(undefined);
|
|
15257
16047
|
})));
|
|
15258
|
-
return forkJoin(fieldObservables).pipe(map
|
|
16048
|
+
return forkJoin(fieldObservables).pipe(map((fields) => {
|
|
15259
16049
|
const validFields = fields.filter((f) => f !== undefined);
|
|
15260
16050
|
// If no fields resolved, return undefined
|
|
15261
16051
|
if (validFields.length === 0) {
|
|
@@ -15275,6 +16065,7 @@ const ARRAY_EVENT_TYPES = [
|
|
|
15275
16065
|
'append-array-item',
|
|
15276
16066
|
'prepend-array-item',
|
|
15277
16067
|
'insert-array-item',
|
|
16068
|
+
'move-array-item',
|
|
15278
16069
|
'pop-array-item',
|
|
15279
16070
|
'shift-array-item',
|
|
15280
16071
|
'remove-at-index',
|
|
@@ -15291,6 +16082,8 @@ function toArrayAction(event) {
|
|
|
15291
16082
|
return { action: 'add', template: event.template, index: 0 };
|
|
15292
16083
|
case 'insert-array-item':
|
|
15293
16084
|
return { action: 'add', template: event.template, index: event.index };
|
|
16085
|
+
case 'move-array-item':
|
|
16086
|
+
return { action: 'move', fromIndex: event.fromIndex, toIndex: event.toIndex };
|
|
15294
16087
|
case 'pop-array-item':
|
|
15295
16088
|
return { action: 'remove' };
|
|
15296
16089
|
case 'shift-array-item':
|
|
@@ -15320,13 +16113,13 @@ function toArrayAction(event) {
|
|
|
15320
16113
|
* ```
|
|
15321
16114
|
*/
|
|
15322
16115
|
function observeArrayActions(eventBus, arrayKey) {
|
|
15323
|
-
return eventBus.on(ARRAY_EVENT_TYPES).pipe(filter((event) => event.arrayKey === arrayKey()), map
|
|
16116
|
+
return eventBus.on(ARRAY_EVENT_TYPES).pipe(filter((event) => event.arrayKey === arrayKey()), map(toArrayAction));
|
|
15324
16117
|
}
|
|
15325
16118
|
|
|
15326
16119
|
/**
|
|
15327
16120
|
* Container component for rendering dynamic arrays of fields.
|
|
15328
16121
|
*
|
|
15329
|
-
* Supports add/remove operations via the arrayEvent() builder API.
|
|
16122
|
+
* Supports add/remove/move operations via the arrayEvent() builder API.
|
|
15330
16123
|
* Uses differential updates to optimize rendering - only recreates items when necessary.
|
|
15331
16124
|
* Each item gets a scoped injector with ARRAY_CONTEXT for position-aware operations.
|
|
15332
16125
|
* Supports multiple sibling fields per array item (e.g., name + email without a wrapper).
|
|
@@ -15339,6 +16132,7 @@ class ArrayFieldComponent {
|
|
|
15339
16132
|
fieldRegistry = injectFieldRegistry();
|
|
15340
16133
|
parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
|
|
15341
16134
|
parentInjector = inject(Injector);
|
|
16135
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
15342
16136
|
eventBus = inject(EventBus);
|
|
15343
16137
|
logger = inject(DynamicFormLogger);
|
|
15344
16138
|
templateRegistry = inject(ARRAY_TEMPLATE_REGISTRY);
|
|
@@ -15346,10 +16140,10 @@ class ArrayFieldComponent {
|
|
|
15346
16140
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15347
16141
|
// Inputs
|
|
15348
16142
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15349
|
-
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
15350
|
-
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : []));
|
|
15351
|
-
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : []));
|
|
15352
|
-
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
|
|
16143
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
16144
|
+
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
16145
|
+
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
|
|
16146
|
+
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
15353
16147
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15354
16148
|
// Computed Signals
|
|
15355
16149
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -15357,8 +16151,8 @@ class ArrayFieldComponent {
|
|
|
15357
16151
|
const base = 'df-field df-array';
|
|
15358
16152
|
const custom = this.className();
|
|
15359
16153
|
return custom ? `${base} ${custom}` : base;
|
|
15360
|
-
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
15361
|
-
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
|
|
16154
|
+
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
|
|
16155
|
+
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
|
|
15362
16156
|
/**
|
|
15363
16157
|
* Gets the auto-remove button FieldDef from normalization metadata.
|
|
15364
16158
|
* Set by simplified array normalization for primitive arrays with remove buttons.
|
|
@@ -15368,7 +16162,7 @@ class ArrayFieldComponent {
|
|
|
15368
16162
|
autoRemoveButton = computed(() => {
|
|
15369
16163
|
const metadata = getNormalizedArrayMetadata(this.field());
|
|
15370
16164
|
return metadata?.autoRemoveButton;
|
|
15371
|
-
}, ...(ngDevMode ? [{ debugName: "autoRemoveButton" }] : []));
|
|
16165
|
+
}, ...(ngDevMode ? [{ debugName: "autoRemoveButton" }] : /* istanbul ignore next */ []));
|
|
15372
16166
|
/**
|
|
15373
16167
|
* For primitive array items, the key of the value field template (e.g., 'value').
|
|
15374
16168
|
* Used to wrap the FormControl in the item context so getFieldTree(key) works.
|
|
@@ -15394,7 +16188,22 @@ class ArrayFieldComponent {
|
|
|
15394
16188
|
return definitions[0].key;
|
|
15395
16189
|
}
|
|
15396
16190
|
return undefined;
|
|
15397
|
-
}, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : []));
|
|
16191
|
+
}, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
|
|
16192
|
+
/**
|
|
16193
|
+
* Normalized item templates WITHOUT auto-remove button appended.
|
|
16194
|
+
* Each element is normalized to an array: single FieldDef → [FieldDef], array stays as-is.
|
|
16195
|
+
*
|
|
16196
|
+
* Used by moveItem() to stash raw templates into the templateRegistry, preserving
|
|
16197
|
+
* the invariant that registry entries are pre-synthesis (withAutoRemove() adds the
|
|
16198
|
+
* button during resolution).
|
|
16199
|
+
*/
|
|
16200
|
+
rawItemTemplates = computed(() => {
|
|
16201
|
+
const arrayField = this.field();
|
|
16202
|
+
const definitions = arrayField.fields || [];
|
|
16203
|
+
return definitions.map((def) => {
|
|
16204
|
+
return (Array.isArray(def) ? def : [def]);
|
|
16205
|
+
});
|
|
16206
|
+
}, ...(ngDevMode ? [{ debugName: "rawItemTemplates" }] : /* istanbul ignore next */ []));
|
|
15398
16207
|
/**
|
|
15399
16208
|
* Gets the item templates (field definitions) for the array.
|
|
15400
16209
|
* Each element can be either:
|
|
@@ -15408,19 +16217,14 @@ class ArrayFieldComponent {
|
|
|
15408
16217
|
* Returns normalized templates where all items are arrays for consistent handling.
|
|
15409
16218
|
*/
|
|
15410
16219
|
itemTemplates = computed(() => {
|
|
15411
|
-
const
|
|
15412
|
-
const definitions = arrayField.fields || [];
|
|
16220
|
+
const raw = this.rawItemTemplates();
|
|
15413
16221
|
const removeButton = this.autoRemoveButton();
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
return
|
|
15417
|
-
|
|
15418
|
-
if (removeButton) {
|
|
15419
|
-
return [...normalized, removeButton];
|
|
15420
|
-
}
|
|
15421
|
-
return normalized;
|
|
16222
|
+
if (!removeButton)
|
|
16223
|
+
return raw;
|
|
16224
|
+
return raw.map((normalized) => {
|
|
16225
|
+
return [...normalized, removeButton];
|
|
15422
16226
|
});
|
|
15423
|
-
}, ...(ngDevMode ? [{ debugName: "itemTemplates" }] : []));
|
|
16227
|
+
}, ...(ngDevMode ? [{ debugName: "itemTemplates" }] : /* istanbul ignore next */ []));
|
|
15424
16228
|
arrayFieldTrees = computed(() => {
|
|
15425
16229
|
const arrayKey = this.field().key;
|
|
15426
16230
|
const parentForm = this.parentFieldSignalContext.form;
|
|
@@ -15438,12 +16242,14 @@ class ArrayFieldComponent {
|
|
|
15438
16242
|
items.push(itemFieldTree ?? null);
|
|
15439
16243
|
}
|
|
15440
16244
|
return items;
|
|
15441
|
-
}, ...(ngDevMode ? [{ debugName: "arrayFieldTrees" }] : []));
|
|
16245
|
+
}, ...(ngDevMode ? [{ debugName: "arrayFieldTrees" }] : /* istanbul ignore next */ []));
|
|
15442
16246
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15443
16247
|
// State Signals
|
|
15444
16248
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15445
|
-
resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : []));
|
|
15446
|
-
updateVersion = signal(0, ...(ngDevMode ? [{ debugName: "updateVersion" }] : []));
|
|
16249
|
+
resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : /* istanbul ignore next */ []));
|
|
16250
|
+
updateVersion = signal(0, ...(ngDevMode ? [{ debugName: "updateVersion" }] : /* istanbul ignore next */ []));
|
|
16251
|
+
pendingInitializationCycle = signal(null, ...(ngDevMode ? [{ debugName: "pendingInitializationCycle" }] : /* istanbul ignore next */ []));
|
|
16252
|
+
settledInitializationCycle = signal(null, ...(ngDevMode ? [{ debugName: "settledInitializationCycle" }] : /* istanbul ignore next */ []));
|
|
15447
16253
|
/**
|
|
15448
16254
|
* Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().
|
|
15449
16255
|
* Used by child linkedSignals to reactively track their position in the array.
|
|
@@ -15451,9 +16257,10 @@ class ArrayFieldComponent {
|
|
|
15451
16257
|
itemPositionMap = computed(() => {
|
|
15452
16258
|
const items = this.resolvedItemsSignal();
|
|
15453
16259
|
return new Map(items.map((item, idx) => [item.id, idx]));
|
|
15454
|
-
}, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : []));
|
|
16260
|
+
}, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : /* istanbul ignore next */ []));
|
|
15455
16261
|
/** Read-only view of resolved items for template consumption. */
|
|
15456
|
-
resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : []));
|
|
16262
|
+
resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
|
|
16263
|
+
allResolvedFieldsRenderReady = computed(() => this.resolvedItems().every((item) => item.fields.every((field) => field.renderReady())), ...(ngDevMode ? [{ debugName: "allResolvedFieldsRenderReady" }] : /* istanbul ignore next */ []));
|
|
15457
16264
|
/**
|
|
15458
16265
|
* Whether the array has reached its configured maxLength.
|
|
15459
16266
|
* Exposed as a public signal so add-button components can bind [disabled]="atMaxLength()".
|
|
@@ -15465,7 +16272,7 @@ class ArrayFieldComponent {
|
|
|
15465
16272
|
const arrayKey = this.field().key;
|
|
15466
16273
|
const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
|
|
15467
16274
|
return currentArray.length >= maxLength;
|
|
15468
|
-
}, ...(ngDevMode ? [{ debugName: "atMaxLength" }] : []));
|
|
16275
|
+
}, ...(ngDevMode ? [{ debugName: "atMaxLength" }] : /* istanbul ignore next */ []));
|
|
15469
16276
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15470
16277
|
// Auto-remove Cache
|
|
15471
16278
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -15492,6 +16299,12 @@ class ArrayFieldComponent {
|
|
|
15492
16299
|
explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
|
|
15493
16300
|
this.performDifferentialUpdate(fieldTrees);
|
|
15494
16301
|
});
|
|
16302
|
+
explicitEffect([this.pendingInitializationCycle, this.settledInitializationCycle, this.allResolvedFieldsRenderReady], ([pending, settled, allReady]) => {
|
|
16303
|
+
if (pending !== null && settled === pending && allReady) {
|
|
16304
|
+
emitComponentInitialized(this.eventBus, 'array', this.field().key, this.parentInjector);
|
|
16305
|
+
this.pendingInitializationCycle.set(null);
|
|
16306
|
+
}
|
|
16307
|
+
});
|
|
15495
16308
|
}
|
|
15496
16309
|
setupEventHandlers() {
|
|
15497
16310
|
observeArrayActions(this.eventBus, () => this.key())
|
|
@@ -15508,6 +16321,9 @@ class ArrayFieldComponent {
|
|
|
15508
16321
|
}
|
|
15509
16322
|
void this.handleAddFromEvent(action.template, action.index);
|
|
15510
16323
|
}
|
|
16324
|
+
else if (action.action === 'move') {
|
|
16325
|
+
this.moveItem(action.fromIndex, action.toIndex);
|
|
16326
|
+
}
|
|
15511
16327
|
else {
|
|
15512
16328
|
this.removeItem(action.index);
|
|
15513
16329
|
}
|
|
@@ -15522,9 +16338,13 @@ class ArrayFieldComponent {
|
|
|
15522
16338
|
* Supports both primitive (single FieldDef) and object (FieldDef[]) templates.
|
|
15523
16339
|
*/
|
|
15524
16340
|
async handleAddFromEvent(template, index) {
|
|
15525
|
-
// Normalize template to mutable array for consistent handling
|
|
16341
|
+
// Normalize template to mutable array for consistent handling.
|
|
16342
|
+
// A single FieldDef with valueHandling: 'flatten' (container, row) is an object item
|
|
16343
|
+
// whose children should be flattened — NOT a primitive item. Only true leaf fields
|
|
16344
|
+
// (input, checkbox, etc.) are primitive when passed as a single FieldDef.
|
|
15526
16345
|
const templates = Array.isArray(template) ? [...template] : [template];
|
|
15527
|
-
const
|
|
16346
|
+
const isSingleField = !Array.isArray(template);
|
|
16347
|
+
const isPrimitiveItem = isSingleField && getFieldValueHandling(templates[0].type, this.rawFieldRegistry()) !== 'flatten';
|
|
15528
16348
|
// Track primitive field key for full-API arrays that start empty.
|
|
15529
16349
|
// Simplified arrays already have this info from normalization metadata.
|
|
15530
16350
|
if (isPrimitiveItem && templates[0].key && !this.primitiveFieldKey()) {
|
|
@@ -15552,14 +16372,14 @@ class ArrayFieldComponent {
|
|
|
15552
16372
|
for (const templateField of templates) {
|
|
15553
16373
|
const rawValue = getFieldDefaultValue(templateField, this.rawFieldRegistry());
|
|
15554
16374
|
const valueHandling = getFieldValueHandling(templateField.type, this.rawFieldRegistry());
|
|
15555
|
-
const isContainer = templateField.type === 'group' || templateField.type === 'row';
|
|
16375
|
+
const isContainer = templateField.type === 'group' || templateField.type === 'row' || templateField.type === 'container';
|
|
15556
16376
|
if (isContainer) {
|
|
15557
16377
|
if (isGroupField(templateField)) {
|
|
15558
16378
|
// Groups wrap their fields under the group key
|
|
15559
16379
|
value = { ...value, [templateField.key]: rawValue };
|
|
15560
16380
|
}
|
|
15561
16381
|
else {
|
|
15562
|
-
// Rows flatten their fields directly
|
|
16382
|
+
// Rows and containers flatten their fields directly
|
|
15563
16383
|
value = { ...value, ...rawValue };
|
|
15564
16384
|
}
|
|
15565
16385
|
}
|
|
@@ -15624,26 +16444,31 @@ class ArrayFieldComponent {
|
|
|
15624
16444
|
// Clean up template registry when all items are removed
|
|
15625
16445
|
this.templateRegistry.clear();
|
|
15626
16446
|
this.resolvedItemsSignal.set([]);
|
|
16447
|
+
if (resolvedItems.length === 0 && this.pendingInitializationCycle() === null && this.settledInitializationCycle() === null) {
|
|
16448
|
+
this.pendingInitializationCycle.set(currentVersion);
|
|
16449
|
+
this.settledInitializationCycle.set(currentVersion);
|
|
16450
|
+
}
|
|
15627
16451
|
break;
|
|
15628
16452
|
case 'initial':
|
|
16453
|
+
this.pendingInitializationCycle.set(currentVersion);
|
|
16454
|
+
this.settledInitializationCycle.set(null);
|
|
15629
16455
|
void this.resolveAllItems(fieldTrees, currentVersion);
|
|
15630
16456
|
break;
|
|
15631
16457
|
case 'append':
|
|
15632
16458
|
void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, currentVersion);
|
|
15633
16459
|
break;
|
|
15634
16460
|
case 'recreate': {
|
|
15635
|
-
//
|
|
15636
|
-
//
|
|
15637
|
-
|
|
15638
|
-
const positionalTemplates = resolvedItems.map((item
|
|
15639
|
-
//
|
|
15640
|
-
|
|
15641
|
-
return undefined; // Will use itemTemplates[idx] during resolve
|
|
15642
|
-
}
|
|
15643
|
-
// For dynamically added items, look up their stored template
|
|
16461
|
+
// Capture templates by item ID so each item is recreated with its original template,
|
|
16462
|
+
// even after move operations that change positions. Registry entries (from dynamic adds
|
|
16463
|
+
// or moves) take priority over positional itemTemplates lookup.
|
|
16464
|
+
const positionalTemplates = resolvedItems.map((item) => {
|
|
16465
|
+
// Registry entries (from dynamic adds or moves) take priority;
|
|
16466
|
+
// unmoved initial items return undefined → falls through to itemTemplates[idx] during resolve.
|
|
15644
16467
|
return this.templateRegistry.get(item.id);
|
|
15645
16468
|
});
|
|
15646
16469
|
this.resolvedItemsSignal.set([]);
|
|
16470
|
+
this.pendingInitializationCycle.set(currentVersion);
|
|
16471
|
+
this.settledInitializationCycle.set(null);
|
|
15647
16472
|
void this.resolveAllItems(fieldTrees, currentVersion, positionalTemplates);
|
|
15648
16473
|
break;
|
|
15649
16474
|
}
|
|
@@ -15660,7 +16485,7 @@ class ArrayFieldComponent {
|
|
|
15660
16485
|
return of(undefined);
|
|
15661
16486
|
})));
|
|
15662
16487
|
try {
|
|
15663
|
-
const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map
|
|
16488
|
+
const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
|
|
15664
16489
|
if (updateId === this.updateVersion()) {
|
|
15665
16490
|
// Update template registry with new item IDs for dynamically added items
|
|
15666
16491
|
if (positionalTemplates) {
|
|
@@ -15679,7 +16504,9 @@ class ArrayFieldComponent {
|
|
|
15679
16504
|
});
|
|
15680
16505
|
}
|
|
15681
16506
|
this.resolvedItemsSignal.set(items);
|
|
15682
|
-
|
|
16507
|
+
if (this.pendingInitializationCycle() === updateId) {
|
|
16508
|
+
this.settledInitializationCycle.set(updateId);
|
|
16509
|
+
}
|
|
15683
16510
|
}
|
|
15684
16511
|
}
|
|
15685
16512
|
catch (err) {
|
|
@@ -15700,7 +16527,7 @@ class ArrayFieldComponent {
|
|
|
15700
16527
|
}));
|
|
15701
16528
|
});
|
|
15702
16529
|
try {
|
|
15703
|
-
const newItems = await firstValueFrom(forkJoin(safeItemObservables).pipe(map
|
|
16530
|
+
const newItems = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
|
|
15704
16531
|
if (updateId === this.updateVersion()) {
|
|
15705
16532
|
this.resolvedItemsSignal.update((current) => [...current, ...newItems]);
|
|
15706
16533
|
}
|
|
@@ -15781,6 +16608,61 @@ class ArrayFieldComponent {
|
|
|
15781
16608
|
this.autoRemoveCache.set(templates, cached);
|
|
15782
16609
|
return cached;
|
|
15783
16610
|
}
|
|
16611
|
+
/**
|
|
16612
|
+
* Handles move operations — reorders an existing item without destroying it.
|
|
16613
|
+
* Updates resolvedItems and form value atomically. Since the array length
|
|
16614
|
+
* doesn't change, `determineDifferentialOperation` returns 'none' and no
|
|
16615
|
+
* recreate is triggered. The `@for` loop tracks by `item.id`, so Angular
|
|
16616
|
+
* moves the DOM node instead of destroying/recreating. The `itemPositionMap`
|
|
16617
|
+
* computed auto-recomputes, propagating new indices to child linkedSignals.
|
|
16618
|
+
*/
|
|
16619
|
+
moveItem(fromIndex, toIndex) {
|
|
16620
|
+
const arrayKey = this.field().key;
|
|
16621
|
+
const parentForm = this.parentFieldSignalContext.form;
|
|
16622
|
+
const currentValue = parentForm().value();
|
|
16623
|
+
const currentArray = getArrayValue(currentValue, arrayKey);
|
|
16624
|
+
const length = currentArray.length;
|
|
16625
|
+
if (fromIndex < 0 || fromIndex >= length) {
|
|
16626
|
+
this.logger.warn(`moveArrayItem fromIndex ${fromIndex} is out of bounds for array '${arrayKey}' with length ${length}. Operation skipped.`);
|
|
16627
|
+
return;
|
|
16628
|
+
}
|
|
16629
|
+
if (toIndex < 0 || toIndex >= length) {
|
|
16630
|
+
this.logger.warn(`moveArrayItem toIndex ${toIndex} is out of bounds for array '${arrayKey}' with length ${length}. Operation skipped.`);
|
|
16631
|
+
return;
|
|
16632
|
+
}
|
|
16633
|
+
if (fromIndex === toIndex)
|
|
16634
|
+
return;
|
|
16635
|
+
// Register raw (pre-auto-remove) templates for all items whose position will change.
|
|
16636
|
+
// Initial items use itemTemplates[currentIndex] during recreate, but after
|
|
16637
|
+
// a move their position no longer matches their original template. Stashing
|
|
16638
|
+
// the template by item ID lets the recreate path resolve the correct one.
|
|
16639
|
+
// We use rawItemTemplates (without auto-remove button) because the recreate path
|
|
16640
|
+
// calls withAutoRemove() during resolution — storing the synthesized version would
|
|
16641
|
+
// cause duplicate remove buttons.
|
|
16642
|
+
const rawTemplates = this.rawItemTemplates();
|
|
16643
|
+
const currentItems = this.resolvedItemsSignal();
|
|
16644
|
+
const lo = Math.min(fromIndex, toIndex);
|
|
16645
|
+
const hi = Math.max(fromIndex, toIndex);
|
|
16646
|
+
for (let i = lo; i <= hi; i++) {
|
|
16647
|
+
const item = currentItems[i];
|
|
16648
|
+
if (item && !this.templateRegistry.has(item.id) && i < rawTemplates.length) {
|
|
16649
|
+
this.templateRegistry.set(item.id, rawTemplates[i]);
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
// Reorder resolvedItems — splice preserves object identity (no destroy/recreate)
|
|
16653
|
+
this.resolvedItemsSignal.update((current) => {
|
|
16654
|
+
const newItems = [...current];
|
|
16655
|
+
const [moved] = newItems.splice(fromIndex, 1);
|
|
16656
|
+
newItems.splice(toIndex, 0, moved);
|
|
16657
|
+
return newItems;
|
|
16658
|
+
});
|
|
16659
|
+
// Reorder form value array the same way
|
|
16660
|
+
const newArray = [...currentArray];
|
|
16661
|
+
const [movedValue] = newArray.splice(fromIndex, 1);
|
|
16662
|
+
newArray.splice(toIndex, 0, movedValue);
|
|
16663
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16664
|
+
parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
|
|
16665
|
+
}
|
|
15784
16666
|
/**
|
|
15785
16667
|
* Handles remove operations from events (pop, shift, removeAt).
|
|
15786
16668
|
* Updates resolvedItems FIRST, then form value - this ensures differential
|
|
@@ -15831,8 +16713,8 @@ class ArrayFieldComponent {
|
|
|
15831
16713
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15832
16714
|
parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
|
|
15833
16715
|
}
|
|
15834
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
15835
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
16716
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16717
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, providers: [
|
|
15836
16718
|
// Each array gets its own template registry to track templates used for dynamically added items
|
|
15837
16719
|
{ provide: ARRAY_TEMPLATE_REGISTRY, useFactory: () => new Map() },
|
|
15838
16720
|
// Each array gets its own ID generator for SSR hydration compatibility
|
|
@@ -15847,15 +16729,15 @@ class ArrayFieldComponent {
|
|
|
15847
16729
|
[attr.data-array-item-index]="i"
|
|
15848
16730
|
>
|
|
15849
16731
|
@for (field of item.fields; track $index) {
|
|
15850
|
-
<ng-container *
|
|
16732
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15851
16733
|
}
|
|
15852
16734
|
</div>
|
|
15853
16735
|
}
|
|
15854
|
-
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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-array-item-gap, var(--df-grid-row-gap, .5rem));width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}.df-array-item{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-array-item>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type:
|
|
16736
|
+
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);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);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);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.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),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);--df-grid-row-gap: var(--df-grid-row-gap-md)}.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);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.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);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.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-array-item-gap, var(--df-grid-row-gap, .5rem));width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}.df-array-item{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-array-item>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15855
16737
|
}
|
|
15856
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
16738
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayFieldComponent, decorators: [{
|
|
15857
16739
|
type: Component,
|
|
15858
|
-
args: [{ selector: 'array-field', imports: [
|
|
16740
|
+
args: [{ selector: 'array-field', imports: [DfFieldOutlet], template: `
|
|
15859
16741
|
@for (item of resolvedItems(); track item.id; let i = $index) {
|
|
15860
16742
|
<div
|
|
15861
16743
|
class="df-array-item"
|
|
@@ -15865,7 +16747,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
15865
16747
|
[attr.data-array-item-index]="i"
|
|
15866
16748
|
>
|
|
15867
16749
|
@for (field of item.fields; track $index) {
|
|
15868
|
-
<ng-container *
|
|
16750
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
15869
16751
|
}
|
|
15870
16752
|
</div>
|
|
15871
16753
|
}
|
|
@@ -15890,12 +16772,95 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
|
|
|
15890
16772
|
});
|
|
15891
16773
|
|
|
15892
16774
|
/**
|
|
15893
|
-
*
|
|
16775
|
+
* Layout container that wraps child fields with UI chrome.
|
|
16776
|
+
*
|
|
16777
|
+
* Resolves children like a row, then chains wrapper components around them
|
|
16778
|
+
* using imperative `ViewContainerRef.createComponent()` — each wrapper's
|
|
16779
|
+
* `#fieldComponent` slot hosts the next wrapper or the children template.
|
|
16780
|
+
*
|
|
16781
|
+
* Does not create a new form context - fields share the parent's context.
|
|
16782
|
+
* Field values are flattened into the parent form (no nesting under container key).
|
|
16783
|
+
* Purely a visual/layout container with no impact on form structure.
|
|
15894
16784
|
*/
|
|
15895
|
-
|
|
15896
|
-
|
|
15897
|
-
|
|
16785
|
+
class ContainerFieldComponent {
|
|
16786
|
+
destroyRef = inject(DestroyRef);
|
|
16787
|
+
fieldRegistry = injectFieldRegistry();
|
|
16788
|
+
injector = inject(Injector);
|
|
16789
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
16790
|
+
eventBus = inject(EventBus);
|
|
16791
|
+
logger = inject(DynamicFormLogger);
|
|
16792
|
+
wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);
|
|
16793
|
+
defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });
|
|
16794
|
+
childrenTpl = viewChild.required('childrenTpl', { read: TemplateRef });
|
|
16795
|
+
wrapperContainer = viewChild.required('wrapperContainer', { read: ViewContainerRef });
|
|
16796
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
16797
|
+
key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
16798
|
+
className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
|
|
16799
|
+
hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
16800
|
+
hostClasses = computed(() => computeContainerHostClasses('container', this.className()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
|
|
16801
|
+
disabled = computed(() => this.field().disabled || false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
16802
|
+
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
|
|
16803
|
+
fieldsSource = computed(() => (this.field().fields || []), ...(ngDevMode ? [{ debugName: "fieldsSource" }] : /* istanbul ignore next */ []));
|
|
16804
|
+
resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
|
|
16805
|
+
loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
|
|
16806
|
+
registry: this.rawFieldRegistry(),
|
|
16807
|
+
injector: this.injector,
|
|
16808
|
+
destroyRef: this.destroyRef,
|
|
16809
|
+
onError: (fieldDef, error) => {
|
|
16810
|
+
const fieldKey = fieldDef.key || '<no key>';
|
|
16811
|
+
const containerKey = this.field().key || '<no key>';
|
|
16812
|
+
this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
|
|
16813
|
+
`within container '${containerKey}'. Ensure the field type is registered in your field registry.`, error);
|
|
16814
|
+
},
|
|
16815
|
+
})), { initialValue: [], injector: this.injector });
|
|
16816
|
+
wrappers = computed(() => resolveWrappers(this.field(), this.defaultWrappersSignal?.(), this.wrapperAutoAssociations), { ...(ngDevMode ? { debugName: "wrappers" } : /* istanbul ignore next */ {}), equal: isSameWrapperChain });
|
|
16817
|
+
constructor() {
|
|
16818
|
+
// `row` is a virtual field type backed by ContainerFieldComponent — at
|
|
16819
|
+
// runtime the spread in rowFieldMapper preserves `type: 'row'` on the
|
|
16820
|
+
// field input, even though the declared type is `ContainerField`. Widen
|
|
16821
|
+
// here so `(events)` consumers filtering by `componentType === 'row'`
|
|
16822
|
+
// keep receiving emissions.
|
|
16823
|
+
setupContainerInitEffect(this.resolvedFields, this.eventBus, () => (this.field().type === 'row' ? 'row' : 'container'), () => this.field().key, this.injector);
|
|
16824
|
+
createWrapperChainController({
|
|
16825
|
+
vcr: this.wrapperContainer,
|
|
16826
|
+
wrappers: this.wrappers,
|
|
16827
|
+
renderInnermost: (slot) => slot.createEmbeddedView(this.childrenTpl()),
|
|
16828
|
+
});
|
|
16829
|
+
}
|
|
16830
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ContainerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16831
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ContainerFieldComponent, isStandalone: true, selector: "div[container-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.disabled": "disabled()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, viewQueries: [{ propertyName: "childrenTpl", first: true, predicate: ["childrenTpl"], descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "wrapperContainer", first: true, predicate: ["wrapperContainer"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: `
|
|
16832
|
+
<ng-template #childrenTpl>
|
|
16833
|
+
@for (field of resolvedFields(); track field.key) {
|
|
16834
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
16835
|
+
}
|
|
16836
|
+
</ng-template>
|
|
16837
|
+
<ng-container #wrapperContainer></ng-container>
|
|
16838
|
+
`, isInline: true, styles: [":host{display:block;width:100%}:host.df-container-hidden{display:none}\n"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15898
16839
|
}
|
|
16840
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ContainerFieldComponent, decorators: [{
|
|
16841
|
+
type: Component,
|
|
16842
|
+
args: [{ selector: 'div[container-field]', imports: [DfFieldOutlet], template: `
|
|
16843
|
+
<ng-template #childrenTpl>
|
|
16844
|
+
@for (field of resolvedFields(); track field.key) {
|
|
16845
|
+
<ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
|
|
16846
|
+
}
|
|
16847
|
+
</ng-template>
|
|
16848
|
+
<ng-container #wrapperContainer></ng-container>
|
|
16849
|
+
`, host: {
|
|
16850
|
+
'[class]': 'hostClasses()',
|
|
16851
|
+
'[class.disabled]': 'disabled()',
|
|
16852
|
+
'[class.df-container-hidden]': 'hidden()',
|
|
16853
|
+
'[attr.aria-hidden]': 'hidden() || null',
|
|
16854
|
+
'[id]': '`${key()}`',
|
|
16855
|
+
'[attr.data-testid]': 'key()',
|
|
16856
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%}:host.df-container-hidden{display:none}\n"] }]
|
|
16857
|
+
}], ctorParameters: () => [], propDecorators: { childrenTpl: [{ type: i0.ViewChild, args: ['childrenTpl', { ...{ read: TemplateRef }, isSignal: true }] }], wrapperContainer: [{ type: i0.ViewChild, args: ['wrapperContainer', { ...{ read: ViewContainerRef }, isSignal: true }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
|
|
16858
|
+
|
|
16859
|
+
var containerField_component = /*#__PURE__*/Object.freeze({
|
|
16860
|
+
__proto__: null,
|
|
16861
|
+
ContainerFieldComponent: ContainerFieldComponent,
|
|
16862
|
+
default: ContainerFieldComponent
|
|
16863
|
+
});
|
|
15899
16864
|
|
|
15900
16865
|
/**
|
|
15901
16866
|
* Interpolates {{param}} placeholders in message with error values
|
|
@@ -16028,5 +16993,5 @@ function extractErrorParams(error) {
|
|
|
16028
16993
|
* Generated bundle index. Do not edit.
|
|
16029
16994
|
*/
|
|
16030
16995
|
|
|
16031
|
-
export {
|
|
16032
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
16996
|
+
export { evaluateNonFieldDisabled as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FIELD_REGISTRY as F, GroupFieldComponent as G, PreviousPageEvent as H, INITIALIZATION_TIMEOUT_MS as I, RootFormRegistryService as J, SubmitEvent as K, applyMetaToElement as L, MoveArrayItemEvent as M, NextPageEvent as N, applyValidator as O, PageChangeEvent as P, applyValidators as Q, RemoveAtIndexEvent as R, ShiftArrayItemEvent as S, arrayEvent as T, arrayFieldMapper as U, baseFieldMapper as V, WRAPPER_REGISTRY as W, buildBaseInputs as X, containerFieldMapper as Y, createField as Z, createWrappers as _, createFieldResolutionPipe as a, evaluateNonFieldHidden as a0, field as a1, formConfig as a2, getArrayLength as a3, groupFieldMapper as a4, hasFormValue as a5, interpolateParams as a6, isArrayField as a7, isCheckedField as a8, isContainerField as a9, withPreviousValue as aA, withValueExclusionDefaults as aB, wrapperProps as aC, isContainerTypedField as aa, isDisplayOnlyField as ab, isEqual as ac, isFormStateCondition as ad, isGroupField as ae, isLeafField as af, isPageField as ag, isRowField as ah, isSimplifiedArrayField as ai, isValueBearingField as aj, isValueField as ak, isWrapperTypeDefinition as al, isWrappersBundle as am, omit as an, pageFieldMapper as ao, provideDynamicForm as ap, resolveNextButtonDisabled as aq, resolveNonFieldDisabled as ar, resolveNonFieldHidden as as, resolveSubmitButtonDisabled as at, resolveTokens as au, rowFieldMapper as av, textFieldMapper as aw, toReadonlyFieldTree as ax, withEventFormValue as ay, withLoggerConfig as az, DfFieldOutlet as b, computeContainerHostClasses as c, derivedFromDeferred as d, DynamicTextPipe as e, dynamicTextToObservable as f, AppendArrayItemEvent as g, ArrayFieldComponent as h, injectFieldRegistry as i, ContainerFieldComponent as j, DEFAULT_PROPS as k, DEFAULT_VALIDATION_MESSAGES as l, DEFAULT_WRAPPERS as m, DynamicForm as n, DynamicFormError as o, EventDispatcher as p, FIELD_SIGNAL_CONTEXT as q, FORM_OPTIONS as r, setupContainerInitEffect as s, FormClearEvent as t, FormResetEvent as u, validatePageNesting as v, InsertArrayItemEvent as w, NoopLogger as x, PopArrayItemEvent as y, PrependArrayItemEvent as z };
|
|
16997
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-D276M2vh.mjs.map
|