ng-form-foundry 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,12 @@ function isMap(node) {
51
51
  }
52
52
  /** Whether the node is an optional (presence) node: its key is data, absent until enabled. */
53
53
  function hasPresence(node) {
54
- return ((node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'map' || node.kind === 'choice') &&
54
+ return ((node.kind === 'leaf' ||
55
+ node.kind === 'nodeGroup' ||
56
+ node.kind === 'map' ||
57
+ node.kind === 'choice' ||
58
+ node.kind === 'leafList' ||
59
+ node.kind === 'nodeGroupList') &&
55
60
  node.presence === true);
56
61
  }
57
62
  /**
@@ -1073,6 +1078,12 @@ class NodeGroupListRendererComponent {
1073
1078
  /** Field-layout appearance from the enclosing group, forwarded to every item form. */
1074
1079
  inheritedAppearance = null;
1075
1080
  message = new EventEmitter();
1081
+ /**
1082
+ * Emitted when the last entry of a presence (optional / advisoryRequired) list
1083
+ * is removed — the host de-materializes the whole list (→ absent). An optional
1084
+ * list has no present-empty state; a required list never routes here.
1085
+ */
1086
+ removeList = new EventEmitter();
1076
1087
  // forwardRef: DynamicRecursiveFormComponent and this component import each
1077
1088
  // other, so the class reference is undefined when this query is evaluated at
1078
1089
  // decoration time. forwardRef defers the lookup and keeps the selector valid.
@@ -1102,6 +1113,13 @@ class NodeGroupListRendererComponent {
1102
1113
  });
1103
1114
  }
