ng-form-foundry 0.0.1 → 0.2.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.
Files changed (40) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +72 -38
  3. package/fesm2022/ng-form-foundry.mjs +822 -0
  4. package/fesm2022/ng-form-foundry.mjs.map +1 -0
  5. package/index.d.ts +307 -0
  6. package/package.json +42 -5
  7. package/ng-package.json +0 -7
  8. package/src/lib/core/dynamic-recursive-forms-builder.ts +0 -129
  9. package/src/lib/core/utils.ts +0 -33
  10. package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.html +0 -31
  11. package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.scss +0 -11
  12. package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.spec.ts +0 -22
  13. package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.ts +0 -39
  14. package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.html +0 -168
  15. package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.scss +0 -128
  16. package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.spec.ts +0 -23
  17. package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.ts +0 -71
  18. package/src/lib/dynamic-recursive-form/dynamic-recursive-form.stories.ts +0 -36
  19. package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.html +0 -8
  20. package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.scss +0 -9
  21. package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.spec.ts +0 -22
  22. package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.ts +0 -21
  23. package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.html +0 -31
  24. package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.scss +0 -57
  25. package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.spec.ts +0 -23
  26. package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.ts +0 -52
  27. package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.html +0 -17
  28. package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.scss +0 -11
  29. package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.spec.ts +0 -22
  30. package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.ts +0 -41
  31. package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.html +0 -24
  32. package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.scss +0 -48
  33. package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.spec.ts +0 -22
  34. package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.ts +0 -101
  35. package/src/lib/types/dynamic-recursive.types.ts +0 -97
  36. package/src/lib/types/examples/complex-oai.ts +0 -938
  37. package/src/public-api.ts +0 -12
  38. package/tsconfig.lib.json +0 -18
  39. package/tsconfig.lib.prod.json +0 -11
  40. package/tsconfig.spec.json +0 -14
