ng-form-foundry 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/fesm2022/ng-form-foundry.mjs +754 -169
- package/fesm2022/ng-form-foundry.mjs.map +1 -1
- package/index.d.ts +347 -40
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i1 from '@angular/forms';
|
|
2
2
|
import { Validators, FormControl, FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Input, Component, EventEmitter, Output, inject, ChangeDetectorRef, forwardRef, ViewChildren, HostBinding } from '@angular/core';
|
|
4
|
+
import { Input, Component, EventEmitter, Output, inject, ChangeDetectorRef, forwardRef, ViewChildren, HostBinding, input, output, model, computed } from '@angular/core';
|
|
5
5
|
import * as i2 from '@angular/material/form-field';
|
|
6
6
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
7
7
|
import * as i3$1 from '@angular/material/input';
|
|
@@ -10,17 +10,18 @@ import * as i4 from '@angular/material/checkbox';
|
|
|
10
10
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
11
11
|
import * as i3 from '@angular/material/select';
|
|
12
12
|
import { MatSelectModule } from '@angular/material/select';
|
|
13
|
-
import * as
|
|
13
|
+
import * as i2$1 from '@angular/material/icon';
|
|
14
14
|
import { MatIconModule } from '@angular/material/icon';
|
|
15
|
-
import * as
|
|
15
|
+
import * as i4$1 from '@angular/material/button';
|
|
16
16
|
import { MatButtonModule } from '@angular/material/button';
|
|
17
|
-
import {
|
|
17
|
+
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
|
|
18
18
|
import { MatDialog } from '@angular/material/dialog';
|
|
19
|
-
import * as i2$
|
|
19
|
+
import * as i2$2 from '@angular/material/expansion';
|
|
20
20
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
21
|
-
import * as i5 from '@angular/material/card';
|
|
22
21
|
import { MatCardModule } from '@angular/material/card';
|
|
23
22
|
import { NgTemplateOutlet } from '@angular/common';
|
|
23
|
+
import * as i4$2 from '@angular/material/menu';
|
|
24
|
+
import { MatMenuModule } from '@angular/material/menu';
|
|
24
25
|
|
|
25
26
|
/** The control name that records which case of a {@link NodeChoice} is active. */
|
|
26
27
|
const CASE_KEY = '__case';
|
|
@@ -41,10 +42,74 @@ function isNodeGroupList(node) {
|
|
|
41
42
|
function isChoice(node) {
|
|
42
43
|
return node.kind === 'choice';
|
|
43
44
|
}
|
|
45
|
+
function isMap(node) {
|
|
46
|
+
return node.kind === 'map';
|
|
47
|
+
}
|
|
44
48
|
function enumValidator(choices) {
|
|
45
49
|
const set = new Set(choices);
|
|
46
50
|
return (ctrl) => ctrl.value == null || set.has(ctrl.value) ? null : { enum: true };
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* JSON Schema `pattern`: an *unanchored* `RegExp.test`, unlike Angular's built-in
|
|
54
|
+
* `Validators.pattern` which anchors the expression. An invalid regex disables
|
|
55
|
+
* the check rather than throwing. Empty/absent values pass (use `required`).
|
|
56
|
+
*/
|
|
57
|
+
function patternValidator(pattern) {
|
|
58
|
+
let re;
|
|
59
|
+
try {
|
|
60
|
+
re = new RegExp(pattern);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return () => null;
|
|
64
|
+
}
|
|
65
|
+
return (ctrl) => {
|
|
66
|
+
const v = ctrl.value;
|
|
67
|
+
if (v == null || v === '')
|
|
68
|
+
return null;
|
|
69
|
+
return re.test(String(v)) ? null : { pattern: { requiredPattern: pattern, actualValue: v } };
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** JSON Schema `type: 'integer'`: reject a value that is not a whole number. */
|
|
73
|
+
function integerValidator() {
|
|
74
|
+
return (ctrl) => {
|
|
75
|
+
const v = ctrl.value;
|
|
76
|
+
if (v == null || v === '')
|
|
77
|
+
return null;
|
|
78
|
+
const num = typeof v === 'number' ? v : Number(v);
|
|
79
|
+
return Number.isInteger(num) ? null : { integer: true };
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** JSON Schema `multipleOf`: reject a value that is not an integer multiple of `step`. */
|
|
83
|
+
function multipleOfValidator(step) {
|
|
84
|
+
return (ctrl) => {
|
|
85
|
+
const v = ctrl.value;
|
|
86
|
+
if (v == null || v === '')
|
|
87
|
+
return null;
|
|
88
|
+
const num = typeof v === 'number' ? v : Number(v);
|
|
89
|
+
if (Number.isNaN(num))
|
|
90
|
+
return null;
|
|
91
|
+
const ratio = num / step;
|
|
92
|
+
// A small tolerance absorbs binary float drift (e.g. 0.3 / 0.1).
|
|
93
|
+
return Math.abs(ratio - Math.round(ratio)) < 1e-9
|
|
94
|
+
? null
|
|
95
|
+
: { multipleOf: { multipleOf: step, actual: num } };
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** JSON Schema `format: uri`: reject a string that is not a parseable absolute URI. */
|
|
99
|
+
function uriValidator() {
|
|
100
|
+
return (ctrl) => {
|
|
101
|
+
const v = ctrl.value;
|
|
102
|
+
if (v == null || v === '')
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
new URL(String(v));
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { uri: true };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
48
113
|
function buildLeafControl(leaf, initial) {
|
|
49
114
|
const validators = [];
|
|
50
115
|
if ('required' in leaf && leaf.required)
|
|
@@ -53,9 +118,38 @@ function buildLeafControl(leaf, initial) {
|
|
|
53
118
|
const choices = leaf.enum;
|
|
54
119
|
validators.push(enumValidator(choices));
|
|
55
120
|
}
|
|
121
|
+
if (leaf.type === 'string') {
|
|
122
|
+
const s = leaf;
|
|
123
|
+
if (s.pattern != null)
|
|
124
|
+
validators.push(patternValidator(s.pattern));
|
|
125
|
+
if (s.minLength != null)
|
|
126
|
+
validators.push(Validators.minLength(s.minLength));
|
|
127
|
+
if (s.maxLength != null)
|
|
128
|
+
validators.push(Validators.maxLength(s.maxLength));
|
|
129
|
+
if (s.format === 'email')
|
|
130
|
+
validators.push(Validators.email);
|
|
131
|
+
else if (s.format === 'uri' || s.format === 'url')
|
|
132
|
+
validators.push(uriValidator());
|
|
133
|
+
}
|
|
134
|
+
else if (leaf.type === 'number') {
|
|
135
|
+
const n = leaf;
|
|
136
|
+
if (n.integer)
|
|
137
|
+
validators.push(integerValidator());
|
|
138
|
+
if (n.min != null)
|
|
139
|
+
validators.push(Validators.min(n.min));
|
|
140
|
+
if (n.max != null)
|
|
141
|
+
validators.push(Validators.max(n.max));
|
|
142
|
+
if (n.multipleOf != null)
|
|
143
|
+
validators.push(multipleOfValidator(n.multipleOf));
|
|
144
|
+
}
|
|
56
145
|
const defaultValue = initial ?? ('default' in leaf ? leaf.default : undefined) ?? null;
|
|
146
|
+
// A nullable leaf drops `nonNullable`, so `null` is a first-class value that
|
|
147
|
+
// `reset()` restores and that survives the round-trip (JSON Schema `null`).
|
|
148
|
+
// The typed model still treats a leaf value as non-null, so the runtime
|
|
149
|
+
// nullable control is cast back to the declared type.
|
|
150
|
+
const nullable = 'nullable' in leaf && leaf.nullable === true;
|
|
57
151
|
return new FormControl(defaultValue, {
|
|
58
|
-
nonNullable:
|
|
152
|
+
nonNullable: !nullable,
|
|
59
153
|
validators,
|
|
60
154
|
});
|
|
61
155
|
}
|
|
@@ -98,17 +192,144 @@ function buildNodeGroupListControl(list, initial = null) {
|
|
|
98
192
|
* name plus that case's field controls. Only the active case's fields are built,
|
|
99
193
|
* matching the inline YANG encoding; switching the case swaps them.
|
|
100
194
|
*/
|
|
195
|
+
/**
|
|
196
|
+
* Normalize a {@link ChoiceCase} to a field record. A field record is returned
|
|
197
|
+
* as-is; a single node (a leaf-bodied case, e.g. a scalar `anyOf` branch) becomes
|
|
198
|
+
* a one-field record keyed by the node's `name`. The discriminant is a top-level
|
|
199
|
+
* `kind` string, which a field record never has (its keys are field names).
|
|
200
|
+
*/
|
|
201
|
+
function caseFields(body) {
|
|
202
|
+
return typeof body.kind === 'string'
|
|
203
|
+
? { [body.name]: body }
|
|
204
|
+
: body;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The active case of a choice: an explicit `__case` in the initial value, else
|
|
208
|
+
* the case whose fields best match the initial data (inline wire data carries no
|
|
209
|
+
* `__case`), else the schema `default`. This lets a choice seed from real
|
|
210
|
+
* instance data whose branch is discriminated by which fields are present.
|
|
211
|
+
*/
|
|
212
|
+
function resolveChoiceCase(choice, initial) {
|
|
213
|
+
const explicit = initial?.[CASE_KEY];
|
|
214
|
+
if (typeof explicit === 'string')
|
|
215
|
+
return explicit;
|
|
216
|
+
return inferChoiceCase(choice, initial) ?? choice.default;
|
|
217
|
+
}
|
|
218
|
+
/** Pick the case whose fields most overlap the initial data (none if nothing matches). */
|
|
219
|
+
function inferChoiceCase(choice, initial) {
|
|
220
|
+
if (!initial)
|
|
221
|
+
return undefined;
|
|
222
|
+
let best;
|
|
223
|
+
let bestScore = 0;
|
|
224
|
+
for (const name of Object.keys(choice.cases)) {
|
|
225
|
+
const fields = Object.keys(caseFields(choice.cases[name]));
|
|
226
|
+
let score = 0;
|
|
227
|
+
for (const field of fields)
|
|
228
|
+
if (field in initial)
|
|
229
|
+
score++;
|
|
230
|
+
if (score > bestScore) {
|
|
231
|
+
bestScore = score;
|
|
232
|
+
best = name;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return best;
|
|
236
|
+
}
|
|
101
237
|
function buildChoiceControl(choice, initial) {
|
|
102
|
-
const active = initial
|
|
238
|
+
const active = resolveChoiceCase(choice, initial);
|
|
103
239
|
const controls = { [CASE_KEY]: new FormControl(active ?? null) };
|
|
104
240
|
if (active && choice.cases[active]) {
|
|
105
|
-
const caseChildren = choice.cases[active];
|
|
241
|
+
const caseChildren = caseFields(choice.cases[active]);
|
|
106
242
|
for (const key in caseChildren) {
|
|
107
243
|
controls[key] = buildControl(caseChildren[key], initial?.[key]);
|
|
108
244
|
}
|
|
109
245
|
}
|
|
110
246
|
return new FormGroup(controls);
|
|
111
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Switch a choice's FormGroup to `caseName`: sets `__case`, removes every other
|
|
250
|
+
* control, and builds `caseName`'s fields (normalized via {@link caseFields})
|
|
251
|
+
* with their defaults. An unknown case name leaves only `__case`.
|
|
252
|
+
*/
|
|
253
|
+
function switchChoiceCase(group, choice, caseName) {
|
|
254
|
+
group.get(CASE_KEY)?.setValue(caseName);
|
|
255
|
+
for (const name of Object.keys(group.controls)) {
|
|
256
|
+
if (name !== CASE_KEY)
|
|
257
|
+
group.removeControl(name);
|
|
258
|
+
}
|
|
259
|
+
const caseChildren = choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {};
|
|
260
|
+
for (const name in caseChildren) {
|
|
261
|
+
group.addControl(name, buildControl(caseChildren[name]));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Append a map entry built from `map.value` and return its committed key, or
|
|
266
|
+
* `null` when nothing was added. With no `key`, the first free `keyN`
|
|
267
|
+
* placeholder is generated (not checked against `keyPattern` — placeholders are
|
|
268
|
+
* meant to be renamed). An explicit `key` is rejected when it duplicates an
|
|
269
|
+
* existing entry or violates `keyPattern`. Rejects when `maxEntries` is reached.
|
|
270
|
+
*/
|
|
271
|
+
function addMapEntry(group, map, key) {
|
|
272
|
+
if (map.maxEntries != null && Object.keys(group.controls).length >= map.maxEntries)
|
|
273
|
+
return null;
|
|
274
|
+
let committed;
|
|
275
|
+
if (key != null) {
|
|
276
|
+
if (group.contains(key))
|
|
277
|
+
return null;
|
|
278
|
+
if (map.keyPattern && !new RegExp(map.keyPattern).test(key))
|
|
279
|
+
return null;
|
|
280
|
+
committed = key;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
let n = Object.keys(group.controls).length + 1;
|
|
284
|
+
committed = `key${n}`;
|
|
285
|
+
while (group.contains(committed))
|
|
286
|
+
committed = `key${++n}`;
|
|
287
|
+
}
|
|
288
|
+
group.addControl(committed, buildControl(map.value));
|
|
289
|
+
return committed;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Rename entry `oldKey` to `newKey.trim()`, preserving the control instance
|
|
293
|
+
* (remove + re-add, so the value survives). Returns whether the rename was
|
|
294
|
+
* committed: an empty, unchanged, duplicate, or `keyPattern`-violating key is a
|
|
295
|
+
* no-op, leaving the entry under its current name.
|
|
296
|
+
*/
|
|
297
|
+
function renameMapEntry(group, map, oldKey, newKey) {
|
|
298
|
+
const committed = newKey.trim();
|
|
299
|
+
if (!committed || committed === oldKey || group.contains(committed))
|
|
300
|
+
return false;
|
|
301
|
+
if (map.keyPattern && !new RegExp(map.keyPattern).test(committed))
|
|
302
|
+
return false;
|
|
303
|
+
const control = group.get(oldKey);
|
|
304
|
+
if (!control)
|
|
305
|
+
return false;
|
|
306
|
+
group.removeControl(oldKey);
|
|
307
|
+
group.addControl(committed, control);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
/** Remove entry `key` unless the map is at `minEntries`. Returns whether it was removed. */
|
|
311
|
+
function removeMapEntry(group, map, key) {
|
|
312
|
+
if (!group.contains(key))
|
|
313
|
+
return false;
|
|
314
|
+
if (map.minEntries != null && Object.keys(group.controls).length <= map.minEntries)
|
|
315
|
+
return false;
|
|
316
|
+
group.removeControl(key);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build the FormGroup for a map: one control per entry, keyed by the entry key,
|
|
321
|
+
* each built from the map's shared `value` schema. Because the entry keys are the
|
|
322
|
+
* control names, `getRawValue()` is the map object directly. Empty when no
|
|
323
|
+
* initial object is supplied; the renderer adds/removes/renames entries.
|
|
324
|
+
*/
|
|
325
|
+
function buildMapControl(map, initial) {
|
|
326
|
+
const controls = {};
|
|
327
|
+
const source = initial && typeof initial === 'object' && !Array.isArray(initial) ? initial : {};
|
|
328
|
+
for (const key of Object.keys(source)) {
|
|
329
|
+
controls[key] = buildControl(map.value, source[key]);
|
|
330
|
+
}
|
|
331
|
+
return new FormGroup(controls);
|
|
332
|
+
}
|
|
112
333
|
function buildControl(node, initial) {
|
|
113
334
|
if (isLeaf(node)) {
|
|
114
335
|
return buildLeafControl(node, initial);
|
|
@@ -116,6 +337,9 @@ function buildControl(node, initial) {
|
|
|
116
337
|
if (isChoice(node)) {
|
|
117
338
|
return buildChoiceControl(node, initial ? initial : null);
|
|
118
339
|
}
|
|
340
|
+
if (isMap(node)) {
|
|
341
|
+
return buildMapControl(node, initial ? initial : null);
|
|
342
|
+
}
|
|
119
343
|
if (isLeafList(node)) {
|
|
120
344
|
return buildLeafListControl(node, initial !== null
|
|
121
345
|
? initial
|
|
@@ -144,17 +368,23 @@ function buildControl(node, initial) {
|
|
|
144
368
|
* names and value types.
|
|
145
369
|
*/
|
|
146
370
|
/**
|
|
147
|
-
* Remove presence
|
|
148
|
-
* built form (and from `form.value`) until the user toggles them on.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
371
|
+
* Remove presence nodes that have no initial value so they are absent from the
|
|
372
|
+
* built form (and from `form.value`) until the user toggles them on. Applies to
|
|
373
|
+
* presence groups, leaves, maps, and choices. Runs on the fully-built, attached
|
|
374
|
+
* tree. `removeControl` is used rather than `disable` because a disabled control
|
|
375
|
+
* is still included in `FormGroup.value`.
|
|
151
376
|
*/
|
|
152
377
|
function applyPresence(group, schema, initial) {
|
|
153
378
|
for (const key in schema.children) {
|
|
154
379
|
const child = schema.children[key];
|
|
380
|
+
const childInitial = initial?.[key];
|
|
381
|
+
if (child.kind === 'leaf' || child.kind === 'map' || child.kind === 'choice') {
|
|
382
|
+
if (child.presence && childInitial == null)
|
|
383
|
+
group.removeControl(key);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
155
386
|
if (child.kind !== 'nodeGroup')
|
|
156
387
|
continue;
|
|
157
|
-
const childInitial = initial?.[key];
|
|
158
388
|
if (child.presence && childInitial == null) {
|
|
159
389
|
group.removeControl(key);
|
|
160
390
|
}
|
|
@@ -213,19 +443,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
213
443
|
}] } });
|
|
214
444
|
|
|
215
445
|
class LeafRendererComponent {
|
|
446
|
+
elementRef;
|
|
216
447
|
leaf_;
|
|
217
448
|
control = new FormControl();
|
|
218
449
|
initialValue;
|
|
219
450
|
removable = false;
|
|
220
451
|
editable = true;
|
|
452
|
+
/** Focus the field once rendered — for fields the user just added (e.g. an enabled presence leaf). */
|
|
453
|
+
autofocus = false;
|
|
221
454
|
remove = new EventEmitter();
|
|
455
|
+
constructor(elementRef) {
|
|
456
|
+
this.elementRef = elementRef;
|
|
457
|
+
}
|
|
222
458
|
ngOnInit() {
|
|
223
459
|
if (this.initialValue) {
|
|
224
460
|
this.control.patchValue(this.initialValue);
|
|
225
461
|
}
|
|
226
462
|
}
|
|
227
|
-
|
|
228
|
-
|
|
463
|
+
ngAfterViewInit() {
|
|
464
|
+
if (this.autofocus) {
|
|
465
|
+
// Deferred out of the change-detection pass: focusing flips the form
|
|
466
|
+
// field's label-float state, which would otherwise trip NG0100.
|
|
467
|
+
setTimeout(() => this.elementRef.nativeElement.querySelector('input, mat-select')?.focus());
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Whether this field accepts input: the form is editable and the leaf is not `readOnly`. */
|
|
471
|
+
get fieldEditable() {
|
|
472
|
+
return this.editable && !('name' in this.leaf_ && this.leaf_.readOnly);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* A human-readable message for the control's active validation error, or `''`
|
|
476
|
+
* when valid. `mat-form-field` only shows it once the field is in an error
|
|
477
|
+
* state (invalid and touched), so it can be bound unconditionally.
|
|
478
|
+
*/
|
|
479
|
+
get errorText() {
|
|
480
|
+
const e = this.control.errors;
|
|
481
|
+
if (!e)
|
|
482
|
+
return '';
|
|
483
|
+
const label = 'name' in this.leaf_ ? this.leaf_.label ?? this.leaf_.name : 'Value';
|
|
484
|
+
if (e['required'])
|
|
485
|
+
return `${label} is required`;
|
|
486
|
+
if (e['minlength'])
|
|
487
|
+
return `Must be at least ${e['minlength'].requiredLength} characters`;
|
|
488
|
+
if (e['maxlength'])
|
|
489
|
+
return `Must be at most ${e['maxlength'].requiredLength} characters`;
|
|
490
|
+
if (e['pattern'])
|
|
491
|
+
return `Must match ${e['pattern'].requiredPattern ?? 'the required pattern'}`;
|
|
492
|
+
if (e['email'])
|
|
493
|
+
return 'Must be a valid email address';
|
|
494
|
+
if (e['uri'])
|
|
495
|
+
return 'Must be a valid URI';
|
|
496
|
+
if (e['min'])
|
|
497
|
+
return `Must be ≥ ${e['min'].min}`;
|
|
498
|
+
if (e['max'])
|
|
499
|
+
return `Must be ≤ ${e['max'].max}`;
|
|
500
|
+
if (e['multipleOf'])
|
|
501
|
+
return `Must be a multiple of ${e['multipleOf'].multipleOf}`;
|
|
502
|
+
if (e['enum'])
|
|
503
|
+
return 'Not an allowed value';
|
|
504
|
+
return 'Invalid value';
|
|
505
|
+
}
|
|
506
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
507
|
+
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", autofocus: "autofocus" }, outputs: { remove: "remove" }, ngImport: i0, template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\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]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove\" (click)=\"remove.emit(0)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\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: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { 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: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafEnumRendererComponent, selector: "nff-leaf-enum-renderer", inputs: ["leafEnum", "initialValue", "control", "removable", "remove", "editable"] }] });
|
|
229
508
|
}
|
|
230
509
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
|
|
231
510
|
type: Component,
|
|
@@ -237,9 +516,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
237
516
|
MatSelectModule,
|
|
238
517
|
MatIconModule,
|
|
239
518
|
MatButtonModule,
|
|
519
|
+
MatTooltip,
|
|
240
520
|
LeafEnumRendererComponent,
|
|
241
|
-
], template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"
|
|
242
|
-
}], propDecorators: { leaf_: [{
|
|
521
|
+
], template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\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]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove\" (click)=\"remove.emit(0)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\n"] }]
|
|
522
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { leaf_: [{
|
|
243
523
|
type: Input
|
|
244
524
|
}], control: [{
|
|
245
525
|
type: Input
|
|
@@ -249,6 +529,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
249
529
|
type: Input
|
|
250
530
|
}], editable: [{
|
|
251
531
|
type: Input
|
|
532
|
+
}], autofocus: [{
|
|
533
|
+
type: Input
|
|
252
534
|
}], remove: [{
|
|
253
535
|
type: Output
|
|
254
536
|
}] } });
|
|
@@ -298,7 +580,7 @@ class NodeGroupListRendererComponent {
|
|
|
298
580
|
setLastEditable() {
|
|
299
581
|
const lastItem = this.items.last;
|
|
300
582
|
if (lastItem) {
|
|
301
|
-
lastItem.editable
|
|
583
|
+
lastItem.editable.set(true);
|
|
302
584
|
}
|
|
303
585
|
this.cdr.detectChanges();
|
|
304
586
|
}
|
|
@@ -315,7 +597,7 @@ class NodeGroupListRendererComponent {
|
|
|
315
597
|
}
|
|
316
598
|
}
|
|
317
599
|
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) }] });
|
|
600
|
+
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", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }] });
|
|
319
601
|
}
|
|
320
602
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
|
|
321
603
|
type: Component,
|
|
@@ -424,7 +706,7 @@ class LeafListRendererComponent {
|
|
|
424
706
|
this.formArray.push(new FormControl());
|
|
425
707
|
}
|
|
426
708
|
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:
|
|
709
|
+
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: i2$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: i4$1.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
710
|
}
|
|
429
711
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
|
|
430
712
|
type: Component,
|
|
@@ -492,25 +774,113 @@ function asFormGroup(control) {
|
|
|
492
774
|
return control;
|
|
493
775
|
}
|
|
494
776
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
777
|
+
/**
|
|
778
|
+
* Renders a {@link NodeMap}: an open, arbitrary-keyed record. Each entry is a row
|
|
779
|
+
* with an editable key and the shared value schema's control; the user can add,
|
|
780
|
+
* remove, and rename entries. The map's control is a `FormGroup` whose control
|
|
781
|
+
* names are the entry keys, so the key is edited as a *rename* (remove + re-add
|
|
782
|
+
* the same control) committed on blur — see {@link renameEntry}.
|
|
783
|
+
*/
|
|
784
|
+
class NodeMapRendererComponent {
|
|
785
|
+
nodeMap;
|
|
498
786
|
formGroup = new FormGroup({});
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
title;
|
|
503
|
-
editable = false;
|
|
504
|
-
addButtonCallback = null;
|
|
505
|
-
root = false;
|
|
787
|
+
editable = true;
|
|
788
|
+
/** Ordered view of entry keys, kept stable across renames (`addControl` appends). */
|
|
789
|
+
entryKeys = [];
|
|
506
790
|
ngOnInit() {
|
|
507
|
-
this.
|
|
508
|
-
|
|
509
|
-
|
|
791
|
+
this.entryKeys = Object.keys(this.formGroup.controls);
|
|
792
|
+
}
|
|
793
|
+
/** The value schema as a leaf (used when `value.kind === 'leaf'`). */
|
|
794
|
+
get valueLeaf() {
|
|
795
|
+
return this.nodeMap.value;
|
|
796
|
+
}
|
|
797
|
+
/** The value schema as a group (used when `value.kind === 'nodeGroup'`). */
|
|
798
|
+
get valueGroup() {
|
|
799
|
+
return this.nodeMap.value;
|
|
800
|
+
}
|
|
801
|
+
get atMax() {
|
|
802
|
+
return this.nodeMap.maxEntries != null && this.entryKeys.length >= this.nodeMap.maxEntries;
|
|
803
|
+
}
|
|
804
|
+
get atMin() {
|
|
805
|
+
return this.nodeMap.minEntries != null && this.entryKeys.length <= this.nodeMap.minEntries;
|
|
806
|
+
}
|
|
807
|
+
/** Append a new entry under a unique placeholder key. Delegates to {@link addMapEntry}. */
|
|
808
|
+
addEntry() {
|
|
809
|
+
const key = addMapEntry(this.formGroup, this.nodeMap);
|
|
810
|
+
if (key != null)
|
|
811
|
+
this.entryKeys = [...this.entryKeys, key];
|
|
812
|
+
}
|
|
813
|
+
/** Drop an entry (its control leaves the group, so it drops from the value). Delegates to {@link removeMapEntry}. */
|
|
814
|
+
removeEntry(key) {
|
|
815
|
+
if (removeMapEntry(this.formGroup, this.nodeMap, key)) {
|
|
816
|
+
this.entryKeys = this.entryKeys.filter((k) => k !== key);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Commit an edited key by renaming its control once (the value is preserved).
|
|
821
|
+
* An empty, unchanged, duplicate, or `keyPattern`-violating key is a no-op,
|
|
822
|
+
* leaving the entry under its current name. Delegates to {@link renameMapEntry}.
|
|
823
|
+
*/
|
|
824
|
+
renameEntry(oldKey, rawKey) {
|
|
825
|
+
if (renameMapEntry(this.formGroup, this.nodeMap, oldKey, rawKey)) {
|
|
826
|
+
const newKey = rawKey.trim();
|
|
827
|
+
this.entryKeys = this.entryKeys.map((k) => (k === oldKey ? newKey : k));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
asFormControl = asFormControl;
|
|
831
|
+
asFormGroup = asFormGroup;
|
|
832
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
833
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeMapRendererComponent, isStandalone: true, selector: "nff-node-map-renderer", inputs: { nodeMap: "nodeMap", formGroup: "formGroup", editable: "editable" }, ngImport: i0, template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value)\"\n >\n </mat-form-field>\n\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.get(key))\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.get(key))\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-title{font:var(--mat-sys-title-small);color:var(--mat-sys-on-surface-variant)}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;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}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i4$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatInputModule) }, { kind: "directive", type: i0.forwardRef(() => 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: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove", "editableChange"] }] });
|
|
834
|
+
}
|
|
835
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
|
|
836
|
+
type: Component,
|
|
837
|
+
args: [{ selector: 'nff-node-map-renderer', standalone: true, imports: [
|
|
838
|
+
MatButtonModule,
|
|
839
|
+
MatIconModule,
|
|
840
|
+
MatFormFieldModule,
|
|
841
|
+
MatInputModule,
|
|
842
|
+
LeafRendererComponent,
|
|
843
|
+
// node-map-renderer and dynamic-recursive-form import each other (a map value
|
|
844
|
+
// may be a group); forwardRef defers the reference to break the cycle.
|
|
845
|
+
forwardRef(() => DynamicRecursiveFormComponent),
|
|
846
|
+
], template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value)\"\n >\n </mat-form-field>\n\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.get(key))\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.get(key))\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-title{font:var(--mat-sys-title-small);color:var(--mat-sys-on-surface-variant)}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;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}\n"] }]
|
|
847
|
+
}], propDecorators: { nodeMap: [{
|
|
848
|
+
type: Input
|
|
849
|
+
}], formGroup: [{
|
|
850
|
+
type: Input
|
|
851
|
+
}], editable: [{
|
|
852
|
+
type: Input
|
|
853
|
+
}] } });
|
|
854
|
+
|
|
855
|
+
class DynamicRecursiveFormComponent {
|
|
856
|
+
/** The form-description schema to render (a root or nested `NodeGroup`). */
|
|
857
|
+
schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : []));
|
|
858
|
+
/** Optional value object to seed the form; keyed by the schema's `children` keys. */
|
|
859
|
+
initialValue = input(null, ...(ngDevMode ? [{ debugName: "initialValue" }] : []));
|
|
860
|
+
/** The reactive group this form binds to. Defaults to an empty group. */
|
|
861
|
+
formGroup = input(new FormGroup({}), ...(ngDevMode ? [{ debugName: "formGroup" }] : []));
|
|
862
|
+
/** Index of this form within a parent list, used by `addButtonCallback`. */
|
|
863
|
+
index = input(null, ...(ngDevMode ? [{ debugName: "index" }] : []));
|
|
864
|
+
/** Whether this form may be removed from a parent list (shows a remove control). */
|
|
865
|
+
removable = input(false, ...(ngDevMode ? [{ debugName: "removable" }] : []));
|
|
866
|
+
/** Emitted when the user removes this form from a parent list. */
|
|
867
|
+
remove = output();
|
|
868
|
+
/** Card/section title; falls back to the schema label or name. */
|
|
869
|
+
title = input(...(ngDevMode ? [undefined, { debugName: "title" }] : []));
|
|
870
|
+
/** Whether fields accept input. Two-way: also toggled by the built-in edit control. */
|
|
871
|
+
editable = model(false, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
872
|
+
/** Invoked with {@link index} to append a new sibling form to a parent list. */
|
|
873
|
+
addButtonCallback = input(null, ...(ngDevMode ? [{ debugName: "addButtonCallback" }] : []));
|
|
874
|
+
/** True when the schema is a root group (rendered flat, without a wrapping card). */
|
|
875
|
+
root = computed(() => this.schema().root ?? false, ...(ngDevMode ? [{ debugName: "root" }] : []));
|
|
876
|
+
ngOnInit() {
|
|
877
|
+
const initial = this.initialValue();
|
|
878
|
+
if (initial) {
|
|
879
|
+
this.formGroup().patchValue(initial);
|
|
510
880
|
}
|
|
511
881
|
}
|
|
512
882
|
get nodeGroupChildrenList() {
|
|
513
|
-
const children = this.schema.children ?? {};
|
|
883
|
+
const children = this.schema().children ?? {};
|
|
514
884
|
return Object.entries(children).map(([key, value]) => ({
|
|
515
885
|
key,
|
|
516
886
|
value: value,
|
|
@@ -524,13 +894,51 @@ class DynamicRecursiveFormComponent {
|
|
|
524
894
|
* group from `form.value`; adding it rebuilds the sub-group from its schema.
|
|
525
895
|
*/
|
|
526
896
|
togglePresence(key, schema, present) {
|
|
897
|
+
const group = this.formGroup();
|
|
898
|
+
if (present) {
|
|
899
|
+
if (!group.get(key)) {
|
|
900
|
+
group.addControl(key, buildFormFromSchema(schema));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else if (group.get(key)) {
|
|
904
|
+
group.removeControl(key);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** The key of the presence leaf the user just enabled; its field grabs focus when rendered. */
|
|
908
|
+
presenceFocusKey = null;
|
|
909
|
+
/**
|
|
910
|
+
* Add or remove a presence leaf's control on this form. Removing it drops the
|
|
911
|
+
* leaf from `form.value`; adding it rebuilds the control from its schema and
|
|
912
|
+
* focuses the rendered field.
|
|
913
|
+
*/
|
|
914
|
+
toggleLeafPresence(key, schema, present) {
|
|
915
|
+
const group = this.formGroup();
|
|
527
916
|
if (present) {
|
|
528
|
-
if (!
|
|
529
|
-
|
|
917
|
+
if (!group.get(key)) {
|
|
918
|
+
group.addControl(key, buildControl(schema));
|
|
919
|
+
this.presenceFocusKey = key;
|
|
530
920
|
}
|
|
531
921
|
}
|
|
532
|
-
else if (
|
|
533
|
-
|
|
922
|
+
else if (group.get(key)) {
|
|
923
|
+
group.removeControl(key);
|
|
924
|
+
if (this.presenceFocusKey === key)
|
|
925
|
+
this.presenceFocusKey = null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Add or remove a presence map's or choice's control on this form. Removing it
|
|
930
|
+
* drops the node from `form.value`; adding it rebuilds the control (an empty
|
|
931
|
+
* map, or a choice group holding `__case`) from its schema.
|
|
932
|
+
*/
|
|
933
|
+
toggleNodePresence(key, schema, present) {
|
|
934
|
+
const group = this.formGroup();
|
|
935
|
+
if (present) {
|
|
936
|
+
if (!group.get(key)) {
|
|
937
|
+
group.addControl(key, buildControl(schema));
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
else if (group.get(key)) {
|
|
941
|
+
group.removeControl(key);
|
|
534
942
|
}
|
|
535
943
|
}
|
|
536
944
|
CASE_KEY = CASE_KEY;
|
|
@@ -539,41 +947,49 @@ class DynamicRecursiveFormComponent {
|
|
|
539
947
|
}
|
|
540
948
|
/** The active case name of a choice control, or null if none is selected. */
|
|
541
949
|
activeCase(key) {
|
|
542
|
-
return this.formGroup.get(key)?.get(CASE_KEY)?.value ?? null;
|
|
950
|
+
return this.formGroup().get(key)?.get(CASE_KEY)?.value ?? null;
|
|
543
951
|
}
|
|
544
952
|
/** A synthetic flattened NodeGroup used to render the active case's fields against the choice's FormGroup. */
|
|
545
953
|
caseAsGroup(choice, caseName) {
|
|
546
954
|
return {
|
|
547
955
|
kind: 'nodeGroup',
|
|
548
956
|
name: choice.name,
|
|
549
|
-
children: choice.cases[caseName]
|
|
957
|
+
children: choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {},
|
|
550
958
|
appearance: { flatten: true },
|
|
551
959
|
};
|
|
552
960
|
}
|
|
553
|
-
/**
|
|
961
|
+
/** The display label for a case: the schema's `caseLabels` entry, else the case name. */
|
|
962
|
+
caseLabel(choice, caseName) {
|
|
963
|
+
return choice.caseLabels?.[caseName] ?? caseName;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* A copy of a group flagged to render its fields inline (no inner panel). Used
|
|
967
|
+
* for a presence group's body, whose container is the presence panel itself, so
|
|
968
|
+
* the group's own section panel would be a redundant second box.
|
|
969
|
+
*/
|
|
970
|
+
flatGroup(group) {
|
|
971
|
+
return { ...group, appearance: { ...group.appearance, flatten: true } };
|
|
972
|
+
}
|
|
973
|
+
/** Swap a choice's field controls when the selected case changes. Delegates to {@link switchChoiceCase}. */
|
|
554
974
|
switchCase(key, choice, caseName) {
|
|
555
|
-
|
|
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
|
-
}
|
|
975
|
+
switchChoiceCase(this.formGroup().get(key), choice, caseName);
|
|
564
976
|
}
|
|
565
977
|
asFormGroup = asFormGroup;
|
|
566
978
|
asFormArray = asFormArray;
|
|
567
979
|
asFormControl = asFormControl;
|
|
568
980
|
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"] }] });
|
|
981
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, 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' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else 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]=\"flatGroup(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 <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</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\">{{ caseLabel(child, 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 </mat-expansion-panel>\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 @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\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-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\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()!); $event.stopPropagation()\">\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 </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\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.set(!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' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else 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 == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</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\">{{ caseLabel(child, 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 </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\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]=\"flatGroup(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 <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 }\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}.node-section{width:100%;margin:4px 0}.section-actions{justify-content:flex-end;align-items:center;gap:4px}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top: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: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "minItems", "maxItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatus), selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i0.forwardRef(() => i1.FormControlName), selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatAccordion), selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelDescription), selector: "mat-panel-description" }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i4$1.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: i0.forwardRef(() => i4$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatCardModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatCheckboxModule) }, { kind: "component", type: i0.forwardRef(() => 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: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatSelectModule) }, { kind: "component", type: i0.forwardRef(() => 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: i0.forwardRef(() => i3.MatOption), selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
570
982
|
}
|
|
571
983
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
|
|
572
984
|
type: Component,
|
|
573
985
|
args: [{ imports: [
|
|
574
986
|
LeafRendererComponent,
|
|
575
|
-
NodeGroupListRendererComponent,
|
|
576
987
|
LeafListRendererComponent,
|
|
988
|
+
// node-group-list-renderer and node-map-renderer import this component back
|
|
989
|
+
// (list items and map values may be groups); forwardRef tolerates either
|
|
990
|
+
// module-evaluation order for the cycle.
|
|
991
|
+
forwardRef(() => NodeGroupListRendererComponent),
|
|
992
|
+
forwardRef(() => NodeMapRendererComponent),
|
|
577
993
|
ReactiveFormsModule,
|
|
578
994
|
MatExpansionModule,
|
|
579
995
|
MatIconModule,
|
|
@@ -584,52 +1000,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
584
1000
|
MatFormFieldModule,
|
|
585
1001
|
MatSelectModule,
|
|
586
1002
|
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
|
-
}] } });
|
|
1003
|
+
], 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' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else 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]=\"flatGroup(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 <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</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\">{{ caseLabel(child, 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 </mat-expansion-panel>\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 @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\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-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\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()!); $event.stopPropagation()\">\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 </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\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.set(!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' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else 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 == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</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\">{{ caseLabel(child, 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 </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\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]=\"flatGroup(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 <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 }\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}.node-section{width:100%;margin:4px 0}.section-actions{justify-content:flex-end;align-items:center;gap:4px}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top: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"] }]
|
|
1004
|
+
}], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }, { type: i0.Output, args: ["editableChange"] }], addButtonCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonCallback", required: false }] }] } });
|
|
608
1005
|
|
|
609
1006
|
/**
|
|
610
|
-
* A tree/detail editor for a schema-built form: the structure (
|
|
611
|
-
*
|
|
612
|
-
* fields for editing on the right. A `+` on a list node adds an entry; a
|
|
613
|
-
* button on each item removes it.
|
|
614
|
-
*
|
|
1007
|
+
* A tree/detail editor for a schema-built form: the structure (groups, lists,
|
|
1008
|
+
* maps, choices) is a tree on the left, and selecting a node shows that node's
|
|
1009
|
+
* fields for editing on the right. A `+` on a list or map node adds an entry; a
|
|
1010
|
+
* delete button on each item removes it. Absent optional (presence) children are
|
|
1011
|
+
* offered by a "+ Optional field" menu row at the end of their parent's
|
|
1012
|
+
* children; present ones carry a delete button that returns them to the menu.
|
|
1013
|
+
*
|
|
1014
|
+
* The component draws no outer container — only a divider between the tree and
|
|
1015
|
+
* detail panes — so the embedding client owns the surrounding chrome. The tree
|
|
1016
|
+
* is built once from the `schema`/`formGroup` provided at initialization.
|
|
1017
|
+
* An alternative to the all-in-one {@link DynamicRecursiveFormComponent} for
|
|
1018
|
+
* large configs.
|
|
615
1019
|
*/
|
|
616
1020
|
class ConfigEditorComponent {
|
|
617
|
-
schema
|
|
618
|
-
|
|
619
|
-
|
|
1021
|
+
/** The form-description schema whose structure the tree renders. */
|
|
1022
|
+
schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : []));
|
|
1023
|
+
/** The schema-built reactive group the editor binds to and mutates. */
|
|
1024
|
+
formGroup = input.required(...(ngDevMode ? [{ debugName: "formGroup" }] : []));
|
|
1025
|
+
/** Whether fields accept input and structural controls (add/remove/menus) show. */
|
|
1026
|
+
editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
620
1027
|
root;
|
|
621
1028
|
selected = null;
|
|
622
1029
|
expanded = new Set();
|
|
623
1030
|
nextId = 0;
|
|
624
1031
|
ngOnInit() {
|
|
625
|
-
|
|
1032
|
+
const schema = this.schema();
|
|
1033
|
+
this.root = this.buildTree(schema, this.formGroup(), schema.label ?? schema.name);
|
|
626
1034
|
this.expanded.add(this.root.id);
|
|
627
1035
|
this.select(this.root);
|
|
628
1036
|
}
|
|
629
1037
|
select(node) {
|
|
630
1038
|
this.selected = node;
|
|
631
|
-
// Reveal the node's
|
|
632
|
-
|
|
1039
|
+
// Reveal the node's rows in the tree as it opens on the right. Navigating
|
|
1040
|
+
// also retires any pending just-added-leaf focus request.
|
|
1041
|
+
this.focusLeafKey = null;
|
|
1042
|
+
if (this.hasExpandableContent(node))
|
|
633
1043
|
this.expanded.add(node.id);
|
|
634
1044
|
}
|
|
635
1045
|
/** Select a child (list item or sub-group) from the detail pane, keeping its parent expanded in the tree. */
|
|
@@ -662,6 +1072,18 @@ class ConfigEditorComponent {
|
|
|
662
1072
|
else
|
|
663
1073
|
this.expanded.add(node.id);
|
|
664
1074
|
}
|
|
1075
|
+
/** Whether the row shows an expand twisty: it has child rows to reveal (children or an optionals menu row). */
|
|
1076
|
+
hasExpandableContent(node) {
|
|
1077
|
+
return node.children.length > 0 || (this.editable() && !!node.optionals?.length);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Whether the node's form subtree holds a validation error. Group/choice, list,
|
|
1081
|
+
* and map nodes each check their backing control; `invalid` aggregates over
|
|
1082
|
+
* descendants, so an error anywhere below lights every ancestor row.
|
|
1083
|
+
*/
|
|
1084
|
+
hasError(node) {
|
|
1085
|
+
return !!(node.group?.invalid || node.list?.array.invalid || node.map?.group.invalid);
|
|
1086
|
+
}
|
|
665
1087
|
/** Append a new item to a list node's FormArray and to the tree, then select it. */
|
|
666
1088
|
addItem(listNode) {
|
|
667
1089
|
const list = listNode.list;
|
|
@@ -685,40 +1107,130 @@ class ConfigEditorComponent {
|
|
|
685
1107
|
listNode.list.array.removeAt(item.removable.index);
|
|
686
1108
|
listNode.children.splice(listNode.children.indexOf(item), 1);
|
|
687
1109
|
this.renumber(listNode);
|
|
688
|
-
|
|
689
|
-
this.select(listNode);
|
|
1110
|
+
this.reselectIfOrphaned(listNode);
|
|
690
1111
|
}
|
|
691
|
-
/**
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1112
|
+
/** The key of the optional leaf the user just added; its detail-pane field grabs focus when rendered. */
|
|
1113
|
+
focusLeafKey = null;
|
|
1114
|
+
/** Add an absent optional child from the menu: build its control, place it in the tree, and select it. */
|
|
1115
|
+
addOptional(node, entry) {
|
|
1116
|
+
if (!node.group || node.group.get(entry.key))
|
|
695
1117
|
return;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
node.leafLists = built.leafLists;
|
|
704
|
-
node.group = group;
|
|
705
|
-
}
|
|
706
|
-
node.present = true;
|
|
1118
|
+
const control = entry.schema.kind === 'nodeGroup'
|
|
1119
|
+
? buildFormFromSchema(entry.schema)
|
|
1120
|
+
: buildControl(entry.schema);
|
|
1121
|
+
node.group.addControl(entry.key, control);
|
|
1122
|
+
node.optionals = node.optionals?.filter((o) => o !== entry);
|
|
1123
|
+
if (entry.schema.kind === 'leaf') {
|
|
1124
|
+
node.leaves.push({ key: entry.key, node: entry.schema, optional: entry });
|
|
707
1125
|
this.select(node);
|
|
1126
|
+
this.focusLeafKey = entry.key;
|
|
1127
|
+
return;
|
|
708
1128
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1129
|
+
const child = this.buildChildNode(entry.schema, control, entry.label);
|
|
1130
|
+
if (!child)
|
|
1131
|
+
return;
|
|
1132
|
+
child.presenceRemovable = { entry };
|
|
1133
|
+
node.children.push(child);
|
|
1134
|
+
this.expanded.add(node.id);
|
|
1135
|
+
this.select(child);
|
|
1136
|
+
}
|
|
1137
|
+
/** Remove a present optional child node, returning its entry to the parent's menu. */
|
|
1138
|
+
removeOptional(parent, node) {
|
|
1139
|
+
const entry = node.presenceRemovable?.entry;
|
|
1140
|
+
if (!entry || !parent.group)
|
|
1141
|
+
return;
|
|
1142
|
+
parent.group.removeControl(entry.key);
|
|
1143
|
+
parent.children.splice(parent.children.indexOf(node), 1);
|
|
1144
|
+
this.reinsertOptional(parent, entry);
|
|
1145
|
+
this.reselectIfOrphaned(parent);
|
|
1146
|
+
}
|
|
1147
|
+
/** Remove a present optional leaf from the detail pane, returning its entry to the menu. */
|
|
1148
|
+
removeOptionalLeaf(node, leaf) {
|
|
1149
|
+
if (!leaf.optional || !node.group)
|
|
1150
|
+
return;
|
|
1151
|
+
node.group.removeControl(leaf.key);
|
|
1152
|
+
node.leaves = node.leaves.filter((l) => l !== leaf);
|
|
1153
|
+
this.reinsertOptional(node, leaf.optional);
|
|
1154
|
+
}
|
|
1155
|
+
/** The active case name of a choice node, or null when none is selected. */
|
|
1156
|
+
activeCase(node) {
|
|
1157
|
+
return node.choice?.group.get(CASE_KEY)?.value ?? null;
|
|
1158
|
+
}
|
|
1159
|
+
/** The display label of a choice node's active case, or null when no case is selected. */
|
|
1160
|
+
activeCaseLabel(node) {
|
|
1161
|
+
const c = node.choice;
|
|
1162
|
+
const active = this.activeCase(node);
|
|
1163
|
+
return c && active ? this.caseLabel(c.schema, active) : null;
|
|
1164
|
+
}
|
|
1165
|
+
/** The display label for a case: the schema's `caseLabels` entry, else the case name. */
|
|
1166
|
+
caseLabel(choice, caseName) {
|
|
1167
|
+
return choice.caseLabels?.[caseName] ?? caseName;
|
|
1168
|
+
}
|
|
1169
|
+
/** Switch a choice node's active case: swap the group's controls and rebuild the subtree in place. */
|
|
1170
|
+
switchTreeCase(node, caseName) {
|
|
1171
|
+
const c = node.choice;
|
|
1172
|
+
if (!c)
|
|
1173
|
+
return;
|
|
1174
|
+
switchChoiceCase(c.group, c.schema, caseName);
|
|
1175
|
+
const rebuilt = this.buildTree(this.caseAsGroup(c.schema, caseName), c.group, node.label);
|
|
1176
|
+
node.children = rebuilt.children;
|
|
1177
|
+
node.leaves = rebuilt.leaves;
|
|
1178
|
+
node.leafLists = rebuilt.leafLists;
|
|
1179
|
+
node.optionals = rebuilt.optionals;
|
|
1180
|
+
this.expanded.add(node.id);
|
|
1181
|
+
this.select(node);
|
|
1182
|
+
}
|
|
1183
|
+
/** Append a new entry to a complex map node under a generated unique key, then select it. */
|
|
1184
|
+
addTreeMapEntry(mapNode) {
|
|
1185
|
+
const m = mapNode.map;
|
|
1186
|
+
if (!m)
|
|
1187
|
+
return;
|
|
1188
|
+
const key = addMapEntry(m.group, m.schema);
|
|
1189
|
+
if (key == null)
|
|
1190
|
+
return;
|
|
1191
|
+
const child = this.buildChildNode(m.schema.value, m.group.get(key), key);
|
|
1192
|
+
if (!child)
|
|
1193
|
+
return;
|
|
1194
|
+
child.mapEntry = { mapGroup: m.group, mapSchema: m.schema, key };
|
|
1195
|
+
mapNode.children.push(child);
|
|
1196
|
+
this.expanded.add(mapNode.id);
|
|
1197
|
+
this.select(child);
|
|
1198
|
+
}
|
|
1199
|
+
/** Remove a complex map entry (down to `minEntries`) from the form group and the tree. */
|
|
1200
|
+
removeTreeMapEntry(mapNode, entryNode) {
|
|
1201
|
+
const m = mapNode.map;
|
|
1202
|
+
const e = entryNode.mapEntry;
|
|
1203
|
+
if (!m || !e || !removeMapEntry(m.group, m.schema, e.key))
|
|
1204
|
+
return;
|
|
1205
|
+
mapNode.children.splice(mapNode.children.indexOf(entryNode), 1);
|
|
1206
|
+
this.reselectIfOrphaned(mapNode);
|
|
1207
|
+
}
|
|
1208
|
+
/** Commit a rename-on-blur of a map entry's key; on success the node is relabeled. */
|
|
1209
|
+
renameTreeMapEntry(entryNode, rawKey) {
|
|
1210
|
+
const e = entryNode.mapEntry;
|
|
1211
|
+
if (!e)
|
|
1212
|
+
return;
|
|
1213
|
+
if (renameMapEntry(e.mapGroup, e.mapSchema, e.key, rawKey)) {
|
|
1214
|
+
const committed = rawKey.trim();
|
|
1215
|
+
e.key = committed;
|
|
1216
|
+
entryNode.label = committed;
|
|
716
1217
|
}
|
|
717
1218
|
}
|
|
1219
|
+
/** Whether a map node is at `maxEntries` (the add control is hidden). */
|
|
1220
|
+
mapAtMax(node) {
|
|
1221
|
+
const m = node?.map;
|
|
1222
|
+
return !!m && m.schema.maxEntries != null && Object.keys(m.group.controls).length >= m.schema.maxEntries;
|
|
1223
|
+
}
|
|
1224
|
+
/** Whether a map node is at `minEntries` (entry remove controls are hidden). */
|
|
1225
|
+
mapAtMin(node) {
|
|
1226
|
+
const m = node?.map;
|
|
1227
|
+
return !!m && m.schema.minEntries != null && Object.keys(m.group.controls).length <= m.schema.minEntries;
|
|
1228
|
+
}
|
|
718
1229
|
asFormControl = asFormControl;
|
|
719
1230
|
asFormArray = asFormArray;
|
|
720
|
-
|
|
721
|
-
|
|
1231
|
+
CASE_KEY = CASE_KEY;
|
|
1232
|
+
objectKeys(obj) {
|
|
1233
|
+
return Object.keys(obj);
|
|
722
1234
|
}
|
|
723
1235
|
/** Re-index and re-label a list node's item children (just "#n") after add/remove. */
|
|
724
1236
|
renumber(listNode) {
|
|
@@ -728,65 +1240,142 @@ class ConfigEditorComponent {
|
|
|
728
1240
|
child.label = `#${i + 1}`;
|
|
729
1241
|
});
|
|
730
1242
|
}
|
|
1243
|
+
/** Select `fallback` when the current selection is no longer reachable in the tree. */
|
|
1244
|
+
reselectIfOrphaned(fallback) {
|
|
1245
|
+
if (this.selected && this.pathTo(this.selected).length === 0)
|
|
1246
|
+
this.select(fallback);
|
|
1247
|
+
}
|
|
1248
|
+
/** Re-insert a removed optional's entry into the node's menu, keeping schema order. */
|
|
1249
|
+
reinsertOptional(node, entry) {
|
|
1250
|
+
const list = (node.optionals ??= []);
|
|
1251
|
+
const at = list.findIndex((o) => o.order > entry.order);
|
|
1252
|
+
if (at === -1)
|
|
1253
|
+
list.push(entry);
|
|
1254
|
+
else
|
|
1255
|
+
list.splice(at, 0, entry);
|
|
1256
|
+
}
|
|
1257
|
+
/** Synthetic group over a case's normalized fields, so a case body builds like any group. */
|
|
1258
|
+
caseAsGroup(choice, caseName) {
|
|
1259
|
+
return {
|
|
1260
|
+
kind: 'nodeGroup',
|
|
1261
|
+
name: choice.name,
|
|
1262
|
+
children: choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {},
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
731
1265
|
buildTree(schema, group, label) {
|
|
732
1266
|
const leaves = [];
|
|
733
1267
|
const leafLists = [];
|
|
734
1268
|
const children = [];
|
|
735
|
-
|
|
1269
|
+
const optionals = [];
|
|
1270
|
+
const keys = Object.keys(schema.children);
|
|
1271
|
+
keys.forEach((key, order) => {
|
|
736
1272
|
const child = schema.children[key];
|
|
1273
|
+
const control = group.get(key);
|
|
1274
|
+
const entry = { key, schema: child, label: this.labelOf(child, key), order };
|
|
737
1275
|
if (child.kind === 'leaf') {
|
|
738
|
-
|
|
1276
|
+
if (child.presence && !control)
|
|
1277
|
+
optionals.push(entry);
|
|
1278
|
+
else if (control)
|
|
1279
|
+
leaves.push({ key, node: child, optional: child.presence ? entry : undefined });
|
|
1280
|
+
return;
|
|
739
1281
|
}
|
|
740
|
-
|
|
1282
|
+
if (child.kind === 'leafList') {
|
|
741
1283
|
leafLists.push({ key, node: child });
|
|
1284
|
+
return;
|
|
742
1285
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
}
|
|
1286
|
+
const presence = child.kind !== 'nodeGroupList' && child.presence;
|
|
1287
|
+
if (presence && !control) {
|
|
1288
|
+
optionals.push(entry);
|
|
1289
|
+
return;
|
|
757
1290
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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.
|
|
1291
|
+
const node = this.buildChildNode(child, control, entry.label);
|
|
1292
|
+
if (!node)
|
|
1293
|
+
return;
|
|
1294
|
+
if (presence)
|
|
1295
|
+
node.presenceRemovable = { entry };
|
|
1296
|
+
children.push(node);
|
|
1297
|
+
});
|
|
1298
|
+
const node = { id: String(this.nextId++), label, children, leaves, leafLists, group };
|
|
1299
|
+
if (optionals.length)
|
|
1300
|
+
node.optionals = optionals;
|
|
1301
|
+
return node;
|
|
1302
|
+
}
|
|
1303
|
+
/** Build the tree node for a single non-leaf child, dispatching on its kind. Null when its control is missing. */
|
|
1304
|
+
buildChildNode(schema, control, label) {
|
|
1305
|
+
if (schema.kind === 'nodeGroup') {
|
|
1306
|
+
return control instanceof FormGroup ? this.buildTree(schema, control, label) : null;
|
|
785
1307
|
}
|
|
786
|
-
|
|
1308
|
+
if (schema.kind === 'nodeGroupList') {
|
|
1309
|
+
const array = control;
|
|
1310
|
+
const itemLabel = schema.type.label ?? schema.type.name;
|
|
1311
|
+
const items = array instanceof FormArray
|
|
1312
|
+
? array.controls
|
|
1313
|
+
.filter((c) => c instanceof FormGroup)
|
|
1314
|
+
.map((item, i) => {
|
|
1315
|
+
// Just "#n": the item sits under its list node, so repeating the
|
|
1316
|
+
// item name (e.g. "Interface #1") only echoes the parent.
|
|
1317
|
+
const node = this.buildTree(schema.type, item, `#${i + 1}`);
|
|
1318
|
+
node.removable = { array, index: i };
|
|
1319
|
+
return node;
|
|
1320
|
+
})
|
|
1321
|
+
: [];
|
|
1322
|
+
return {
|
|
1323
|
+
id: String(this.nextId++),
|
|
1324
|
+
label,
|
|
1325
|
+
children: items,
|
|
1326
|
+
leaves: [],
|
|
1327
|
+
leafLists: [],
|
|
1328
|
+
group: null,
|
|
1329
|
+
list: array instanceof FormArray
|
|
1330
|
+
? { array, itemSchema: schema.type, itemLabel, minItems: schema.minItems ?? 0 }
|
|
1331
|
+
: undefined,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
if (schema.kind === 'choice') {
|
|
1335
|
+
if (!(control instanceof FormGroup))
|
|
1336
|
+
return null;
|
|
1337
|
+
const active = control.get(CASE_KEY)?.value;
|
|
1338
|
+
const node = active && schema.cases[active]
|
|
1339
|
+
? this.buildTree(this.caseAsGroup(schema, active), control, label)
|
|
1340
|
+
: { id: String(this.nextId++), label, children: [], leaves: [], leafLists: [], group: control };
|
|
1341
|
+
node.choice = { schema, group: control };
|
|
1342
|
+
return node;
|
|
1343
|
+
}
|
|
1344
|
+
if (schema.kind === 'map') {
|
|
1345
|
+
if (!(control instanceof FormGroup))
|
|
1346
|
+
return null;
|
|
1347
|
+
const complex = schema.value.kind === 'nodeGroup' ||
|
|
1348
|
+
schema.value.kind === 'choice' ||
|
|
1349
|
+
schema.value.kind === 'map' ||
|
|
1350
|
+
schema.value.kind === 'nodeGroupList';
|
|
1351
|
+
const entries = complex
|
|
1352
|
+
? Object.keys(control.controls)
|
|
1353
|
+
.map((key) => {
|
|
1354
|
+
const entryNode = this.buildChildNode(schema.value, control.get(key), key);
|
|
1355
|
+
if (entryNode)
|
|
1356
|
+
entryNode.mapEntry = { mapGroup: control, mapSchema: schema, key };
|
|
1357
|
+
return entryNode;
|
|
1358
|
+
})
|
|
1359
|
+
.filter((n) => n !== null)
|
|
1360
|
+
: [];
|
|
1361
|
+
return {
|
|
1362
|
+
id: String(this.nextId++),
|
|
1363
|
+
label,
|
|
1364
|
+
children: entries,
|
|
1365
|
+
leaves: [],
|
|
1366
|
+
leafLists: [],
|
|
1367
|
+
group: null,
|
|
1368
|
+
map: { schema, group: control, complex },
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
/** A node's display label: its schema `label`, else its record key. */
|
|
1374
|
+
labelOf(node, key) {
|
|
1375
|
+
return ('label' in node ? node.label : undefined) ?? key;
|
|
787
1376
|
}
|
|
788
1377
|
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"] }] });
|
|
1378
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: true, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null } }, 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 [class.error]=\"hasError(node)\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (hasExpandableContent(node)) {\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 <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\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.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n (click)=\"addTreeMapEntry(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 @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(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 @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\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.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(selected)\" (selectionChange)=\"switchTreeCase(selected, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(selected) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (selected.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(selected, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (selected.map && !selected.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"selected.map.schema\"\n [formGroup]=\"selected.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (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 [removable]=\"!!leaf.optional\"\n [autofocus]=\"leaf.key === focusLeafKey\"\n (remove)=\"removeOptionalLeaf(selected, leaf)\"\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 (editable() && selected.optionals?.length) {\n <div class=\"detail-optional\">\n <button matButton [matMenuTriggerFor]=\"detailOptionalsMenu\">\n <mat-icon>add</mat-icon> Optional field\n </button>\n <mat-menu #detailOptionalsMenu=\"matMenu\">\n @for (opt of selected.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(selected, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n </div>\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\" [matTooltip]=\"child.label\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n }\n\n @if (selected.list; as 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\" [matTooltip]=\"item.label\" (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 {{ 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 {{ list.itemLabel }}\n </button>\n </div>\n }\n }\n\n @if (selected.map && selected.map.complex) {\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\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && !mapAtMin(selected)) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No entries yet.</p>\n }\n @if (editable() && !mapAtMax(selected)) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addTreeMapEntry(selected)\">\n <mat-icon>add</mat-icon> Add entry\n </button>\n </div>\n }\n }\n\n @if (\n !selected.choice &&\n !selected.mapEntry &&\n !selected.map &&\n !selected.list &&\n !selected.leaves.length &&\n !selected.leafLists.length &&\n !selected.children.length &&\n !(editable() && selected.optionals?.length)\n ) {\n <p class=\"empty\">This node has no fields.</p>\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: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));padding:4px 12px 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}.tree-row.error .tree-label{color:var(--mat-sys-error, #b3261e)}.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}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55))}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary, #6750a4)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;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}.case-select,.key-field{display:block;max-width:320px;margin-bottom:8px}.detail-optional{display:flex;justify-content:flex-start}.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: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4$1.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: i4$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { 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: 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: 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"] }, { kind: "component", type: LeafRendererComponent, selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: LeafListRendererComponent, selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "component", type: NodeMapRendererComponent, selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }] });
|
|
790
1379
|
}
|
|
791
1380
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
|
|
792
1381
|
type: Component,
|
|
@@ -795,20 +1384,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
795
1384
|
NgTemplateOutlet,
|
|
796
1385
|
MatIconModule,
|
|
797
1386
|
MatButtonModule,
|
|
798
|
-
|
|
1387
|
+
MatMenuModule,
|
|
1388
|
+
MatFormFieldModule,
|
|
1389
|
+
MatInputModule,
|
|
1390
|
+
MatSelectModule,
|
|
799
1391
|
MatTooltip,
|
|
800
1392
|
LeafRendererComponent,
|
|
801
1393
|
LeafListRendererComponent,
|
|
802
|
-
|
|
803
|
-
}], propDecorators: { schema: [{
|
|
804
|
-
|
|
805
|
-
args: [{ required: true }]
|
|
806
|
-
}], formGroup: [{
|
|
807
|
-
type: Input,
|
|
808
|
-
args: [{ required: true }]
|
|
809
|
-
}], editable: [{
|
|
810
|
-
type: Input
|
|
811
|
-
}] } });
|
|
1394
|
+
NodeMapRendererComponent,
|
|
1395
|
+
], 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 [class.error]=\"hasError(node)\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (hasExpandableContent(node)) {\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 <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\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.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n (click)=\"addTreeMapEntry(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 @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(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 @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\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.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(selected)\" (selectionChange)=\"switchTreeCase(selected, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(selected) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (selected.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(selected, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (selected.map && !selected.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"selected.map.schema\"\n [formGroup]=\"selected.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (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 [removable]=\"!!leaf.optional\"\n [autofocus]=\"leaf.key === focusLeafKey\"\n (remove)=\"removeOptionalLeaf(selected, leaf)\"\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 (editable() && selected.optionals?.length) {\n <div class=\"detail-optional\">\n <button matButton [matMenuTriggerFor]=\"detailOptionalsMenu\">\n <mat-icon>add</mat-icon> Optional field\n </button>\n <mat-menu #detailOptionalsMenu=\"matMenu\">\n @for (opt of selected.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(selected, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n </div>\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\" [matTooltip]=\"child.label\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n }\n\n @if (selected.list; as 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\" [matTooltip]=\"item.label\" (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 {{ 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 {{ list.itemLabel }}\n </button>\n </div>\n }\n }\n\n @if (selected.map && selected.map.complex) {\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\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && !mapAtMin(selected)) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No entries yet.</p>\n }\n @if (editable() && !mapAtMax(selected)) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addTreeMapEntry(selected)\">\n <mat-icon>add</mat-icon> Add entry\n </button>\n </div>\n }\n }\n\n @if (\n !selected.choice &&\n !selected.mapEntry &&\n !selected.map &&\n !selected.list &&\n !selected.leaves.length &&\n !selected.leafLists.length &&\n !selected.children.length &&\n !(editable() && selected.optionals?.length)\n ) {\n <p class=\"empty\">This node has no fields.</p>\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: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));padding:4px 12px 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}.tree-row.error .tree-label{color:var(--mat-sys-error, #b3261e)}.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}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55))}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary, #6750a4)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;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}.case-select,.key-field{display:block;max-width:320px;margin-bottom:8px}.detail-optional{display:flex;justify-content:flex-start}.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"] }]
|
|
1396
|
+
}], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: true }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }] } });
|
|
812
1397
|
|
|
813
1398
|
/*
|
|
814
1399
|
* Public API Surface of ng-form-foundry
|
|
@@ -818,5 +1403,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
818
1403
|
* Generated bundle index. Do not edit.
|
|
819
1404
|
*/
|
|
820
1405
|
|
|
821
|
-
export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, buildControl, buildFormFromSchema, defineSchema };
|
|
1406
|
+
export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, addMapEntry, buildControl, buildFormFromSchema, caseFields, defineSchema, removeMapEntry, renameMapEntry, resolveChoiceCase, switchChoiceCase };
|
|
822
1407
|
//# sourceMappingURL=ng-form-foundry.mjs.map
|