1104
1115
  removeItem($index) {
1116
+ // A presence (optional / advisoryRequired) list has no present-empty state:
1117
+ // removing its last entry de-materializes the whole list (→ absent). A
1118
+ // required (non-presence) list instead stops at its minimum, staying `[]`.
1119
+ if (this.nodeGroupList?.presence && this.formArray.length === 1) {
1120
+ this.removeList.emit();
1121
+ return;
1122
+ }
1105
1123
  if (this.formArray.length <= this.effectiveMin) {
1106
1124
  this.message.emit({
1107
1125
  message: `You cannot remove the last ${this.nodeGroupList.type.name} configuration!`,
@@ -1136,7 +1154,7 @@ class NodeGroupListRendererComponent {
1136
1154
  }
1137
1155
  }
1138
1156
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1139
- 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", showAbsentOptionals: "showAbsentOptionals", minItems: "minItems", maxItems: "maxItems", inheritedAppearance: "inheritedAppearance" }, 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 [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\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", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$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: "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(() => MatTooltipModule) }, { kind: "directive", type: i0.forwardRef(() => i3$1.MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1157
+ 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", showAbsentOptionals: "showAbsentOptionals", minItems: "minItems", maxItems: "maxItems", inheritedAppearance: "inheritedAppearance" }, outputs: { message: "message", removeList: "removeList" }, 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 [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\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)}.empty-add-button{width:100%}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$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: "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(() => MatTooltipModule) }, { kind: "directive", type: i0.forwardRef(() => i3$1.MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1140
1158
  }
1141
1159
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
1142
1160
  type: Component,
@@ -1145,7 +1163,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1145
1163
  MatButtonModule,
1146
1164
  MatIconModule,
1147
1165
  MatTooltipModule,
1148
- ], 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 [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\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"] }]
1166
+ ], 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 [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\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)}.empty-add-button{width:100%}\n"] }]
1149
1167
  }], propDecorators: { nodeGroupList: [{
1150
1168
  type: Input
1151
1169
  }], initialValue: [{
@@ -1164,6 +1182,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1164
1182
  type: Input
1165
1183
  }], message: [{
1166
1184
  type: Output
1185
+ }], removeList: [{
1186
+ type: Output
1167
1187
  }], items: [{
1168
1188
  type: ViewChildren,
1169
1189
  args: [forwardRef(() => DynamicRecursiveFormComponent)]
@@ -1237,6 +1257,12 @@ class LeafListRendererComponent {
1237
1257
  */
1238
1258
  layout = null;
1239
1259
  message = new EventEmitter();
1260
+ /**
1261
+ * Emitted from the empty-list remove button when the list is a presence list —
1262
+ * the host removes the whole list (de-materializes it, → absent). Only wired
1263
+ * for a `presence` `leafList`; the parent maps it to `setNodePresence`.
1264
+ */
1265
+ removeList = new EventEmitter();
1240
1266
  matDialog = inject(MatDialog);
1241
1267
  /**
1242
1268
  * Take a full-width row of its own once the array holds more than one entry.
@@ -1274,6 +1300,13 @@ class LeafListRendererComponent {
1274
1300
  return (this.formArray?.length ?? 0) < this.effectiveMax;
1275
1301
  }
1276
1302
  removeItem($index) {
1303
+ // A presence (optional / advisoryRequired) list has no present-empty state:
1304
+ // removing its last entry de-materializes the whole list (→ absent). A
1305
+ // required (non-presence) list instead stops at its minimum, staying `[]`.
1306
+ if (this.leaf_?.presence && this.formArray.length === 1) {
1307
+ this.removeList.emit();
1308
+ return;
1309
+ }
1277
1310
  if (this.formArray.length <= this.effectiveMin) {
1278
1311
  this.message.emit({
1279
1312
  message: 'You cannot remove the last item!',
@@ -1289,7 +1322,7 @@ class LeafListRendererComponent {
1289
1322
  this.formArray.push(new FormControl());
1290
1323
  }
1291
1324
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1292
- 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", maxItems: "maxItems", layout: "layout" }, outputs: { message: "message" }, host: { properties: { "class.stacked": "this.stacked", "class": "this.typeClass", "style": "this.hostLayout" } }, 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]=\"formArray.length > effectiveMin\"\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 > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (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 @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty leaf-list has no item to host the trailing add button; this\n creates the first entry so the list is not a dead end. -->\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\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}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 0px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 0px);max-width:var(--nff-max-number-field-width, none)}.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{display:flex;flex:1 1 auto;min-width:0}.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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\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: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1325
+ 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", maxItems: "maxItems", layout: "layout" }, outputs: { message: "message", removeList: "removeList" }, host: { properties: { "class.stacked": "this.stacked", "class": "this.typeClass", "style": "this.hostLayout" } }, 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]=\"formArray.length > effectiveMin\"\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 > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (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 @if (editable && formArray.length === 0 && canAdd) {\n <!-- A required (non-presence) list can be present-but-empty; this labeled\n affordance adds the first entry so an empty list is not a dead end\n (mirrors the node-group list's \"Add \u2026 #1\"). A presence list never\n renders empty here \u2014 its first entry appears on materialize, and\n removing the last entry de-materializes it. -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ leaf_.label ?? leaf_.name }} item\n </button>\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 auto}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 220px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 140px);max-width:var(--nff-max-number-field-width, none)}.empty-add-button{width:100%}.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{display:flex;flex:1 1 auto;min-width:0}.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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\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: i2$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: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1293
1326
  }
1294
1327
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
1295
1328
  type: Component,
@@ -1299,7 +1332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1299
1332
  MatButtonModule,
1300
1333
  MatPrefix,
1301
1334
  MatTooltip,
1302
- ], 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]=\"formArray.length > effectiveMin\"\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 > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (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 @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty leaf-list has no item to host the trailing add button; this\n creates the first entry so the list is not a dead end. -->\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\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}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 0px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 0px);max-width:var(--nff-max-number-field-width, none)}.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{display:flex;flex:1 1 auto;min-width:0}.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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1335
+ ], 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]=\"formArray.length > effectiveMin\"\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 > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (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 @if (editable && formArray.length === 0 && canAdd) {\n <!-- A required (non-presence) list can be present-but-empty; this labeled\n affordance adds the first entry so an empty list is not a dead end\n (mirrors the node-group list's \"Add \u2026 #1\"). A presence list never\n renders empty here \u2014 its first entry appears on materialize, and\n removing the last entry de-materializes it. -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ leaf_.label ?? leaf_.name }} item\n </button>\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 auto}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 220px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 140px);max-width:var(--nff-max-number-field-width, none)}.empty-add-button{width:100%}.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{display:flex;flex:1 1 auto;min-width:0}.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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1303
1336
  }], propDecorators: { leaf_: [{
1304
1337
  type: Input
1305
1338
  }], initialValue: [{
@@ -1316,6 +1349,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1316
1349
  type: Input
1317
1350
  }], message: [{
1318
1351
  type: Output
1352
+ }], removeList: [{
1353
+ type: Output
1319
1354
  }], stacked: [{
1320
1355
  type: HostBinding,
1321
1356
  args: ['class.stacked']
@@ -1420,7 +1455,7 @@ class NodeMapRendererComponent {
1420
1455
  asFormGroup = asFormGroup;
1421
1456
  asFormArray = asFormArray;
1422
1457
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1423
- 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", showAbsentOptionals: "showAbsentOptionals", inheritedAppearance: "inheritedAppearance" }, usesOnChanges: true, 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 <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"asLeafList(nodeMap.value)\"\n [formArray]=\"formGroup.controls[key]\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeGroupList]=\"asNodeGroupList(nodeMap.value)\"\n [formArray]=\"asFormArray(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'map') {\n <nff-node-map-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeMap]=\"asNodeMap(nodeMap.value)\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n <!-- A choice-valued map (additionalProperties: anyOf/oneOf) has no\n standalone bare-choice renderer; use the config editor for it. -->\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (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 [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (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-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;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.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(() => i5.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: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }] });
1458
+ 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", showAbsentOptionals: "showAbsentOptionals", inheritedAppearance: "inheritedAppearance" }, usesOnChanges: true, 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 <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"asLeafList(nodeMap.value)\"\n [formArray]=\"formGroup.controls[key]\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeGroupList]=\"asNodeGroupList(nodeMap.value)\"\n [formArray]=\"asFormArray(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'map') {\n <nff-node-map-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeMap]=\"asNodeMap(nodeMap.value)\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n <!-- A choice-valued map (additionalProperties: anyOf/oneOf) has no\n standalone bare-choice renderer; use the config editor for it. -->\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (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 [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (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-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;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.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(() => i5.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: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }] });
1424
1459
  }
1425
1460
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
1426
1461
  type: Component,
@@ -1693,6 +1728,17 @@ class DynamicRecursiveFormComponent {
1693
1728
  if (!present && this.presenceFocusKey === key)
1694
1729
  this.presenceFocusKey = null;
1695
1730
  }
1731
+ /**
1732
+ * Materialize an absent presence (optional / `advisoryRequired`) list *with
1733
+ * its first entry* — clicking "Add <list>" makes the first entry appear at
1734
+ * once. An optional list has no present-empty state: removing the last entry
1735
+ * de-materializes it (→ absent). A required list is always present instead and
1736
+ * uses the renderer's own "Add item" affordance, so it never routes here.
1737
+ */
1738
+ addPresenceList(key, schema) {
1739
+ setNodePresence(this.formGroup(), schema, key, true, [null]);
1740
+ this.ghostControls.delete(key);
1741
+ }
1696
1742
  objectKeys(obj) {
1697
1743
  return Object.keys(obj);
1698
1744
  }
@@ -1737,7 +1783,7 @@ class DynamicRecursiveFormComponent {
1737
1783
  asFormArray = asFormArray;
1738
1784
  asFormControl = asFormControl;
1739
1785
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1740
- 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 }, showAbsentOptionals: { classPropertyName: "showAbsentOptionals", publicName: "showAbsentOptionals", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null }, addButtonLabel: { classPropertyName: "addButtonLabel", publicName: "addButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, focusLeaf: { classPropertyName: "focusLeaf", publicName: "focusLeaf", isSignal: true, isRequired: false, transformFunction: null }, inheritedAppearance: { classPropertyName: "inheritedAppearance", publicName: "inheritedAppearance", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\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 [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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 <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" 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)=\"toggleNodePresence(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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\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%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.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}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { 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: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$3.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$3.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(() => i2$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: i0.forwardRef(() => i2$2.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(() => 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"] }] });
1786
+ 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 }, showAbsentOptionals: { classPropertyName: "showAbsentOptionals", publicName: "showAbsentOptionals", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null }, addButtonLabel: { classPropertyName: "addButtonLabel", publicName: "addButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, focusLeaf: { classPropertyName: "focusLeaf", publicName: "focusLeaf", isSignal: true, isRequired: false, transformFunction: null }, inheritedAppearance: { classPropertyName: "inheritedAppearance", publicName: "inheritedAppearance", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList' && !child.presence) {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n @else if (child.kind == 'leafList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry (no\n empty state). Removing the last entry de-materializes it (the\n renderer's `removeList` \u2192 absent). -->\n @let leafListArray = asFormArray(formGroup().get(childItem.key));\n @if (leafListArray) {\n <div class=\"presence-list\">\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"leafListArray\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let presenceListArray = asFormArray(formGroup().get(childItem.key));\n @if (presenceListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"presenceListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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 <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <!-- A nodeGroupList has no `appearance` of its own, so its section is\n always shown; only an ancestor's collapse hides it. -->\n <mat-expansion-panel class=\"node-section\" [expanded]=\"true\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ child.label ?? child.name }}</mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let rootListArray = asFormArray(formGroup().get(childItem.key));\n @if (rootListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"rootListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" 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)=\"toggleNodePresence(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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\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%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.presence-list-add{width:100%;flex-shrink:0;margin:8px 0;opacity:.6;transition:opacity .12s ease}.presence-list-add:hover,.presence-list-add:focus-visible{opacity:1}.presence-list{flex:1 1 100%;width:100%}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-cols nff-leaf-list-renderer.stacked,.fields.grid-cols .presence-list,.fields.grid-cols .presence-list-add{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.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;opacity:.45;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.remove-button:hover,.remove-button:focus-visible{opacity:1}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { 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: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$3.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$3.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(() => i2$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: i0.forwardRef(() => i2$2.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(() => 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"] }] });
1741
1787
  }