@@ -0,0 +1,822 @@
1
+ import * as i1 from '@angular/forms';
2
+ import { Validators, FormControl, FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
3
+ import * as i0 from '@angular/core';
4
+ import { Input, Component, EventEmitter, Output, inject, ChangeDetectorRef, forwardRef, ViewChildren, HostBinding } from '@angular/core';
5
+ import * as i2 from '@angular/material/form-field';
6
+ import { MatFormFieldModule } from '@angular/material/form-field';
7
+ import * as i3$1 from '@angular/material/input';
8
+ import { MatInputModule, MatPrefix } from '@angular/material/input';
9
+ import * as i4 from '@angular/material/checkbox';
10
+ import { MatCheckboxModule } from '@angular/material/checkbox';
11
+ import * as i3 from '@angular/material/select';
12
+ import { MatSelectModule } from '@angular/material/select';
13
+ import * as i1$1 from '@angular/material/icon';
14
+ import { MatIconModule } from '@angular/material/icon';
15
+ import * as i3$2 from '@angular/material/button';
16
+ import { MatButtonModule } from '@angular/material/button';
17
+ import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
18
+ import { MatDialog } from '@angular/material/dialog';
19
+ import * as i2$1 from '@angular/material/expansion';
20
+ import { MatExpansionModule } from '@angular/material/expansion';
21
+ import * as i5 from '@angular/material/card';
22
+ import { MatCardModule } from '@angular/material/card';
23
+ import { NgTemplateOutlet } from '@angular/common';
24
+
25
+ /** The control name that records which case of a {@link NodeChoice} is active. */
26
+ const CASE_KEY = '__case';
27
+
28
+ // --- type guards
29
+ function isLeaf(node) {
30
+ return node.kind === 'leaf';
31
+ }
32
+ function isLeafList(node) {
33
+ return node.kind === 'leafList';
34
+ }
35
+ function isNodeGroup(node) {
36
+ return node.kind === 'nodeGroup';
37
+ }
38
+ function isNodeGroupList(node) {
39
+ return node.kind === 'nodeGroupList';
40
+ }
41
+ function isChoice(node) {
42
+ return node.kind === 'choice';
43
+ }
44
+ function enumValidator(choices) {
45
+ const set = new Set(choices);
46
+ return (ctrl) => ctrl.value == null || set.has(ctrl.value) ? null : { enum: true };
47
+ }
48
+ function buildLeafControl(leaf, initial) {
49
+ const validators = [];
50
+ if ('required' in leaf && leaf.required)
51
+ validators.push(Validators.required);
52
+ if ('type' in leaf && leaf.type === 'enum') {
53
+ const choices = leaf.enum;
54
+ validators.push(enumValidator(choices));
55
+ }
56
+ const defaultValue = initial ?? ('default' in leaf ? leaf.default : undefined) ?? null;
57
+ return new FormControl(defaultValue, {
58
+ nonNullable: true,
59
+ validators,
60
+ });
61
+ }
62
+ function buildLeafListControl(list, initial) {
63
+ const values = (Array.isArray(initial) ? initial : undefined) ??
64
+ list.default ?? [null];
65
+ return new FormArray(values.map((v) => new FormControl(v)));
66
+ }
67
+ function buildNodeGroupControl(group, initial) {
68
+ const controls = {};
69
+ for (const key in group.children) {
70
+ const child = group.children[key];
71
+ // Forward only this child's slice of the initial data, keyed by the child's
72
+ // record key. Passing the whole `initial` object seeds every leaf with the
73
+ // parent record and prevents list builders from sizing to the real data.
74
+ controls[key] = buildControl(child, initial?.[key]);
75
+ }
76
+ return new FormGroup(controls);
77
+ }
78
+ function buildNodeGroupListControl(list, initial = null) {
79
+ // `initial` is the runtime data array — one group per element. Fall back to a
80
+ // single empty group only when no initial data is supplied.
81
+ const values = Array.isArray(initial) ? initial : [null];
82
+ return new FormArray(values.map((v) => buildNodeGroupControl(list.type, v)));
83
+ }
84
+ /**
85
+ * Build the `AbstractControl` for a single schema node.
86
+ *
87
+ * Dispatches on `node.kind`: a `leaf` becomes a `FormControl`, a `leafList` a
88
+ * `FormArray` of controls, a `nodeGroup` a nested `FormGroup`, and a
89
+ * `nodeGroupList` a `FormArray` of groups. `initial` is the runtime value for
90
+ * this node — a scalar for a leaf, an array for a list, an object for a group —
91
+ * and seeds the control's value (falling back to the node's `default`).
92
+ *
93
+ * Most callers use {@link buildFormFromSchema}; this is exposed for building a
94
+ * control from a single non-root node.
95
+ */
96
+ /**
97
+ * Build the FormGroup for a choice: a `__case` control holding the active case
98
+ * name plus that case's field controls. Only the active case's fields are built,
99
+ * matching the inline YANG encoding; switching the case swaps them.
100
+ */
101
+ function buildChoiceControl(choice, initial) {
102
+ const active = initial?.[CASE_KEY] ?? choice.default;
103
+ const controls = { [CASE_KEY]: new FormControl(active ?? null) };
104
+ if (active && choice.cases[active]) {
105
+ const caseChildren = choice.cases[active];
106
+ for (const key in caseChildren) {
107
+ controls[key] = buildControl(caseChildren[key], initial?.[key]);
108
+ }
109
+ }
110
+ return new FormGroup(controls);
111
+ }
112
+ function buildControl(node, initial) {
113
+ if (isLeaf(node)) {
114
+ return buildLeafControl(node, initial);
115
+ }
116
+ if (isChoice(node)) {
117
+ return buildChoiceControl(node, initial ? initial : null);
118
+ }
119
+ if (isLeafList(node)) {
120
+ return buildLeafListControl(node, initial !== null
121
+ ? initial
122
+ : initial);
123
+ }
124
+ if (isNodeGroup(node)) {
125
+ return buildNodeGroupControl(node, initial ? initial : null);
126
+ }
127
+ if (isNodeGroupList(node)) {
128
+ return buildNodeGroupListControl(node, initial ? initial : null);
129
+ }
130
+ return new FormControl(initial ?? '');
131
+ }
132
+ /**
133
+ * Build a typed `FormGroup` from a root `NodeGroup` schema.
134
+ *
135
+ * The returned group's control structure, keys, and value types are inferred
136
+ * from the schema literal — a `leaf` of `type: 'number'` yields a
137
+ * `FormControl<number>`, a `nodeGroup` a nested `FormGroup`, and so on. `initial`
138
+ * is an optional value object keyed by the schema's `children` keys; each child
139
+ * control is seeded from its matching slice (falling back to the node `default`).
140
+ *
141
+ * Inference only holds when `schema`'s literal type is preserved. Author schemas
142
+ * with {@link defineSchema} or a `satisfies NodeGroup` annotation — never
143
+ * `const schema: NodeGroup = ...`, which widens `children` and erases the field
144
+ * names and value types.
145
+ */
146
+ /**
147
+ * Remove presence groups that have no initial value so they are absent from the
148
+ * built form (and from `form.value`) until the user toggles them on. Runs on the
149
+ * fully-built, attached tree. `removeControl` is used rather than `disable`
150
+ * because a disabled group is still included in `FormGroup.value`.
151
+ */
152
+ function applyPresence(group, schema, initial) {
153
+ for (const key in schema.children) {
154
+ const child = schema.children[key];
155
+ if (child.kind !== 'nodeGroup')
156
+ continue;
157
+ const childInitial = initial?.[key];
158
+ if (child.presence && childInitial == null) {
159
+ group.removeControl(key);
160
+ }
161
+ else if (group.get(key) instanceof FormGroup) {
162
+ applyPresence(group.get(key), child, childInitial);
163
+ }
164
+ }
165
+ }
166
+ function buildFormFromSchema(schema, initial = null) {
167
+ const group = buildNodeGroupControl(schema, initial);
168
+ applyPresence(group, schema, initial);
169
+ return group;
170
+ }
171
+ /**
172
+ * Capture a schema literal with its exact type while checking it against
173
+ * `NodeGroup`.
174
+ *
175
+ * This is an identity function whose only job is the `const` type parameter,
176
+ * which keeps the narrow literal type of `schema` (field names, each node's
177
+ * `type`) instead of widening it to `NodeGroup`. Assigning a schema to a
178
+ * `: NodeGroup`-annotated constant widens `children` to
179
+ * `Record<string, NodeType>` and erases that information, so
180
+ * {@link buildFormFromSchema} can no longer infer a typed `FormGroup`. Passing
181
+ * the schema through `defineSchema` (or annotating it `satisfies NodeGroup`)
182
+ * preserves the schema-to-`FormGroup` inference.
183
+ */
184
+ function defineSchema(schema) {
185
+ return schema;
186
+ }
187
+
188
+ class LeafEnumRendererComponent {
189
+ leafEnum;
190
+ initialValue;
191
+ control = new FormControl();
192
+ removable = false;
193
+ remove;
194
+ editable = true;
195
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafEnumRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
196
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafEnumRendererComponent, isStandalone: true, selector: "nff-leaf-enum-renderer", inputs: { leafEnum: "leafEnum", initialValue: "initialValue", control: "control", removable: "removable", remove: "remove", editable: "editable" }, ngImport: i0, template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n <mat-select [formControl]=\"control\" value=\"{{ control.value }}\">\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n", styles: [":host{flex:1;min-width:0;width:100%}mat-form-field{width:100%}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] });
197
+ }
198
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafEnumRendererComponent, decorators: [{
199
+ type: Component,
200
+ args: [{ selector: 'nff-leaf-enum-renderer', standalone: true, imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule], template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n <mat-select [formControl]=\"control\" value=\"{{ control.value }}\">\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n", styles: [":host{flex:1;min-width:0;width:100%}mat-form-field{width:100%}\n"] }]
201
+ }], propDecorators: { leafEnum: [{
202
+ type: Input
203
+ }], initialValue: [{
204
+ type: Input
205
+ }], control: [{
206
+ type: Input
207
+ }], removable: [{
208
+ type: Input
209
+ }], remove: [{
210
+ type: Input
211
+ }], editable: [{
212
+ type: Input
213
+ }] } });
214
+
215
+ class LeafRendererComponent {
216
+ leaf_;
217
+ control = new FormControl();
218
+ initialValue;
219
+ removable = false;
220
+ editable = true;
221
+ remove = new EventEmitter();
222
+ ngOnInit() {
223
+ if (this.initialValue) {
224
+ this.control.patchValue(this.initialValue);
225
+ }
226
+ }
227
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
228
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafRendererComponent, isStandalone: true, selector: "nff-leaf-renderer", inputs: { leaf_: "leaf_", control: "control", initialValue: "initialValue", removable: "removable", editable: "editable" }, outputs: { remove: "remove" }, ngImport: i0, template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!editable\" matInput type=\"text\" [formControl]=\"control\">\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!editable\" matInput type=\"number\" [formControl]=\"control\">\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"editable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%}mat-form-field{flex:1;min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: LeafEnumRendererComponent, selector: "nff-leaf-enum-renderer", inputs: ["leafEnum", "initialValue", "control", "removable", "remove", "editable"] }] });
229
+ }
230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
231
+ type: Component,
232
+ args: [{ selector: 'nff-leaf-renderer', standalone: true, imports: [
233
+ MatFormFieldModule,
234
+ ReactiveFormsModule,
235
+ MatInputModule,
236
+ MatCheckboxModule,
237
+ MatSelectModule,
238
+ MatIconModule,
239
+ MatButtonModule,
240
+ LeafEnumRendererComponent,
241
+ ], template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!editable\" matInput type=\"text\" [formControl]=\"control\">\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!editable\" matInput type=\"number\" [formControl]=\"control\">\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"editable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%}mat-form-field{flex:1;min-width:0}\n"] }]
242
+ }], propDecorators: { leaf_: [{
243
+ type: Input
244
+ }], control: [{
245
+ type: Input
246
+ }], initialValue: [{
247
+ type: Input
248
+ }], removable: [{
249
+ type: Input
250
+ }], editable: [{
251
+ type: Input
252
+ }], remove: [{
253
+ type: Output
254
+ }] } });
255
+
256
+ class NodeGroupListRendererComponent {
257
+ nodeGroupList;
258
+ initialValue;
259
+ formArray = new FormArray([]);
260
+ editable = true;
261
+ minItems = 1;
262
+ maxItems = 1;
263
+ message = new EventEmitter();
264
+ // forwardRef: DynamicRecursiveFormComponent and this component import each
265
+ // other, so the class reference is undefined when this query is evaluated at
266
+ // decoration time. forwardRef defers the lookup and keeps the selector valid.
267
+ items;
268
+ cdr = inject(ChangeDetectorRef);
269
+ ngOnInit() {
270
+ if (this.initialValue) {
271
+ this.formArray.patchValue(this.initialValue);
272
+ }
273
+ if (this.nodeGroupList.maxItems) {
274
+ this.maxItems = this.nodeGroupList.maxItems;
275
+ }
276
+ if (this.nodeGroupList.minItems) {
277
+ this.minItems = this.nodeGroupList.minItems;
278
+ }
279
+ }
280
+ ngAfterViewInit() {
281
+ this.items.changes.subscribe(() => {
282
+ this.setLastEditable();
283
+ });
284
+ }
285
+ removeItem($index) {
286
+ if (this.formArray.length <= this.minItems) {
287
+ this.message.emit({
288
+ message: `You cannot remove the last ${this.nodeGroupList.type.name} configuration!`,
289
+ type: 'error',
290
+ });
291
+ return;
292
+ }
293
+ this.formArray.removeAt($index);
294
+ }
295
+ addItem = (index) => {
296
+ this.formArray.push(buildFormFromSchema(this.nodeGroupList.type, null));
297
+ };
298
+ setLastEditable() {
299
+ const lastItem = this.items.last;
300
+ if (lastItem) {
301
+ lastItem.editable = true;
302
+ }
303
+ this.cdr.detectChanges();
304
+ }
305
+ asFormGroup(group) {
306
+ return group;
307
+ }
308
+ getTitle($index) {
309
+ let title = `${this.nodeGroupList.type.label ?? this.nodeGroupList.type.name}`;
310
+ if (this.formArray.length > 1) {
311
+ return `${title} #${$index + 1}`;
312
+ }
313
+ else {
314
+ return title;
315
+ }
316
+ }
317
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
318
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeGroupListRendererComponent, isStandalone: true, selector: "nff-node-group-list-renderer", inputs: { nodeGroupList: "nodeGroupList", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems", maxItems: "maxItems" }, outputs: { message: "message" }, viewQueries: [{ propertyName: "items", predicate: i0.forwardRef(() => DynamicRecursiveFormComponent), descendants: true }], ngImport: i0, template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }] });
319
+ }
320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
321
+ type: Component,
322
+ args: [{ selector: 'nff-node-group-list-renderer', standalone: true, imports: [
323
+ forwardRef(() => DynamicRecursiveFormComponent),
324
+ MatButtonModule,
325
+ MatIconModule,
326
+ MatTooltipModule,
327
+ ], template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"] }]
328
+ }], propDecorators: { nodeGroupList: [{
329
+ type: Input
330
+ }], initialValue: [{
331
+ type: Input
332
+ }], formArray: [{
333
+ type: Input
334
+ }], editable: [{
335
+ type: Input
336
+ }], minItems: [{
337
+ type: Input
338
+ }], maxItems: [{
339
+ type: Input
340
+ }], message: [{
341
+ type: Output
342
+ }], items: [{
343
+ type: ViewChildren,
344
+ args: [forwardRef(() => DynamicRecursiveFormComponent)]
345
+ }] } });
346
+
347
+ class AnonLeafRendererComponent {
348
+ AnonLeaf;
349
+ initialValue;
350
+ control = new FormControl();
351
+ removable = false;
352
+ editable = true;
353
+ index;
354
+ remove = new EventEmitter();
355
+ label;
356
+ emitRemoveEvent() {
357
+ this.remove.emit();
358
+ }
359
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AnonLeafRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
360
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: AnonLeafRendererComponent, isStandalone: true, selector: "nff-anon-leaf-renderer", inputs: { AnonLeaf: "AnonLeaf", initialValue: "initialValue", control: "control", removable: "removable", editable: "editable", index: "index", label: "label" }, outputs: { remove: "remove" }, ngImport: i0, template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n <mat-checkbox [disabled]=\"!editable\" [formControl]=\"control\">value</mat-checkbox>\n}\n @else if (AnonLeaf.type === 'enum' && 'enum' in AnonLeaf) {\n <mat-form-field>\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <mat-select [formControl]=\"control\">\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n}\n</div>\n", styles: [".anon-leaf-container{display:flex;flex-direction:row;gap:8px;align-items:center;flex:1 1 0}mat-form-field{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }] });
361
+ }
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AnonLeafRendererComponent, decorators: [{
363
+ type: Component,
364
+ args: [{ selector: 'nff-anon-leaf-renderer', standalone: true, imports: [
365
+ ReactiveFormsModule,
366
+ MatFormFieldModule,
367
+ MatSelectModule,
368
+ MatIconModule,
369
+ MatCheckboxModule,
370
+ MatInputModule,
371
+ MatButtonModule,
372
+ ], template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n <mat-checkbox [disabled]=\"!editable\" [formControl]=\"control\">value</mat-checkbox>\n}\n @else if (AnonLeaf.type === 'enum' && 'enum' in AnonLeaf) {\n <mat-form-field>\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <mat-select [formControl]=\"control\">\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n}\n</div>\n", styles: [".anon-leaf-container{display:flex;flex-direction:row;gap:8px;align-items:center;flex:1 1 0}mat-form-field{flex:1}\n"] }]
373
+ }], propDecorators: { AnonLeaf: [{
374
+ type: Input
375
+ }], initialValue: [{
376
+ type: Input
377
+ }], control: [{
378
+ type: Input
379
+ }], removable: [{
380
+ type: Input
381
+ }], editable: [{
382
+ type: Input
383
+ }], index: [{
384
+ type: Input
385
+ }], remove: [{
386
+ type: Output
387
+ }], label: [{
388
+ type: Input
389
+ }] } });
390
+
391
+ class LeafListRendererComponent {
392
+ leaf_;
393
+ initialValue;
394
+ formArray;
395
+ editable = true;
396
+ minItems = 1;
397
+ message = new EventEmitter();
398
+ matDialog = inject(MatDialog);
399
+ /**
400
+ * Take a full-width row of its own once the array holds more than one entry.
401
+ * A multi-entry array grows vertically as its items wrap; sitting inline next
402
+ * to a regular leaf that would stretch the leaf to match. On its own line it
403
+ * cannot. Consumed by the `:host(.stacked)` rule.
404
+ */
405
+ get stacked() {
406
+ return (this.formArray?.length ?? 0) > 1;
407
+ }
408
+ ngOnInit() {
409
+ if (this.initialValue) {
410
+ this.formArray.patchValue(this.initialValue);
411
+ }
412
+ }
413
+ removeItem($index) {
414
+ if (this.formArray.length <= this.minItems) {
415
+ this.message.emit({
416
+ message: 'You cannot remove the last item!',
417
+ type: 'error',
418
+ });
419
+ return;
420
+ }
421
+ this.formArray.removeAt($index);
422
+ }
423
+ addItem() {
424
+ this.formArray.push(new FormControl());
425
+ }
426
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
427
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafListRendererComponent, isStandalone: true, selector: "nff-leaf-list-renderer", inputs: { leaf_: "leaf_", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems" }, outputs: { message: "message" }, host: { properties: { "class.stacked": "this.stacked" } }, ngImport: i0, template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: AnonLeafRendererComponent, selector: "nff-anon-leaf-renderer", inputs: ["AnonLeaf", "initialValue", "control", "removable", "editable", "index", "label"], outputs: ["remove"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }] });
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
430
+ type: Component,
431
+ args: [{ selector: 'nff-leaf-list-renderer', standalone: true, imports: [
432
+ MatIconModule,
433
+ AnonLeafRendererComponent,
434
+ MatButtonModule,
435
+ MatPrefix,
436
+ ], template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"] }]
437
+ }], propDecorators: { leaf_: [{
438
+ type: Input
439
+ }], initialValue: [{
440
+ type: Input
441
+ }], formArray: [{
442
+ type: Input
443
+ }], editable: [{
444
+ type: Input
445
+ }], minItems: [{
446
+ type: Input
447
+ }], message: [{
448
+ type: Output
449
+ }], stacked: [{
450
+ type: HostBinding,
451
+ args: ['class.stacked']
452
+ }] } });
453
+
454
+ function nodeGroupChildrenList(schema) {
455
+ const children = schema.children ?? {};
456
+ return Object.entries(children).map(([key, value]) => ({
457
+ key,
458
+ value: value,
459
+ }));
460
+ }
461
+ function nodeGroupChildrenLeafs(schema) {
462
+ const children = schema.children ?? {};
463
+ return Object.entries(children).reduce((acc, [key, value]) => {
464
+ if (value.kind === 'leaf') {
465
+ acc.push({ key, value: value });
466
+ }
467
+ return acc;
468
+ }, []);
469
+ }
470
+ function removeNullAndEmptyArrays(value) {
471
+ if (Array.isArray(value)) {
472
+ const cleanedArray = value
473
+ .map(removeNullAndEmptyArrays)
474
+ .filter(v => v != null && (typeof v !== 'object' || Object.keys(v).length > 0));
475
+ return cleanedArray.length > 0 ? cleanedArray : undefined;
476
+ }
477
+ if (value && typeof value === 'object') {
478
+ const cleanedObject = Object.fromEntries(Object.entries(value)
479
+ .map(([k, v]) => [k, removeNullAndEmptyArrays(v)])
480
+ .filter(([_, v]) => v != null && (typeof v !== 'object' || Object.keys(v).length > 0)));
481
+ return Object.keys(cleanedObject).length > 0 ? cleanedObject : undefined;
482
+ }
483
+ return value != null ? value : undefined;
484
+ }
485
+ function asFormControl(control) {
486
+ return control;
487
+ }
488
+ function asFormArray(control) {
489
+ return control;
490
+ }
491
+ function asFormGroup(control) {
492
+ return control;
493
+ }
494
+
495
+ class DynamicRecursiveFormComponent {
496
+ schema;
497
+ initialValue;
498
+ formGroup = new FormGroup({});
499
+ index = null;
500
+ removable = false;
501
+ remove = new EventEmitter();
502
+ title;
503
+ editable = false;
504
+ addButtonCallback = null;
505
+ root = false;
506
+ ngOnInit() {
507
+ this.root = this.schema.root ?? false;
508
+ if (this.initialValue) {
509
+ this.formGroup.patchValue(this.initialValue);
510
+ }
511
+ }
512
+ get nodeGroupChildrenList() {
513
+ const children = this.schema.children ?? {};
514
+ return Object.entries(children).map(([key, value]) => ({
515
+ key,
516
+ value: value,
517
+ }));
518
+ }
519
+ emitRemoveEvent() {
520
+ this.remove.emit();
521
+ }
522
+ /**
523
+ * Add or remove a presence group's control on this form. Removing it drops the
524
+ * group from `form.value`; adding it rebuilds the sub-group from its schema.
525
+ */
526
+ togglePresence(key, schema, present) {
527
+ if (present) {
528
+ if (!this.formGroup.get(key)) {
529
+ this.formGroup.addControl(key, buildFormFromSchema(schema));
530
+ }
531
+ }
532
+ else if (this.formGroup.get(key)) {
533
+ this.formGroup.removeControl(key);
534
+ }
535
+ }
536
+ CASE_KEY = CASE_KEY;
537
+ objectKeys(obj) {
538
+ return Object.keys(obj);
539
+ }
540
+ /** The active case name of a choice control, or null if none is selected. */
541
+ activeCase(key) {
542
+ return this.formGroup.get(key)?.get(CASE_KEY)?.value ?? null;
543
+ }
544
+ /** A synthetic flattened NodeGroup used to render the active case's fields against the choice's FormGroup. */
545
+ caseAsGroup(choice, caseName) {
546
+ return {
547
+ kind: 'nodeGroup',
548
+ name: choice.name,
549
+ children: choice.cases[caseName] ?? {},
550
+ appearance: { flatten: true },
551
+ };
552
+ }
553
+ /** Swap a choice's field controls when the selected case changes. */
554
+ switchCase(key, choice, caseName) {
555
+ const group = this.formGroup.get(key);
556
+ for (const name of Object.keys(group.controls)) {
557
+ if (name !== CASE_KEY)
558
+ group.removeControl(name);
559
+ }
560
+ const caseChildren = choice.cases[caseName] ?? {};
561
+ for (const name in caseChildren) {
562
+ group.addControl(name, buildControl(caseChildren[name]));
563
+ }
564
+ }
565
+ asFormGroup = asFormGroup;
566
+ asFormArray = asFormArray;
567
+ asFormControl = asFormControl;
568
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
569
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: "schema", initialValue: "initialValue", formGroup: "formGroup", index: "index", removable: "removable", title: "title", editable: "editable", addButtonCallback: "addButtonCallback" }, outputs: { remove: "remove" }, ngImport: i0, template: "@if (!root) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup.get(child.name))\n [removable]=false\n [editable]=editable\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=editable\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup.get(childItem.key)\"\n [disabled]=\"!editable\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup.get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(childItem.key))\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup.get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ child.label ?? child.name }}</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseName }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n }\n\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n }\n }\n </div>\n </ng-template>\n @if (schema.appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable && index != null && addButtonCallback != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema.label ?? schema.name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback(index)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && removable) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-card appearance=\"outlined\" [class]=\"{ 'mat-card-no-broder': schema.appearance?.noBorder }\">\n <mat-card-header>\n <div class=\"card-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable}\" (click)=\"editable = !editable\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable && index != null && addButtonCallback != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema.label ?? schema.name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback(index)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && removable) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n <mat-card-title class=\"config-form-subsection-card\">\n {{ title ?? schema.label ?? schema.name }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-card-content>\n </mat-card>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable }\" (click)=\"editable = !editable\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup.get(child.name))\n [removable]=false\n [editable]=editable\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=editable\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup.get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ child.label ?? child.name }}</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseName }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n }\n </div>\n </div>\n <mat-accordion multi>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ title ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel togglePosition=\"before\" [expanded]=\"!!formGroup.get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup.get(childItem.key)\"\n [disabled]=\"!editable\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup.get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(childItem.key))\"\n [editable]=\"editable\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <mat-expansion-panel togglePosition=\"before\" expanded=\"true\">\n <mat-expansion-panel-header >\n <mat-panel-title>\n {{ title ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [title]=\"child.name ?? 'General Settings'\"\n [editable]=\"editable\"\n />\n </mat-expansion-panel>\n }\n }\n </mat-accordion>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content,.mat-expansion-panel-content:hover{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.node-groups-container{display:flex;flex-direction:column;flex-basis:100%}.section-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;width:100%}mat-card{padding:0;width:100%}.mat-card-no-broder{border:0}mat-card-content{box-sizing:border-box;padding:16px}mat-card-header{display:flex;flex-direction:row;align-items:flex-start;padding:16px;gap:16px}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-transition .5s}.card-actions{display:flex;flex-direction:row;align-items:center;gap:4px}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}.flattened-style-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;align-items:flex-start}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"], dependencies: [{ kind: "component", type: DynamicRecursiveFormComponent, selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove"] }, { kind: "component", type: LeafRendererComponent, selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable"], outputs: ["remove"] }, { kind: "component", type: NodeGroupListRendererComponent, selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "minItems", "maxItems"], outputs: ["message"] }, { kind: "component", type: LeafListRendererComponent, selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i2$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i2$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i2$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i2$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i5.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i5.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i5.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i5.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
570
+ }
571
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
572
+ type: Component,
573
+ args: [{ imports: [
574
+ LeafRendererComponent,
575
+ NodeGroupListRendererComponent,
576
+ LeafListRendererComponent,
577
+ ReactiveFormsModule,
578
+ MatExpansionModule,
579
+ MatIconModule,
580
+ MatButtonModule,
581
+ NgTemplateOutlet,
582
+ MatCardModule,
583
+ MatCheckboxModule,
584
+ MatFormFieldModule,
585
+ MatSelectModule,
586
+ MatTooltip,
587
+ ], selector: 'nff-dynamic-recursive-form', standalone: true, template: "@if (!root) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup.get(child.name))\n [removable]=false\n [editable]=editable\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=editable\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup.get(childItem.key)\"\n [disabled]=\"!editable\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup.get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(childItem.key))\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup.get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ child.label ?? child.name }}</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseName }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n }\n\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n }\n }\n </div>\n </ng-template>\n @if (schema.appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable && index != null && addButtonCallback != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema.label ?? schema.name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback(index)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && removable) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-card appearance=\"outlined\" [class]=\"{ 'mat-card-no-broder': schema.appearance?.noBorder }\">\n <mat-card-header>\n <div class=\"card-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable}\" (click)=\"editable = !editable\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable && index != null && addButtonCallback != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema.label ?? schema.name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback(index)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && removable) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n <mat-card-title class=\"config-form-subsection-card\">\n {{ title ?? schema.label ?? schema.name }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-card-content>\n </mat-card>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable }\" (click)=\"editable = !editable\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup.get(child.name))\n [removable]=false\n [editable]=editable\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=editable\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup.get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ child.label ?? child.name }}</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseName }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable\"\n />\n }\n </div>\n }\n }\n </div>\n </div>\n <mat-accordion multi>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ title ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [editable]=\"editable\"\n />\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel togglePosition=\"before\" [expanded]=\"!!formGroup.get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup.get(childItem.key)\"\n [disabled]=\"!editable\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup.get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(childItem.key))\"\n [editable]=\"editable\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <mat-expansion-panel togglePosition=\"before\" expanded=\"true\">\n <mat-expansion-panel-header >\n <mat-panel-title>\n {{ title ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup.get(child.name))\"\n [initialValue]=\"formGroup.get(child.name)?.value\"\n [title]=\"child.name ?? 'General Settings'\"\n [editable]=\"editable\"\n />\n </mat-expansion-panel>\n }\n }\n </mat-accordion>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content,.mat-expansion-panel-content:hover{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.node-groups-container{display:flex;flex-direction:column;flex-basis:100%}.section-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;width:100%}mat-card{padding:0;width:100%}.mat-card-no-broder{border:0}mat-card-content{box-sizing:border-box;padding:16px}mat-card-header{display:flex;flex-direction:row;align-items:flex-start;padding:16px;gap:16px}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-transition .5s}.card-actions{display:flex;flex-direction:row;align-items:center;gap:4px}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}.flattened-style-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;align-items:flex-start}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"] }]
588
+ }], propDecorators: { schema: [{
589
+ type: Input,
590
+ args: [{ required: true }]
591
+ }], initialValue: [{
592
+ type: Input
593
+ }], formGroup: [{
594
+ type: Input
595
+ }], index: [{
596
+ type: Input
597
+ }], removable: [{
598
+ type: Input
599
+ }], remove: [{
600
+ type: Output
601
+ }], title: [{
602
+ type: Input
603
+ }], editable: [{
604
+ type: Input
605
+ }], addButtonCallback: [{
606
+ type: Input
607
+ }] } });
608
+
609
+ /**
610
+ * A tree/detail editor for a schema-built form: the structure (containers, lists,
611
+ * groups) is a tree on the left, and selecting a node shows that node's leaf
612
+ * fields for editing on the right. A `+` on a list node adds an entry; a delete
613
+ * button on each item removes it. An alternative to the all-in-one
614
+ * {@link DynamicRecursiveFormComponent} for large configs.
615
+ */
616
+ class ConfigEditorComponent {
617
+ schema;
618
+ formGroup;
619
+ editable = true;
620
+ root;
621
+ selected = null;
622
+ expanded = new Set();
623
+ nextId = 0;
624
+ ngOnInit() {
625
+ this.root = this.buildTree(this.schema, this.formGroup, this.schema.label ?? this.schema.name);
626
+ this.expanded.add(this.root.id);
627
+ this.select(this.root);
628
+ }
629
+ select(node) {
630
+ this.selected = node;
631
+ // Reveal the node's direct children in the tree as it opens on the right.
632
+ if (node.children.length)
633
+ this.expanded.add(node.id);
634
+ }
635
+ /** Select a child (list item or sub-group) from the detail pane, keeping its parent expanded in the tree. */
636
+ open(parent, child) {
637
+ this.expanded.add(parent.id);
638
+ this.select(child);
639
+ }
640
+ /**
641
+ * Root-to-`target` path for the detail-pane breadcrumb (inclusive of both ends),
642
+ * or an empty array if `target` is not in the current tree. Searched fresh each
643
+ * call so it stays correct after add/remove/presence mutations rebuild subtrees.
644
+ */
645
+ pathTo(target) {
646
+ const walk = (node, trail) => {
647
+ const here = [...trail, node];
648
+ if (node === target)
649
+ return here;
650
+ for (const child of node.children) {
651
+ const found = walk(child, here);
652
+ if (found)
653
+ return found;
654
+ }
655
+ return null;
656
+ };
657
+ return this.root ? (walk(this.root, []) ?? []) : [];
658
+ }
659
+ toggle(node) {
660
+ if (this.expanded.has(node.id))
661
+ this.expanded.delete(node.id);
662
+ else
663
+ this.expanded.add(node.id);
664
+ }
665
+ /** Append a new item to a list node's FormArray and to the tree, then select it. */
666
+ addItem(listNode) {
667
+ const list = listNode.list;
668
+ if (!list)
669
+ return;
670
+ const group = buildFormFromSchema(list.itemSchema);
671
+ list.array.push(group);
672
+ const item = this.buildTree(list.itemSchema, group, list.itemLabel);
673
+ item.removable = { array: list.array, index: list.array.length - 1 };
674
+ listNode.children.push(item);
675
+ this.renumber(listNode);
676
+ this.expanded.add(listNode.id);
677
+ this.select(item);
678
+ }
679
+ /** Remove a list item from its FormArray and the tree (down to `minItems`). */
680
+ removeItem(listNode, item) {
681
+ if (!listNode.list || !item.removable)
682
+ return;
683
+ if (listNode.list.array.length <= listNode.list.minItems)
684
+ return;
685
+ listNode.list.array.removeAt(item.removable.index);
686
+ listNode.children.splice(listNode.children.indexOf(item), 1);
687
+ this.renumber(listNode);
688
+ if (this.selected === item)
689
+ this.select(listNode);
690
+ }
691
+ /** Enable or disable an optional (presence) group by adding/removing its control. */
692
+ setPresence(node, present) {
693
+ const p = node.presence;
694
+ if (!p)
695
+ return;
696
+ if (present) {
697
+ if (!(p.parentGroup.get(p.key) instanceof FormGroup)) {
698
+ const group = buildFormFromSchema(p.schema);
699
+ p.parentGroup.addControl(p.key, group);
700
+ const built = this.buildTree(p.schema, group, node.label);
701
+ node.children = built.children;
702
+ node.leaves = built.leaves;
703
+ node.leafLists = built.leafLists;
704
+ node.group = group;
705
+ }
706
+ node.present = true;
707
+ this.select(node);
708
+ }
709
+ else {
710
+ p.parentGroup.removeControl(p.key);
711
+ node.children = [];
712
+ node.leaves = [];
713
+ node.leafLists = [];
714
+ node.group = null;
715
+ node.present = false;
716
+ }
717
+ }
718
+ asFormControl = asFormControl;
719
+ asFormArray = asFormArray;
720
+ placeholder(label) {
721
+ return { id: String(this.nextId++), label, children: [], leaves: [], leafLists: [], group: null };
722
+ }
723
+ /** Re-index and re-label a list node's item children (just "#n") after add/remove. */
724
+ renumber(listNode) {
725
+ listNode.children.forEach((child, i) => {
726
+ if (child.removable)
727
+ child.removable.index = i;
728
+ child.label = `#${i + 1}`;
729
+ });
730
+ }
731
+ buildTree(schema, group, label) {
732
+ const leaves = [];
733
+ const leafLists = [];
734
+ const children = [];
735
+ for (const key of Object.keys(schema.children)) {
736
+ const child = schema.children[key];
737
+ if (child.kind === 'leaf') {
738
+ leaves.push({ key, node: child });
739
+ }
740
+ else if (child.kind === 'leafList') {
741
+ leafLists.push({ key, node: child });
742
+ }
743
+ else if (child.kind === 'nodeGroup') {
744
+ const childGroup = group.get(key);
745
+ if (child.presence) {
746
+ // Optional group: always a tree node — a placeholder when absent.
747
+ const node = childGroup instanceof FormGroup
748
+ ? this.buildTree(child, childGroup, child.label ?? key)
749
+ : this.placeholder(child.label ?? key);
750
+ node.presence = { parentGroup: group, key, schema: child };
751
+ node.present = childGroup instanceof FormGroup;
752
+ children.push(node);
753
+ }
754
+ else if (childGroup instanceof FormGroup) {
755
+ children.push(this.buildTree(child, childGroup, child.label ?? key));
756
+ }
757
+ }
758
+ else if (child.kind === 'nodeGroupList') {
759
+ const array = group.get(key);
760
+ const itemLabel = child.type.label ?? child.type.name;
761
+ const items = array instanceof FormArray
762
+ ? array.controls
763
+ .filter((c) => c instanceof FormGroup)
764
+ .map((item, i) => {
765
+ // Just "#n": the item sits under its list node, so repeating the
766
+ // item name (e.g. "Interface #1") only echoes the parent.
767
+ const node = this.buildTree(child.type, item, `#${i + 1}`);
768
+ node.removable = { array, index: i };
769
+ return node;
770
+ })
771
+ : [];
772
+ children.push({
773
+ id: String(this.nextId++),
774
+ label: child.label ?? key,
775
+ children: items,
776
+ leaves: [],
777
+ leafLists: [],
778
+ group: null,
779
+ list: array instanceof FormArray
780
+ ? { array, itemSchema: child.type, itemLabel, minItems: child.minItems ?? 0 }
781
+ : undefined,
782
+ });
783
+ }
784
+ // choice nodes are not shown in the tree yet.
785
+ }
786
+ return { id: String(this.nextId++), label, children, leaves, leafLists, group };
787
+ }
788
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
789
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: "schema", formGroup: "formGroup", editable: "editable" }, ngImport: i0, template: "<div class=\"editor\">\n <nav class=\"tree\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n [class.selected]=\"node === selected\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (node.children.length) {\n <button\n matIconButton\n class=\"twisty\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n @if (node.presence) {\n <mat-checkbox\n class=\"presence-check\"\n [checked]=\"!!node.present\"\n [disabled]=\"!editable\"\n (change)=\"setPresence(node, $event.checked)\"\n (click)=\"$event.stopPropagation()\"\n ></mat-checkbox>\n }\n <span class=\"tree-label\" [class.absent]=\"node.presence && !node.present\">{{ node.label }}</span>\n\n @if (editable && node.list) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && node.removable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of pathTo(selected); track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n }\n @if (selected && selected.group) {\n <div class=\"group-body\">\n <div class=\"fields\" [formGroup]=\"selected.group\">\n @for (leaf of selected.leaves; track leaf.key) {\n <nff-leaf-renderer\n [leaf_]=\"leaf.node\"\n [control]=\"asFormControl(selected.group.get(leaf.key))\"\n [editable]=\"editable\"\n />\n }\n @for (list of selected.leafLists; track list.key) {\n <nff-leaf-list-renderer\n [leaf_]=\"list.node\"\n [formArray]=\"asFormArray(selected.group.get(list.key))\"\n [editable]=\"editable\"\n />\n }\n @if (!selected.leaves.length && !selected.leafLists.length && !selected.children.length) {\n <p class=\"empty\">This node has no fields.</p>\n }\n </div>\n @if (selected.children.length) {\n <nav class=\"child-links\">\n <h4 class=\"child-links-title\">Sections</h4>\n @for (child of selected.children; track child.id) {\n <button type=\"button\" class=\"item-link child-link\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n } @else if (selected && selected.presence && !selected.present) {\n <p class=\"empty\">This optional group is off. Tick its box in the tree to add it.</p>\n } @else if (selected && selected.list) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable && item.removable) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No {{ selected.list.itemLabel }} items yet.</p>\n }\n @if (editable) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addItem(selected)\">\n <mat-icon>add</mat-icon> Add {{ selected.list.itemLabel }}\n </button>\n </div>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: [":host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 260px;overflow:auto;border:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));border-radius:8px;padding:4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high, rgba(0, 0, 0, .04))}.tree-row.selected{background:var(--mat-sys-secondary-container, rgba(103, 80, 164, .12));font-weight:600}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.presence-check{flex:0 0 auto;margin-right:2px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-label.absent{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55));font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .5))}.fields{display:flex;flex-direction:column;gap:4px}.empty{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));font-style:italic}.item-list{list-style:none;margin:0 0 8px;padding:0}.item-row{display:flex;align-items:center;gap:4px}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary, #6750a4);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}.remove-item{flex:0 0 auto}.list-actions{display:flex;justify-content:flex-end}.group-body{display:flex;align-items:flex-start;gap:24px}.group-body .fields{flex:1 1 auto;min-width:0}.child-links{flex:0 0 200px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding-left:16px;border-left:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12))}.child-links-title{margin:0 0 4px;font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6))}.child-link{display:inline-flex;align-items:center;gap:2px}.child-link mat-icon{font-size:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafRendererComponent, selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable"], outputs: ["remove"] }, { kind: "component", type: LeafListRendererComponent, selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }] });
790
+ }
791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
792
+ type: Component,
793
+ args: [{ selector: 'nff-config-editor', standalone: true, imports: [
794
+ ReactiveFormsModule,
795
+ NgTemplateOutlet,
796
+ MatIconModule,
797
+ MatButtonModule,
798
+ MatCheckboxModule,
799
+ MatTooltip,
800
+ LeafRendererComponent,
801
+ LeafListRendererComponent,
802
+ ], template: "<div class=\"editor\">\n <nav class=\"tree\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n [class.selected]=\"node === selected\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (node.children.length) {\n <button\n matIconButton\n class=\"twisty\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n @if (node.presence) {\n <mat-checkbox\n class=\"presence-check\"\n [checked]=\"!!node.present\"\n [disabled]=\"!editable\"\n (change)=\"setPresence(node, $event.checked)\"\n (click)=\"$event.stopPropagation()\"\n ></mat-checkbox>\n }\n <span class=\"tree-label\" [class.absent]=\"node.presence && !node.present\">{{ node.label }}</span>\n\n @if (editable && node.list) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable && node.removable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of pathTo(selected); track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n }\n @if (selected && selected.group) {\n <div class=\"group-body\">\n <div class=\"fields\" [formGroup]=\"selected.group\">\n @for (leaf of selected.leaves; track leaf.key) {\n <nff-leaf-renderer\n [leaf_]=\"leaf.node\"\n [control]=\"asFormControl(selected.group.get(leaf.key))\"\n [editable]=\"editable\"\n />\n }\n @for (list of selected.leafLists; track list.key) {\n <nff-leaf-list-renderer\n [leaf_]=\"list.node\"\n [formArray]=\"asFormArray(selected.group.get(list.key))\"\n [editable]=\"editable\"\n />\n }\n @if (!selected.leaves.length && !selected.leafLists.length && !selected.children.length) {\n <p class=\"empty\">This node has no fields.</p>\n }\n </div>\n @if (selected.children.length) {\n <nav class=\"child-links\">\n <h4 class=\"child-links-title\">Sections</h4>\n @for (child of selected.children; track child.id) {\n <button type=\"button\" class=\"item-link child-link\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n } @else if (selected && selected.presence && !selected.present) {\n <p class=\"empty\">This optional group is off. Tick its box in the tree to add it.</p>\n } @else if (selected && selected.list) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable && item.removable) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No {{ selected.list.itemLabel }} items yet.</p>\n }\n @if (editable) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addItem(selected)\">\n <mat-icon>add</mat-icon> Add {{ selected.list.itemLabel }}\n </button>\n </div>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: [":host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 260px;overflow:auto;border:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));border-radius:8px;padding:4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high, rgba(0, 0, 0, .04))}.tree-row.selected{background:var(--mat-sys-secondary-container, rgba(103, 80, 164, .12));font-weight:600}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.presence-check{flex:0 0 auto;margin-right:2px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-label.absent{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55));font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .5))}.fields{display:flex;flex-direction:column;gap:4px}.empty{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));font-style:italic}.item-list{list-style:none;margin:0 0 8px;padding:0}.item-row{display:flex;align-items:center;gap:4px}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary, #6750a4);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}.remove-item{flex:0 0 auto}.list-actions{display:flex;justify-content:flex-end}.group-body{display:flex;align-items:flex-start;gap:24px}.group-body .fields{flex:1 1 auto;min-width:0}.child-links{flex:0 0 200px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding-left:16px;border-left:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12))}.child-links-title{margin:0 0 4px;font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6))}.child-link{display:inline-flex;align-items:center;gap:2px}.child-link mat-icon{font-size:18px;width:18px;height:18px}\n"] }]
803
+ }], propDecorators: { schema: [{
804
+ type: Input,
805
+ args: [{ required: true }]
806
+ }], formGroup: [{
807
+ type: Input,
808
+ args: [{ required: true }]
809
+ }], editable: [{
810
+ type: Input
811
+ }] } });
812
+
813
+ /*
814
+ * Public API Surface of ng-form-foundry
815
+ */
816
+
817
+ /**
818
+ * Generated bundle index. Do not edit.
819
+ */
820
+
821
+ export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, buildControl, buildFormFromSchema, defineSchema };
822
+ //# sourceMappingURL=ng-form-foundry.mjs.map