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.
- package/LICENSE +201 -0
- package/README.md +72 -38
- package/fesm2022/ng-form-foundry.mjs +822 -0
- package/fesm2022/ng-form-foundry.mjs.map +1 -0
- package/index.d.ts +307 -0
- package/package.json +42 -5
- package/ng-package.json +0 -7
- package/src/lib/core/dynamic-recursive-forms-builder.ts +0 -129
- package/src/lib/core/utils.ts +0 -33
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.html +0 -31
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.scss +0 -11
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.spec.ts +0 -22
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.ts +0 -39
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.html +0 -168
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.scss +0 -128
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.spec.ts +0 -23
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.ts +0 -71
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.stories.ts +0 -36
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.html +0 -8
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.scss +0 -9
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.spec.ts +0 -22
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.ts +0 -21
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.html +0 -31
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.scss +0 -57
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.spec.ts +0 -23
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.ts +0 -52
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.html +0 -17
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.scss +0 -11
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.spec.ts +0 -22
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.ts +0 -41
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.html +0 -24
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.scss +0 -48
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.spec.ts +0 -22
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.ts +0 -101
- package/src/lib/types/dynamic-recursive.types.ts +0 -97
- package/src/lib/types/examples/complex-oai.ts +0 -938
- package/src/public-api.ts +0 -12
- package/tsconfig.lib.json +0 -18
- package/tsconfig.lib.prod.json +0 -11
- 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
|