1742
1788
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
1743
1789
  type: Component,
@@ -1758,7 +1804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1758
1804
  MatFormFieldModule,
1759
1805
  MatSelectModule,
1760
1806
  MatTooltip,
1761
- ], selector: 'nff-dynamic-recursive-form', standalone: true, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\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 [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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 <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" 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)=\"toggleNodePresence(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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\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%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.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}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1807
+ ], selector: 'nff-dynamic-recursive-form', standalone: true, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList' && !child.presence) {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n @else if (child.kind == 'leafList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry (no\n empty state). Removing the last entry de-materializes it (the\n renderer's `removeList` \u2192 absent). -->\n @let leafListArray = asFormArray(formGroup().get(childItem.key));\n @if (leafListArray) {\n <div class=\"presence-list\">\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"leafListArray\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let presenceListArray = asFormArray(formGroup().get(childItem.key));\n @if (presenceListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"presenceListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (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]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? '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\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (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 <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\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\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else 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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <!-- A nodeGroupList has no `appearance` of its own, so its section is\n always shown; only an ancestor's collapse hides it. -->\n <mat-expansion-panel class=\"node-section\" [expanded]=\"true\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ child.label ?? child.name }}</mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let rootListArray = asFormArray(formGroup().get(childItem.key));\n @if (rootListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"rootListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" 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)=\"toggleNodePresence(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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\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 [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\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%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.presence-list-add{width:100%;flex-shrink:0;margin:8px 0;opacity:.6;transition:opacity .12s ease}.presence-list-add:hover,.presence-list-add:focus-visible{opacity:1}.presence-list{flex:1 1 100%;width:100%}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-cols nff-leaf-list-renderer.stacked,.fields.grid-cols .presence-list,.fields.grid-cols .presence-list-add{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.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;opacity:.45;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.remove-button:hover,.remove-button:focus-visible{opacity:1}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1762
1808
  }], 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"] }], showAbsentOptionals: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAbsentOptionals", required: false }] }], addButtonCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonCallback", required: false }] }], addButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonLabel", required: false }] }], focusLeaf: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusLeaf", required: false }] }], inheritedAppearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "inheritedAppearance", required: false }] }] } });
