ng-form-foundry 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FormControl, FormArray, FormGroup } from '@angular/forms';
2
- import * as i0 from '@angular/core';
3
- import { OnInit, AfterViewInit, EventEmitter, ElementRef, QueryList, ChangeDetectorRef } from '@angular/core';
2
+ import * as _angular_core from '@angular/core';
3
+ import { OnInit, OnDestroy, AfterViewInit, ElementRef, EventEmitter, QueryList, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
4
4
  import { MatDialog } from '@angular/material/dialog';
5
5
 
6
6
  type LeafRuntimeType<T> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'enum' ? string | number : never;
@@ -153,7 +153,11 @@ type NodeChoice = {
153
153
  presence?: boolean;
154
154
  appearance?: Appearance;
155
155
  };
156
- /** The control name that records which case of a {@link NodeChoice} is active. */
156
+ /**
157
+ * The control name that records which case of a {@link NodeChoice} is active.
158
+ * The name is reserved: it cannot be used as a case field name (the builder
159
+ * throws) or as a map entry key (the entry helpers reject it).
160
+ */
157
161
  declare const CASE_KEY = "__case";
158
162
  /**
159
163
  * An open, arbitrary-keyed record: unlike {@link NodeGroup} (a fixed, declared
@@ -189,28 +193,15 @@ type FormGroupType<T extends NodeGroup> = {
189
193
  };
190
194
  type DFormGroup<T extends NodeGroup> = FormGroup<FormGroupType<T>>;
191
195
 
192
- /**
193
- * Build the `AbstractControl` for a single schema node.
194
- *
195
- * Dispatches on `node.kind`: a `leaf` becomes a `FormControl`, a `leafList` a
196
- * `FormArray` of controls, a `nodeGroup` a nested `FormGroup`, and a
197
- * `nodeGroupList` a `FormArray` of groups. `initial` is the runtime value for
198
- * this node — a scalar for a leaf, an array for a list, an object for a group —
199
- * and seeds the control's value (falling back to the node's `default`).
200
- *
201
- * Most callers use {@link buildFormFromSchema}; this is exposed for building a
202
- * control from a single non-root node.
203
- */
204
- /**
205
- * Build the FormGroup for a choice: a `__case` control holding the active case
206
- * name plus that case's field controls. Only the active case's fields are built,
207
- * matching the inline YANG encoding; switching the case swaps them.
208
- */
209
196
  /**
210
197
  * Normalize a {@link ChoiceCase} to a field record. A field record is returned
211
198
  * as-is; a single node (a leaf-bodied case, e.g. a scalar `anyOf` branch) becomes
212
199
  * a one-field record keyed by the node's `name`. The discriminant is a top-level
213
200
  * `kind` string, which a field record never has (its keys are field names).
201
+ *
202
+ * Throws when a case field is keyed `__case`: that name is reserved for the
203
+ * choice discriminator ({@link CASE_KEY}) and a field under it would silently
204
+ * clobber the active-case control.
214
205
  */
215
206
  declare function caseFields(body: ChoiceCase): Record<string, NodeType>;
216
207
  /**
@@ -223,7 +214,10 @@ declare function resolveChoiceCase(choice: NodeChoice, initial?: Record<string,
223
214
  /**
224
215
  * Switch a choice's FormGroup to `caseName`: sets `__case`, removes every other
225
216
  * control, and builds `caseName`'s fields (normalized via {@link caseFields})
226
- * with their defaults. An unknown case name leaves only `__case`.
217
+ * with their defaults. Presence fields of the new case start absent the
218
+ * switch carries no data that could make them present. An unknown case name
219
+ * leaves only `__case`. The swap is atomic: one value change fires, and every
220
+ * observable snapshot has fields matching its discriminator.
227
221
  */
228
222
  declare function switchChoiceCase(group: FormGroup, choice: NodeChoice, caseName: string): void;
229
223
  /**
@@ -236,14 +230,50 @@ declare function switchChoiceCase(group: FormGroup, choice: NodeChoice, caseName
236
230
  declare function addMapEntry(group: FormGroup, map: NodeMap, key?: string): string | null;
237
231
  /**
238
232
  * Rename entry `oldKey` to `newKey.trim()`, preserving the control instance
239
- * (remove + re-add, so the value survives). Returns whether the rename was
240
- * committed: an empty, unchanged, duplicate, or `keyPattern`-violating key is a
241
- * no-op, leaving the entry under its current name.
233
+ * (so the value survives) and the entry's position in the group's key order —
234
+ * the order `getRawValue()` serializes and the tree editor renders. Returns
235
+ * whether the rename was committed: an empty, reserved (`__case`), unchanged,
236
+ * duplicate, or `keyPattern`-violating key is a no-op, leaving the entry under
237
+ * its current name. Entry keys are looked up verbatim — never via
238
+ * `AbstractControl.get`, which would split keys like `10.0.0.1` into
239
+ * dot-delimited paths. Emits a single value change.
242
240
  */
243
241
  declare function renameMapEntry(group: FormGroup, map: NodeMap, oldKey: string, newKey: string): boolean;
244
242
  /** Remove entry `key` unless the map is at `minEntries`. Returns whether it was removed. */
245
243
  declare function removeMapEntry(group: FormGroup, map: NodeMap, key: string): boolean;
244
+ /**
245
+ * Build the `AbstractControl` for a single schema node.
246
+ *
247
+ * Dispatches on `node.kind`: a `leaf` becomes a `FormControl`, a `leafList` a
248
+ * `FormArray` of controls, a `nodeGroup` a nested `FormGroup`, and a
249
+ * `nodeGroupList` a `FormArray` of groups. `initial` is the runtime value for
250
+ * this node — a scalar for a leaf, an array for a list, an object for a group —
251
+ * and seeds the control's value (falling back to the node's `default`).
252
+ *
253
+ * Most callers use {@link buildFormFromSchema}; this is exposed for building a
254
+ * control from a single non-root node.
255
+ */
246
256
  declare function buildControl<T extends NodeType>(node: T, initial?: unknown | null): DFormControl<T> | FormControl<LeafRuntimeType<any>> | FormArray<any>;
257
+ /**
258
+ * Build a typed `FormGroup` from a root `NodeGroup` schema.
259
+ *
260
+ * The returned group's control structure, keys, and value types are inferred
261
+ * from the schema literal — a `leaf` of `type: 'number'` yields a
262
+ * `FormControl<number>`, a `nodeGroup` a nested `FormGroup`, and so on. `initial`
263
+ * is an optional value object keyed by the schema's `children` keys; each child
264
+ * control is seeded from its matching slice (falling back to the node `default`).
265
+ *
266
+ * Presence nodes whose key is absent from `initial` get no control, at any depth
267
+ * — plain children, list items, map values, and choice case fields alike — so
268
+ * they are absent from the form value until enabled. A key present with an
269
+ * explicit `null` keeps its control: `null` is a value, absence is the missing
270
+ * key.
271
+ *
272
+ * Inference only holds when `schema`'s literal type is preserved. Author schemas
273
+ * with {@link defineSchema} or a `satisfies NodeGroup` annotation — never
274
+ * `const schema: NodeGroup = ...`, which widens `children` and erases the field
275
+ * names and value types.
276
+ */
247
277
  declare function buildFormFromSchema<S extends NodeGroup>(schema: S, initial?: Record<string, unknown> | null): DFormGroup<S>;
248
278
  /**
249
279
  * Capture a schema literal with its exact type while checking it against
@@ -266,56 +296,56 @@ declare function asFormGroup(control: any): FormGroup;
266
296
 
267
297
  declare class DynamicRecursiveFormComponent implements OnInit {
268
298
  /** The form-description schema to render (a root or nested `NodeGroup`). */
269
- readonly schema: i0.InputSignal<NodeGroup>;
299
+ readonly schema: _angular_core.InputSignal<NodeGroup>;
270
300
  /** Optional value object to seed the form; keyed by the schema's `children` keys. */
271
- readonly initialValue: i0.InputSignal<Record<string, unknown> | null>;
301
+ readonly initialValue: _angular_core.InputSignal<Record<string, unknown> | null>;
272
302
  /** The reactive group this form binds to. Defaults to an empty group. */
273
- readonly formGroup: i0.InputSignal<FormGroup<any>>;
303
+ readonly formGroup: _angular_core.InputSignal<FormGroup<any>>;
274
304
  /** Index of this form within a parent list, used by `addButtonCallback`. */
275
- readonly index: i0.InputSignal<number | null>;
305
+ readonly index: _angular_core.InputSignal<number | null>;
276
306
  /** Whether this form may be removed from a parent list (shows a remove control). */
277
- readonly removable: i0.InputSignal<boolean>;
307
+ readonly removable: _angular_core.InputSignal<boolean>;
278
308
  /** Emitted when the user removes this form from a parent list. */
279
- readonly remove: i0.OutputEmitterRef<void>;
309
+ readonly remove: _angular_core.OutputEmitterRef<void>;
280
310
  /** Card/section title; falls back to the schema label or name. */
281
- readonly title: i0.InputSignal<string | undefined>;
311
+ readonly title: _angular_core.InputSignal<string | undefined>;
282
312
  /** Whether fields accept input. Two-way: also toggled by the built-in edit control. */
283
- readonly editable: i0.ModelSignal<boolean>;
313
+ readonly editable: _angular_core.ModelSignal<boolean>;
284
314
  /** Invoked with {@link index} to append a new sibling form to a parent list. */
285
- readonly addButtonCallback: i0.InputSignal<((index: number) => void) | null>;
315
+ readonly addButtonCallback: _angular_core.InputSignal<((index: number) => void) | null>;
316
+ /**
317
+ * Key of a presence leaf whose field should grab focus when it renders — lets
318
+ * a host that added the control itself (e.g. the tree editor's optionals
319
+ * menu) hand focus to the new field, like the form's own add button does.
320
+ */
321
+ readonly focusLeaf: _angular_core.InputSignal<string | null>;
286
322
  /** True when the schema is a root group (rendered flat, without a wrapping card). */
287
- readonly root: i0.Signal<boolean>;
323
+ readonly root: _angular_core.Signal<boolean>;
288
324
  ngOnInit(): void;
289
- get nodeGroupChildrenList(): Array<{
325
+ protected get nodeGroupChildrenList(): Array<{
290
326
  key: string;
291
327
  value: NodeType;
292
328
  }>;
293
- emitRemoveEvent(): void;
294
- /**
295
- * Add or remove a presence group's control on this form. Removing it drops the
296
- * group from `form.value`; adding it rebuilds the sub-group from its schema.
297
- */
298
- togglePresence(key: string, schema: NodeGroup, present: boolean): void;
329
+ protected emitRemoveEvent(): void;
299
330
  /** The key of the presence leaf the user just enabled; its field grabs focus when rendered. */
300
331
  protected presenceFocusKey: string | null;
301
332
  /**
302
- * Add or remove a presence leaf's control on this form. Removing it drops the
303
- * leaf from `form.value`; adding it rebuilds the control from its schema and
304
- * focuses the rendered field.
333
+ * Add or remove a presence node's control on this form any presence kind:
334
+ * group, leaf, map, or choice. Removing it drops the key from `form.value`;
335
+ * adding it builds the control fresh from its schema (nested presence
336
+ * children start absent).
305
337
  */
306
- toggleLeafPresence(key: string, schema: Leaf, present: boolean): void;
338
+ toggleNodePresence(key: string, schema: NodeType, present: boolean): void;
307
339
  /**
308
- * Add or remove a presence map's or choice's control on this form. Removing it
309
- * drops the node from `form.value`; adding it rebuilds the control (an empty
310
- * map, or a choice group holding `__case`) from its schema.
340
+ * {@link toggleNodePresence} for a presence leaf, additionally focusing the
341
+ * rendered field when the toggle just created it.
311
342
  */
312
- toggleNodePresence(key: string, schema: NodeType, present: boolean): void;
313
- protected readonly CASE_KEY = "__case";
314
- objectKeys(obj: Record<string, unknown>): string[];
343
+ toggleLeafPresence(key: string, schema: Leaf, present: boolean): void;
344
+ protected objectKeys(obj: Record<string, unknown>): string[];
315
345
  /** The active case name of a choice control, or null if none is selected. */
316
- activeCase(key: string): string | null;
346
+ protected activeCase(key: string): string | null;
317
347
  /** A synthetic flattened NodeGroup used to render the active case's fields against the choice's FormGroup. */
318
- caseAsGroup(choice: NodeChoice, caseName: string): NodeGroup;
348
+ protected caseAsGroup(choice: NodeChoice, caseName: string): NodeGroup;
319
349
  /** The display label for a case: the schema's `caseLabels` entry, else the case name. */
320
350
  caseLabel(choice: NodeChoice, caseName: string): string;
321
351
  /**
@@ -323,14 +353,14 @@ declare class DynamicRecursiveFormComponent implements OnInit {
323
353
  * for a presence group's body, whose container is the presence panel itself, so
324
354
  * the group's own section panel would be a redundant second box.
325
355
  */
326
- flatGroup(group: NodeGroup): NodeGroup;
356
+ protected flatGroup(group: NodeGroup): NodeGroup;
327
357
  /** Swap a choice's field controls when the selected case changes. Delegates to {@link switchChoiceCase}. */
328
358
  switchCase(key: string, choice: NodeChoice, caseName: string): void;
329
359
  protected readonly asFormGroup: typeof asFormGroup;
330
360
  protected readonly asFormArray: typeof asFormArray;
331
361
  protected readonly asFormControl: typeof asFormControl;
332
- static ɵfac: i0.ɵɵFactoryDeclaration<DynamicRecursiveFormComponent, never>;
333
- static ɵcmp: i0.ɵɵComponentDeclaration<DynamicRecursiveFormComponent, "nff-dynamic-recursive-form", never, { "schema": { "alias": "schema"; "required": true; "isSignal": true; }; "initialValue": { "alias": "initialValue"; "required": false; "isSignal": true; }; "formGroup": { "alias": "formGroup"; "required": false; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; "removable": { "alias": "removable"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; "addButtonCallback": { "alias": "addButtonCallback"; "required": false; "isSignal": true; }; }, { "remove": "remove"; "editable": "editableChange"; }, never, never, true, never>;
362
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DynamicRecursiveFormComponent, never>;
363
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DynamicRecursiveFormComponent, "nff-dynamic-recursive-form", never, { "schema": { "alias": "schema"; "required": true; "isSignal": true; }; "initialValue": { "alias": "initialValue"; "required": false; "isSignal": true; }; "formGroup": { "alias": "formGroup"; "required": false; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; "removable": { "alias": "removable"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; "addButtonCallback": { "alias": "addButtonCallback"; "required": false; "isSignal": true; }; "focusLeaf": { "alias": "focusLeaf"; "required": false; "isSignal": true; }; }, { "remove": "remove"; "editable": "editableChange"; }, never, never, true, never>;
334
364
  }
335
365
 
336
366
  /** Metadata on a list-container node that lets the tree add items to its FormArray. */
@@ -339,51 +369,62 @@ interface ListRef {
339
369
  itemSchema: NodeGroup;
340
370
  itemLabel: string;
341
371
  minItems: number;
372
+ maxItems?: number;
342
373
  }
343
374
  /** An absent optional (presence) child, offered by its parent node's "+ Optional field" menu. */
344
375
  interface OptionalEntry {
345
376
  key: string;
346
377
  schema: NodeType;
347
378
  label: string;
348
- /** Index in the parent schema's children iteration; keeps the menu in schema order. */
349
- order: number;
350
379
  }
351
- /** A navigable node in the config tree. Groups and list items are tree nodes; leaves are their detail. */
380
+ /**
381
+ * One flat section of the detail pane. The selected node's subtree renders as a
382
+ * pre-order list of these — no nesting chrome: each section after the first is
383
+ * separated by a breadcrumb heading (`trail`), and holds only the node's *own*
384
+ * fields (`schema` is a leaf-only slice; complex children are sections of their
385
+ * own, deeper in the list).
386
+ */
387
+ interface DetailSection {
388
+ node: TreeNode;
389
+ /** Selected-node-to-here trail, rendered as the section's breadcrumb heading (omitted on the first section). */
390
+ trail: TreeNode[];
391
+ /** Leaf-only slice of the node's own schema, or null when it has no own fields. */
392
+ schema: NodeGroup | null;
393
+ /** The group `schema` binds to (the node's group; for a choice, the choice group). */
394
+ group: FormGroup | null;
395
+ }
396
+ /**
397
+ * A navigable node in the config tree. Its `id` is the node's stable path from
398
+ * the root (`system/ntp`, `ifaces/0`, `servers/web1`), so expansion and
399
+ * selection survive tree rebuilds. Segments are `%`/`/`-escaped, since map
400
+ * entry keys are arbitrary runtime data; list-item identity is positional.
401
+ */
352
402
  interface TreeNode {
353
403
  id: string;
354
404
  label: string;
355
405
  children: TreeNode[];
356
- /** Leaves editable when this node is selected. A presence leaf carries its `optional` entry so it can be removed back to the menu. */
357
- leaves: {
358
- key: string;
359
- node: Leaf;
360
- optional?: OptionalEntry;
361
- }[];
362
- leafLists: {
363
- key: string;
364
- node: LeafList;
365
- }[];
366
- /** The FormGroup holding this node's leaves, or null for a list-container or map node. */
406
+ /** The node's own FormGroup, or null for a list-container or map node. */
367
407
  group: FormGroup | null;
408
+ /** The node's own schema, set on group-backed nodes (groups, list items, group-valued map entries). */
409
+ schema?: NodeGroup;
368
410
  /** Present on a nodeGroupList node: lets a `+` add an item. */
369
411
  list?: ListRef;
370
- /** Present on a list-item node: the FormArray and current index it can be removed from. */
412
+ /** Present on a list-item node: its current index in the parent list (removal goes through the parent list array). */
371
413
  removable?: {
372
- array: FormArray;
373
414
  index: number;
374
415
  };
375
- /** Absent optional children of this node, offered by its "+ Optional field" menu. */
416
+ /** Absent optional children of this node, offered by its "+ Optional field" menu (schema order). */
376
417
  optionals?: OptionalEntry[];
377
- /** Present on a present optional child node: drives the row's remove control; removal returns `entry` to the parent's menu. */
418
+ /** Present on a present optional child node: drives the row's remove control. */
378
419
  presenceRemovable?: {
379
- entry: OptionalEntry;
420
+ key: string;
380
421
  };
381
422
  /** Present on a choice node: the choice schema and its FormGroup (holding `__case` + the active case's fields). */
382
423
  choice?: {
383
424
  schema: NodeChoice;
384
425
  group: FormGroup;
385
426
  };
386
- /** Present on a map node. `complex` maps expand entries as child nodes; leaf-valued maps edit inline in the detail pane. */
427
+ /** Present on a map node. `complex` maps expand entries as child nodes. */
387
428
  map?: {
388
429
  schema: NodeMap;
389
430
  group: FormGroup;
@@ -398,115 +439,179 @@ interface TreeNode {
398
439
  }
399
440
  /**
400
441
  * A tree/detail editor for a schema-built form: the structure (groups, lists,
401
- * maps, choices) is a tree on the left, and selecting a node shows that node's
402
- * fields for editing on the right. A `+` on a list or map node adds an entry; a
403
- * delete button on each item removes it. Absent optional (presence) children are
404
- * offered by a "+ Optional field" menu row at the end of their parent's
405
- * children; present ones carry a delete button that returns them to the menu.
442
+ * maps, choices) is a tree on the left, and selecting a node renders that
443
+ * node's **entire subtree** on the right as a **flat list of sections** the
444
+ * node's own fields first, then every descendant's fields, each separated by a
445
+ * breadcrumb heading (`Service / Deploy scope / …`) instead of nested panels.
446
+ * Leaf fields render through {@link DynamicRecursiveFormComponent} with a
447
+ * leaf-only schema slice; choice selectors, map rows, and add controls render
448
+ * inline in their section. The tree adds row conveniences of its own: `+` on
449
+ * list and map rows, a delete control on removable rows, and a
450
+ * "+ Optional field" menu for absent presence children.
451
+ *
452
+ * The tree is **derived state**: any structural change to the form — made
453
+ * through the tree rows or through the detail sections — triggers a rebuild (a
454
+ * cheap shape signature over `valueChanges` detects it). Node ids are stable
455
+ * paths, so expansion and selection survive rebuilds. Swapping the `schema` or
456
+ * `formGroup` input rebinds the editor to the new pair, resetting expansion
457
+ * and selection.
406
458
  *
407
459
  * The component draws no outer container — only a divider between the tree and
408
- * detail panes — so the embedding client owns the surrounding chrome. The tree
409
- * is built once from the `schema`/`formGroup` provided at initialization.
410
- * An alternative to the all-in-one {@link DynamicRecursiveFormComponent} for
411
- * large configs.
460
+ * detail panes — so the embedding client owns the surrounding chrome.
412
461
  */
413
- declare class ConfigEditorComponent implements OnInit {
462
+ declare class ConfigEditorComponent implements OnDestroy {
414
463
  /** The form-description schema whose structure the tree renders. */
415
- readonly schema: i0.InputSignal<NodeGroup>;
464
+ readonly schema: _angular_core.InputSignal<NodeGroup>;
416
465
  /** The schema-built reactive group the editor binds to and mutates. */
417
- readonly formGroup: i0.InputSignal<FormGroup<any>>;
466
+ readonly formGroup: _angular_core.InputSignal<FormGroup<any>>;
418
467
  /** Whether fields accept input and structural controls (add/remove/menus) show. */
419
- readonly editable: i0.InputSignal<boolean>;
468
+ readonly editable: _angular_core.InputSignal<boolean>;
420
469
  root: TreeNode;
421
470
  selected: TreeNode | null;
471
+ /** The selected subtree as a flat, breadcrumb-separated section list. */
472
+ sections: DetailSection[];
473
+ /** Root-to-selection trail for the detail-pane breadcrumb, computed once per selection. */
474
+ breadcrumb: TreeNode[];
422
475
  readonly expanded: Set<string>;
423
- private nextId;
424
- ngOnInit(): void;
425
- select(node: TreeNode): void;
426
- /** Select a child (list item or sub-group) from the detail pane, keeping its parent expanded in the tree. */
427
- open(parent: TreeNode, child: TreeNode): void;
476
+ private shape;
477
+ private changes?;
478
+ private readonly host;
479
+ /** Stable per-instance ids for the shape signature, so replacing a control (setControl) reads as a structural change. */
480
+ private readonly controlIds;
481
+ private controlIdSeq;
482
+ constructor();
483
+ ngOnDestroy(): void;
484
+ /** Bind the editor to a schema/form pair: fresh tree, root selection, and shape-sync subscription. */
485
+ private attach;
486
+ select(node: TreeNode, reveal?: boolean): void;
428
487
  /**
429
488
  * Root-to-`target` path for the detail-pane breadcrumb (inclusive of both ends),
430
- * or an empty array if `target` is not in the current tree. Searched fresh each
431
- * call so it stays correct after add/remove/presence mutations rebuild subtrees.
489
+ * or an empty array if `target` is not in the current tree.
432
490
  */
433
491
  pathTo(target: TreeNode): TreeNode[];
434
- toggle(node: TreeNode): void;
492
+ protected toggle(node: TreeNode): void;
493
+ /** Keyboard access for tree rows: Enter/Space selects, ArrowRight expands, ArrowLeft collapses. */
494
+ protected onRowKeydown(event: KeyboardEvent, node: TreeNode): void;
435
495
  /** Whether the row shows an expand twisty: it has child rows to reveal (children or an optionals menu row). */
436
- hasExpandableContent(node: TreeNode): boolean;
496
+ protected hasExpandableContent(node: TreeNode): boolean;
437
497
  /**
438
498
  * Whether the node's form subtree holds a validation error. Group/choice, list,
439
499
  * and map nodes each check their backing control; `invalid` aggregates over
440
500
  * descendants, so an error anywhere below lights every ancestor row.
441
501
  */
442
- hasError(node: TreeNode): boolean;
443
- /** Append a new item to a list node's FormArray and to the tree, then select it. */
502
+ protected hasError(node: TreeNode): boolean;
503
+ /** Append a new item to a list node's FormArray (up to `maxItems`), then select it. */
444
504
  addItem(listNode: TreeNode): void;
445
- /** Remove a list item from its FormArray and the tree (down to `minItems`). */
505
+ /**
506
+ * Remove a list item from its FormArray (down to `minItems`). List-item
507
+ * identity is positional, so expansion state under the list is cleared —
508
+ * otherwise it would silently migrate to the items that shift into the
509
+ * removed indexes.
510
+ */
446
511
  removeItem(listNode: TreeNode, item: TreeNode): void;
447
- /** The key of the optional leaf the user just added; its detail-pane field grabs focus when rendered. */
512
+ /** The just-added optional leaf (section path + key) whose field grabs focus when rendered. */
513
+ protected focusSectionId: string | null;
448
514
  protected focusLeafKey: string | null;
449
- /** Add an absent optional child from the menu: build its control, place it in the tree, and select it. */
515
+ /** Add an absent optional child from the menu: build its control and select the node it lands on. */
450
516
  addOptional(node: TreeNode, entry: OptionalEntry): void;
451
517
  /** Remove a present optional child node, returning its entry to the parent's menu. */
452
518
  removeOptional(parent: TreeNode, node: TreeNode): void;
453
- /** Remove a present optional leaf from the detail pane, returning its entry to the menu. */
454
- removeOptionalLeaf(node: TreeNode, leaf: {
455
- key: string;
456
- node: Leaf;
457
- optional?: OptionalEntry;
458
- }): void;
459
519
  /** The active case name of a choice node, or null when none is selected. */
460
520
  activeCase(node: TreeNode): string | null;
461
521
  /** The display label of a choice node's active case, or null when no case is selected. */
462
522
  activeCaseLabel(node: TreeNode): string | null;
463
523
  /** The display label for a case: the schema's `caseLabels` entry, else the case name. */
464
- caseLabel(choice: NodeChoice, caseName: string): string;
465
- /** Switch a choice node's active case: swap the group's controls and rebuild the subtree in place. */
524
+ protected caseLabel(choice: NodeChoice, caseName: string): string;
525
+ /** Switch a choice node's active case; the structural sync rebuilds the tree and sections. */
466
526
  switchTreeCase(node: TreeNode, caseName: string): void;
527
+ protected objectKeys(obj: Record<string, unknown>): string[];
467
528
  /** Append a new entry to a complex map node under a generated unique key, then select it. */
468
529
  addTreeMapEntry(mapNode: TreeNode): void;
469
- /** Remove a complex map entry (down to `minEntries`) from the form group and the tree. */
530
+ /** Remove a complex map entry (down to `minEntries`). */
470
531
  removeTreeMapEntry(mapNode: TreeNode, entryNode: TreeNode): void;
471
- /** Commit a rename-on-blur of a map entry's key; on success the node is relabeled. */
532
+ /**
533
+ * Commit a rename-on-blur of a map entry's key; on success the entry is
534
+ * selected under its new path and its fresh key field regains focus (the
535
+ * rename re-renders the section under a new id, destroying the input that
536
+ * held focus).
537
+ */
472
538
  renameTreeMapEntry(entryNode: TreeNode, rawKey: string): void;
539
+ /** Move keyboard focus to the selected tree row once the action's re-render settles. */
540
+ private focusSelectedRow;
541
+ /** A muted hint for a section whose node currently renders no content of its own. */
542
+ protected emptySectionHint(s: DetailSection): string | null;
543
+ /** Whether a list node is at `maxItems` (the add control is hidden). */
544
+ protected listAtMax(node: TreeNode | undefined): boolean;
545
+ /** Whether a list node is at `minItems` (item remove controls are hidden). */
546
+ protected listAtMin(node: TreeNode | undefined): boolean;
473
547
  /** Whether a map node is at `maxEntries` (the add control is hidden). */
474
- mapAtMax(node: TreeNode | undefined): boolean;
548
+ protected mapAtMax(node: TreeNode | undefined): boolean;
475
549
  /** Whether a map node is at `minEntries` (entry remove controls are hidden). */
476
- mapAtMin(node: TreeNode | undefined): boolean;
477
- protected readonly asFormControl: typeof asFormControl;
478
- protected readonly asFormArray: typeof asFormArray;
479
- protected readonly CASE_KEY = "__case";
480
- objectKeys(obj: Record<string, unknown>): string[];
481
- /** Re-index and re-label a list node's item children (just "#n") after add/remove. */
482
- private renumber;
483
- /** Select `fallback` when the current selection is no longer reachable in the tree. */
484
- private reselectIfOrphaned;
485
- /** Re-insert a removed optional's entry into the node's menu, keeping schema order. */
486
- private reinsertOptional;
487
- /** Synthetic group over a case's normalized fields, so a case body builds like any group. */
488
- private caseAsGroup;
550
+ protected mapAtMin(node: TreeNode | undefined): boolean;
551
+ /** Rebuild the tree from the current form structure if its shape changed. */
552
+ private syncShape;
553
+ /**
554
+ * A cheap structural signature of a control tree: group keys (plus the active
555
+ * `__case`), array lengths, leaf placeholders. Value edits don't change it;
556
+ * added/removed/renamed controls and case switches do. Reading `__case` from
557
+ * any group is safe because the name is reserved (the builder rejects it as a
558
+ * case field name or map entry key), so only choice groups carry it.
559
+ */
560
+ private shapeOf;
561
+ /** The instance id a container contributes to the shape signature. */
562
+ private uidOf;
563
+ /** Rebuild the whole tree from schema + form and refresh the shape signature. */
564
+ private rebuild;
565
+ /** Re-point `selected` at the rebuilt tree: same path, else the closest surviving ancestor. */
566
+ private reconcileSelection;
567
+ /** The node at `path` in the current tree, or null. Walks by id-prefix segments. */
568
+ private byPath;
569
+ /** Select the node at `path`, falling back through its ancestors to the root. */
570
+ private selectByPath;
571
+ /**
572
+ * Flatten a subtree into detail sections, pre-order: the node's own fields
573
+ * first, then each child as its own breadcrumb-headed section. No nesting
574
+ * chrome — the headings are the boundaries between children.
575
+ */
576
+ private buildSections;
577
+ /** A section's own renderable fields: a leaf-only schema slice and the group it binds to. */
578
+ private sectionContent;
579
+ /**
580
+ * The node's own fields as a flattened schema: leaf and leafList children
581
+ * only. Complex children are rendered as sections of their own, so the
582
+ * embedded form never draws nested section chrome. Null when there are none.
583
+ */
584
+ private leafOnly;
489
585
  private buildTree;
490
586
  /** Build the tree node for a single non-leaf child, dispatching on its kind. Null when its control is missing. */
491
587
  private buildChildNode;
588
+ /** Synthetic group over a case's normalized fields, so a case body builds like any group. */
589
+ private caseAsGroup;
492
590
  /** A node's display label: its schema `label`, else its record key. */
493
591
  private labelOf;
494
- static ɵfac: i0.ɵɵFactoryDeclaration<ConfigEditorComponent, never>;
495
- static ɵcmp: i0.ɵɵComponentDeclaration<ConfigEditorComponent, "nff-config-editor", never, { "schema": { "alias": "schema"; "required": true; "isSignal": true; }; "formGroup": { "alias": "formGroup"; "required": true; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
592
+ /** Join a parent path and a segment; the root's path is the empty string. */
593
+ private join;
594
+ /**
595
+ * Escape a path segment: `/` is the id separator, and map entry keys are
596
+ * arbitrary runtime data that may contain it. Labels stay unescaped — only
597
+ * node identities encode.
598
+ */
599
+ private escapeSeg;
600
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ConfigEditorComponent, never>;
601
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ConfigEditorComponent, "nff-config-editor", never, { "schema": { "alias": "schema"; "required": true; "isSignal": true; }; "formGroup": { "alias": "formGroup"; "required": true; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
496
602
  }
497
603
 
498
- declare class LeafRendererComponent implements OnInit, AfterViewInit {
604
+ declare class LeafRendererComponent implements AfterViewInit {
499
605
  private readonly elementRef;
500
606
  leaf_: Leaf | AnonLeaf;
501
607
  control: FormControl;
502
- initialValue?: any;
503
608
  removable: boolean;
504
609
  editable: boolean;
505
610
  /** Focus the field once rendered — for fields the user just added (e.g. an enabled presence leaf). */
506
611
  autofocus: boolean;
507
- remove: EventEmitter<number>;
612
+ /** Emitted when the user removes this field (e.g. an optional presence leaf). */
613
+ readonly remove: _angular_core.OutputEmitterRef<void>;
508
614
  constructor(elementRef: ElementRef<HTMLElement>);
509
- ngOnInit(): void;
510
615
  ngAfterViewInit(): void;
511
616
  /** Whether this field accepts input: the form is editable and the leaf is not `readOnly`. */
512
617
  get fieldEditable(): boolean;
@@ -516,8 +621,8 @@ declare class LeafRendererComponent implements OnInit, AfterViewInit {
516
621
  * state (invalid and touched), so it can be bound unconditionally.
517
622
  */
518
623
  get errorText(): string;
519
- static ɵfac: i0.ɵɵFactoryDeclaration<LeafRendererComponent, never>;
520
- static ɵcmp: i0.ɵɵComponentDeclaration<LeafRendererComponent, "nff-leaf-renderer", never, { "leaf_": { "alias": "leaf_"; "required": false; }; "control": { "alias": "control"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "autofocus": { "alias": "autofocus"; "required": false; }; }, { "remove": "remove"; }, never, never, true, never>;
624
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<LeafRendererComponent, never>;
625
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<LeafRendererComponent, "nff-leaf-renderer", never, { "leaf_": { "alias": "leaf_"; "required": false; }; "control": { "alias": "control"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "autofocus": { "alias": "autofocus"; "required": false; }; }, { "remove": "remove"; }, never, never, true, never>;
521
626
  }
522
627
 
523
628
  declare class LeafListRendererComponent implements OnInit {
@@ -538,8 +643,8 @@ declare class LeafListRendererComponent implements OnInit {
538
643
  ngOnInit(): void;
539
644
  removeItem($index: number): void;
540
645
  addItem(): void;
541
- static ɵfac: i0.ɵɵFactoryDeclaration<LeafListRendererComponent, never>;
542
- static ɵcmp: i0.ɵɵComponentDeclaration<LeafListRendererComponent, "nff-leaf-list-renderer", never, { "leaf_": { "alias": "leaf_"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "formArray": { "alias": "formArray"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "minItems": { "alias": "minItems"; "required": false; }; }, { "message": "message"; }, never, never, true, never>;
646
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<LeafListRendererComponent, never>;
647
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<LeafListRendererComponent, "nff-leaf-list-renderer", never, { "leaf_": { "alias": "leaf_"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "formArray": { "alias": "formArray"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "minItems": { "alias": "minItems"; "required": false; }; }, { "message": "message"; }, never, never, true, never>;
543
648
  }
544
649
 
545
650
  declare class LeafEnumRendererComponent {
@@ -549,8 +654,8 @@ declare class LeafEnumRendererComponent {
549
654
  removable: boolean;
550
655
  remove: any;
551
656
  editable: boolean;
552
- static ɵfac: i0.ɵɵFactoryDeclaration<LeafEnumRendererComponent, never>;
553
- static ɵcmp: i0.ɵɵComponentDeclaration<LeafEnumRendererComponent, "nff-leaf-enum-renderer", never, { "leafEnum": { "alias": "leafEnum"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "control": { "alias": "control"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "remove": { "alias": "remove"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; }, {}, never, never, true, never>;
657
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<LeafEnumRendererComponent, never>;
658
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<LeafEnumRendererComponent, "nff-leaf-enum-renderer", never, { "leafEnum": { "alias": "leafEnum"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "control": { "alias": "control"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "remove": { "alias": "remove"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; }, {}, never, never, true, never>;
554
659
  }
555
660
 
556
661
  declare class NodeGroupListRendererComponent implements OnInit, AfterViewInit {
@@ -570,8 +675,8 @@ declare class NodeGroupListRendererComponent implements OnInit, AfterViewInit {
570
675
  setLastEditable(): void;
571
676
  asFormGroup(group: any): FormGroup;
572
677
  getTitle($index: number): string;
573
- static ɵfac: i0.ɵɵFactoryDeclaration<NodeGroupListRendererComponent, never>;
574
- static ɵcmp: i0.ɵɵComponentDeclaration<NodeGroupListRendererComponent, "nff-node-group-list-renderer", never, { "nodeGroupList": { "alias": "nodeGroupList"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "formArray": { "alias": "formArray"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "minItems": { "alias": "minItems"; "required": false; }; "maxItems": { "alias": "maxItems"; "required": false; }; }, { "message": "message"; }, never, never, true, never>;
678
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NodeGroupListRendererComponent, never>;
679
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NodeGroupListRendererComponent, "nff-node-group-list-renderer", never, { "nodeGroupList": { "alias": "nodeGroupList"; "required": false; }; "initialValue": { "alias": "initialValue"; "required": false; }; "formArray": { "alias": "formArray"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "minItems": { "alias": "minItems"; "required": false; }; "maxItems": { "alias": "maxItems"; "required": false; }; }, { "message": "message"; }, never, never, true, never>;
575
680
  }
576
681
 
577
682
  /**
@@ -581,13 +686,13 @@ declare class NodeGroupListRendererComponent implements OnInit, AfterViewInit {
581
686
  * names are the entry keys, so the key is edited as a *rename* (remove + re-add
582
687
  * the same control) committed on blur — see {@link renameEntry}.
583
688
  */
584
- declare class NodeMapRendererComponent implements OnInit {
689
+ declare class NodeMapRendererComponent implements OnChanges {
585
690
  nodeMap: NodeMap;
586
691
  formGroup: FormGroup<any>;
587
692
  editable: boolean;
588
693
  /** Ordered view of entry keys, kept stable across renames (`addControl` appends). */
589
694
  entryKeys: string[];
590
- ngOnInit(): void;
695
+ ngOnChanges(changes: SimpleChanges): void;
591
696
  /** The value schema as a leaf (used when `value.kind === 'leaf'`). */
592
697
  get valueLeaf(): Leaf;
593
698
  /** The value schema as a group (used when `value.kind === 'nodeGroup'`). */
@@ -606,8 +711,8 @@ declare class NodeMapRendererComponent implements OnInit {
606
711
  renameEntry(oldKey: string, rawKey: string): void;
607
712
  protected readonly asFormControl: typeof asFormControl;
608
713
  protected readonly asFormGroup: typeof asFormGroup;
609
- static ɵfac: i0.ɵɵFactoryDeclaration<NodeMapRendererComponent, never>;
610
- static ɵcmp: i0.ɵɵComponentDeclaration<NodeMapRendererComponent, "nff-node-map-renderer", never, { "nodeMap": { "alias": "nodeMap"; "required": false; }; "formGroup": { "alias": "formGroup"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; }, {}, never, never, true, never>;
714
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NodeMapRendererComponent, never>;
715
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NodeMapRendererComponent, "nff-node-map-renderer", never, { "nodeMap": { "alias": "nodeMap"; "required": false; }; "formGroup": { "alias": "formGroup"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; }, {}, never, never, true, never>;
611
716
  }
612
717
 
613
718
  export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, addMapEntry, buildControl, buildFormFromSchema, caseFields, defineSchema, removeMapEntry, renameMapEntry, resolveChoiceCase, switchChoiceCase };