1763
1809
 
1764
1810
  /**
@@ -1872,17 +1918,26 @@ class ConfigEditorComponent {
1872
1918
  });
1873
1919
  }
1874
1920
  /**
1875
- * Force the editor to re-read the bound form. Call this when the form was
1876
- * mutated in a way the editor's own `valueChanges` subscription could not
1877
- * observe a structural change made with `{ emitEvent: false }`, or a
1878
- * mutation while a detached change detector left the view unchecked. It
1879
- * re-syncs the tree structure, rebuilds the current selection's sections
1880
- * from the live controls, and renders them.
1921
+ * Force the editor to re-read the bound form. A leaf field normally reflects
1922
+ * an external `setValue` on its own — the reactive value accessor writes the
1923
+ * value synchronously, no change detection needed so this is only for the
1924
+ * corners that bypass that: a structural change made with
1925
+ * `{ emitEvent: false }` (the tree wouldn't otherwise rebuild), a non-reactive
1926
+ * display (the case selector, a map key) that needs re-reading, or a host
1927
+ * whose change detector was detached during the mutation.
1928
+ *
1929
+ * It re-syncs the tree structure and **tears down and recreates** the detail
1930
+ * sections, so every rendered control rebinds to the live form and re-pulls
1931
+ * its current value — even a field whose bound input somehow missed the
1932
+ * external write.
1881
1933
  */
1882
1934
  refresh() {
1883
1935
  this.syncShape(); // structural changes (added/removed/renamed controls)
1884
- if (this.selected)
1885
- this.select(this.selected, false); // value re-read
1936
+ const selected = this.selected;
1937
+ this.sections = []; // tear the embedded detail forms down…
1938
+ this.cdr.detectChanges();
1939
+ if (selected)
1940
+ this.select(selected, false); // …and rebuild them, fresh-bound
1886
1941
  this.cdr.detectChanges();
1887
1942
  }
1888
1943
  select(node, reveal = true) {
@@ -2380,10 +2435,10 @@ class ConfigEditorComponent {
2380
2435
  if (node.choice) {
2381
2436
  const active = this.activeCase(node);
2382
2437
  const body = active && node.choice.schema.cases[active] ? this.caseAsGroup(node.choice.schema, active) : null;
2383
- return { schema: body ? this.leafOnly(body) : null, group: node.choice.group };
2438
+ return { schema: body ? this.leafOnly(body, node.choice.group) : null, group: node.choice.group };
2384
2439
  }
2385
2440
  if (node.schema && node.group) {
2386
- return { schema: this.leafOnly(node.schema), group: node.group };
2441
+ return { schema: this.leafOnly(node.schema, node.group), group: node.group };
2387
2442
  }
2388
2443
  return { schema: null, group: null };
2389
2444
  }
@@ -2394,12 +2449,17 @@ class ConfigEditorComponent {
2394
2449
  * (grid / field-width layout) carries into the slice. Null when there are
2395
2450
  * no own fields.
2396
2451
  */
2397
- leafOnly(schema) {
2452
+ leafOnly(schema, group) {
2398
2453
  const children = {};
2399
2454
  for (const key of Object.keys(schema.children)) {
2400
2455
  const child = schema.children[key];
2401
- if (child.kind === 'leaf' || child.kind === 'leafList')
2402
- children[key] = child;
2456
+ if (child.kind !== 'leaf' && child.kind !== 'leafList')
2457
+ continue;
2458
+ // An absent presence leafList has no inline affordance and no control to
2459
+ // render — it is offered by the "+ Optional field" menu instead.
2460
+ if (child.kind === 'leafList' && child.presence && !group?.get(key))
2461
+ continue;
2462
+ children[key] = child;
2403
2463
  }
2404
2464
  if (!Object.keys(children).length)
2405
2465
  return null;
@@ -2414,14 +2474,15 @@ class ConfigEditorComponent {
2414
2474
  const child = schema.children[key];
2415
2475
  if (child.kind === 'leaf' || child.kind === 'leafList') {
2416
2476
  // Leaves render in the detail pane; an absent presence leaf is also
2417
- // offered by the "+ Optional field" menu, so it can be added from the
2418
- // tree row as well as inline.
2419
- if (child.kind === 'leaf' && child.presence && !group.get(key)) {
2477
+ // offered by the "+ Optional field" menu (addable inline or from the
2478
+ // tree row). An absent presence *leafList* has no inline affordance, so
2479
+ // it is offered only by the menu (and excluded from the leafOnly slice).
2480
+ if (child.presence && !group.get(key)) {
2420
2481
  optionals.push({ key, schema: child, label: this.labelOf(child, key) });
2421
2482
  }
2422
2483
  continue;
2423
2484
  }
2424
- const presence = child.kind !== 'nodeGroupList' && child.presence;
2485
+ const presence = !!child.presence;
2425
2486
  if (presence && !group.get(key)) {
2426
2487
  optionals.push({ key, schema: child, label: this.labelOf(child, key) });
2427
2488
  continue;