ng-form-foundry 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FormControl, FormArray, FormGroup } from '@angular/forms';
2
2
  import * as i0 from '@angular/core';
3
- import { OnInit, EventEmitter, AfterViewInit, QueryList, ChangeDetectorRef } from '@angular/core';
3
+ import { OnInit, AfterViewInit, EventEmitter, ElementRef, QueryList, ChangeDetectorRef } 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;
@@ -10,6 +10,26 @@ type LeafBase = {
10
10
  required?: true | undefined;
11
11
  label?: string;
12
12
  description?: string;
13
+ /**
14
+ * The value may be `null` (JSON Schema `type: [T, 'null']`). Builds a nullable
15
+ * control (drops `nonNullable`), so `null` is a first-class value the
16
+ * constraint validators accept and that survives the round-trip. Distinct from
17
+ * {@link presence}: `nullable` is an explicit `null`, `presence` is an absent key.
18
+ */
19
+ nullable?: boolean;
20
+ /**
21
+ * Optional scalar whose *presence itself* is data (mirrors {@link NodeGroup.presence}).
22
+ * Rendered with an on/off toggle; the control is removed from the parent group
23
+ * when absent (so it drops from `form.value`) and re-added when toggled on. The
24
+ * builder omits it unless an initial value is supplied.
25
+ */
26
+ presence?: boolean;
27
+ /**
28
+ * Render the field read-only even when the surrounding form is editable.
29
+ * Combine with `default` to express a JSON Schema `const` (a fixed,
30
+ * display-only value); single-element `enum` is the alternative for a constant.
31
+ */
32
+ readOnly?: boolean;
13
33
  };
14
34
  type AnonLeaf = {
15
35
  [K in Leaf['type']]: {
@@ -19,10 +39,30 @@ type AnonLeaf = {
19
39
  type LeafString = LeafBase & {
20
40
  type: 'string';
21
41
  default?: LeafRuntimeType<'string'>;
42
+ /**
43
+ * Reject values that don't match this regular expression. Follows JSON Schema
44
+ * `pattern` semantics — an *unanchored* `RegExp.test`, so it matches anywhere
45
+ * in the value unless the pattern itself anchors with `^`/`$`.
46
+ */
47
+ pattern?: string;
48
+ /** Minimum string length (JSON Schema `minLength`). */
49
+ minLength?: number;
50
+ /** Maximum string length (JSON Schema `maxLength`). */
51
+ maxLength?: number;
52
+ /** Semantic string format (JSON Schema `format`); adds a matching validator. */
53
+ format?: 'email' | 'uri' | 'url';
22
54
  };
23
55
  type LeafNumber = LeafBase & {
24
56
  type: 'number';
25
57
  default?: LeafRuntimeType<'number'>;
58
+ /** Require a whole-number value (JSON Schema `type: 'integer'`). */
59
+ integer?: boolean;
60
+ /** Inclusive lower bound (JSON Schema `minimum`). */
61
+ min?: number;
62
+ /** Inclusive upper bound (JSON Schema `maximum`). */
63
+ max?: number;
64
+ /** Require the value to be an integer multiple of this number (JSON Schema `multipleOf`). */
65
+ multipleOf?: number;
26
66
  };
27
67
  type LeafBoolean = LeafBase & {
28
68
  type: 'boolean';
@@ -37,6 +77,8 @@ type LeafEnum = LeafBase & {
37
77
  type Appearance = {
38
78
  flatten?: boolean;
39
79
  noBorder?: boolean;
80
+ /** Start this node's section panel collapsed. Ignored when `flatten` is set. */
81
+ collapsed?: boolean;
40
82
  };
41
83
  type Leaf = LeafString | LeafNumber | LeafBoolean | LeafEnum;
42
84
  type LeafList<TKind extends Leaf['type'] = Leaf['type']> = {
@@ -75,29 +117,132 @@ type NodeGroup = {
75
117
  children: Record<string, NodeType>;
76
118
  appearance?: Appearance;
77
119
  };
120
+ /**
121
+ * One case of a {@link NodeChoice}: either a record of named fields (an object
122
+ * branch), or a single node (a *leaf-bodied* case — e.g. an `anyOf` branch that
123
+ * is a bare scalar). A single node is normalized to a one-field record keyed by
124
+ * its `name` when the form is built.
125
+ */
126
+ type ChoiceCase = Record<string, NodeType> | NodeType;
78
127
  /**
79
128
  * A discriminated selection: the user picks one `case`, and only that case's
80
129
  * fields are present. In the form it is a FormGroup holding a `__case` control
81
130
  * (the active case name) plus that case's field controls; switching the case
82
131
  * swaps the field controls.
132
+ *
133
+ * Cases may be **anonymous / auto-named** (any string key) — for JSON Schema
134
+ * `anyOf`/`oneOf` branches with no name. When a built form is seeded from inline
135
+ * data that carries no `__case`, the builder **infers** the active case from the
136
+ * data shape (the case whose fields best match), so a choice round-trips from
137
+ * real instance data. See the schema reference for the required-set / `const`
138
+ * discriminator recipe.
83
139
  */
84
140
  type NodeChoice = {
85
141
  kind: 'choice';
86
142
  name: string;
87
143
  label?: string;
88
- cases: Record<string, Record<string, NodeType>>;
144
+ cases: Record<string, ChoiceCase>;
145
+ /**
146
+ * Display labels for cases, keyed by case name — for anonymous/auto-named
147
+ * branches whose keys are not human-friendly. Falls back to the case name.
148
+ */
149
+ caseLabels?: Record<string, string>;
89
150
  default?: string;
90
151
  mandatory?: boolean;
152
+ /** Optional choice: rendered with an on/off toggle, omitted from the value when absent. */
153
+ presence?: boolean;
154
+ appearance?: Appearance;
91
155
  };
92
156
  /** The control name that records which case of a {@link NodeChoice} is active. */
93
157
  declare const CASE_KEY = "__case";
94
- type NodeType = Leaf | LeafList | NodeGroup | NodeGroupList | NodeChoice;
95
- type DFormControl<T extends NodeType> = T extends Leaf ? FormControl<LeafRuntimeType<T['type']>> : T extends LeafList ? FormArray<FormControl<LeafRuntimeType<T['type']>>> : T extends NodeGroup ? DFormGroup<T> : T extends NodeGroupList ? FormArray<DFormGroup<T['type']>> : T extends NodeChoice ? FormGroup<any> : never;
158
+ /**
159
+ * An open, arbitrary-keyed record: unlike {@link NodeGroup} (a fixed, declared
160
+ * key set), a map's keys are runtime data and every value conforms to one shared
161
+ * `value` schema. Maps JSON Schema `additionalProperties: <schema>` /
162
+ * `patternProperties`. In the form it is a `FormGroup` whose control *names* are
163
+ * the entry keys, so `getRawValue()` is the map object directly; the renderer
164
+ * lets the user add, remove, and rename entries.
165
+ */
166
+ type NodeMap = {
167
+ kind: 'map';
168
+ name: string;
169
+ label?: string;
170
+ description?: string;
171
+ /** The schema every entry's value conforms to. */
172
+ value: NodeType;
173
+ /** Label for the key column in the editor. Defaults to "Key". */
174
+ keyLabel?: string;
175
+ /** `patternProperties`: entry keys must match this regular expression. */
176
+ keyPattern?: string;
177
+ /** Minimum number of entries (JSON Schema `minProperties`). */
178
+ minEntries?: number;
179
+ /** Maximum number of entries (JSON Schema `maxProperties`). */
180
+ maxEntries?: number;
181
+ /** Optional map: rendered with an on/off toggle, omitted from the value when absent. */
182
+ presence?: boolean;
183
+ appearance?: Appearance;
184
+ };
185
+ type NodeType = Leaf | LeafList | NodeGroup | NodeGroupList | NodeChoice | NodeMap;
186
+ type DFormControl<T extends NodeType> = T extends Leaf ? FormControl<LeafRuntimeType<T['type']>> : T extends LeafList ? FormArray<FormControl<LeafRuntimeType<T['type']>>> : T extends NodeGroup ? DFormGroup<T> : T extends NodeGroupList ? FormArray<DFormGroup<T['type']>> : T extends NodeChoice ? FormGroup<any> : T extends NodeMap ? FormGroup<any> : never;
96
187
  type FormGroupType<T extends NodeGroup> = {
97
188
  [TChild in keyof T['children']]: DFormControl<T['children'][TChild]>;
98
189
  };
99
190
  type DFormGroup<T extends NodeGroup> = FormGroup<FormGroupType<T>>;
100
191
 
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
+ /**
210
+ * Normalize a {@link ChoiceCase} to a field record. A field record is returned
211
+ * as-is; a single node (a leaf-bodied case, e.g. a scalar `anyOf` branch) becomes
212
+ * a one-field record keyed by the node's `name`. The discriminant is a top-level
213
+ * `kind` string, which a field record never has (its keys are field names).
214
+ */
215
+ declare function caseFields(body: ChoiceCase): Record<string, NodeType>;
216
+ /**
217
+ * The active case of a choice: an explicit `__case` in the initial value, else
218
+ * the case whose fields best match the initial data (inline wire data carries no
219
+ * `__case`), else the schema `default`. This lets a choice seed from real
220
+ * instance data whose branch is discriminated by which fields are present.
221
+ */
222
+ declare function resolveChoiceCase(choice: NodeChoice, initial?: Record<string, unknown> | null): string | undefined;
223
+ /**
224
+ * Switch a choice's FormGroup to `caseName`: sets `__case`, removes every other
225
+ * control, and builds `caseName`'s fields (normalized via {@link caseFields})
226
+ * with their defaults. An unknown case name leaves only `__case`.
227
+ */
228
+ declare function switchChoiceCase(group: FormGroup, choice: NodeChoice, caseName: string): void;
229
+ /**
230
+ * Append a map entry built from `map.value` and return its committed key, or
231
+ * `null` when nothing was added. With no `key`, the first free `keyN`
232
+ * placeholder is generated (not checked against `keyPattern` — placeholders are
233
+ * meant to be renamed). An explicit `key` is rejected when it duplicates an
234
+ * existing entry or violates `keyPattern`. Rejects when `maxEntries` is reached.
235
+ */
236
+ declare function addMapEntry(group: FormGroup, map: NodeMap, key?: string): string | null;
237
+ /**
238
+ * 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.
242
+ */
243
+ declare function renameMapEntry(group: FormGroup, map: NodeMap, oldKey: string, newKey: string): boolean;
244
+ /** Remove entry `key` unless the map is at `minEntries`. Returns whether it was removed. */
245
+ declare function removeMapEntry(group: FormGroup, map: NodeMap, key: string): boolean;
101
246
  declare function buildControl<T extends NodeType>(node: T, initial?: unknown | null): DFormControl<T> | FormControl<LeafRuntimeType<any>> | FormArray<any>;
102
247
  declare function buildFormFromSchema<S extends NodeGroup>(schema: S, initial?: Record<string, unknown> | null): DFormGroup<S>;
103
248
  /**
@@ -120,16 +265,26 @@ declare function asFormArray(control: any): FormArray;
120
265
  declare function asFormGroup(control: any): FormGroup;
121
266
 
122
267
  declare class DynamicRecursiveFormComponent implements OnInit {
123
- schema: NodeGroup;
124
- initialValue: any;
125
- formGroup: FormGroup<any>;
126
- index: number | null;
127
- removable: boolean;
128
- remove: EventEmitter<any>;
129
- title: string;
130
- editable: boolean;
131
- addButtonCallback: ((index: number) => void) | null;
132
- root: boolean;
268
+ /** The form-description schema to render (a root or nested `NodeGroup`). */
269
+ readonly schema: i0.InputSignal<NodeGroup>;
270
+ /** Optional value object to seed the form; keyed by the schema's `children` keys. */
271
+ readonly initialValue: i0.InputSignal<Record<string, unknown> | null>;
272
+ /** The reactive group this form binds to. Defaults to an empty group. */
273
+ readonly formGroup: i0.InputSignal<FormGroup<any>>;
274
+ /** Index of this form within a parent list, used by `addButtonCallback`. */
275
+ readonly index: i0.InputSignal<number | null>;
276
+ /** Whether this form may be removed from a parent list (shows a remove control). */
277
+ readonly removable: i0.InputSignal<boolean>;
278
+ /** Emitted when the user removes this form from a parent list. */
279
+ readonly remove: i0.OutputEmitterRef<void>;
280
+ /** Card/section title; falls back to the schema label or name. */
281
+ readonly title: i0.InputSignal<string | undefined>;
282
+ /** Whether fields accept input. Two-way: also toggled by the built-in edit control. */
283
+ readonly editable: i0.ModelSignal<boolean>;
284
+ /** Invoked with {@link index} to append a new sibling form to a parent list. */
285
+ readonly addButtonCallback: i0.InputSignal<((index: number) => void) | null>;
286
+ /** True when the schema is a root group (rendered flat, without a wrapping card). */
287
+ readonly root: i0.Signal<boolean>;
133
288
  ngOnInit(): void;
134
289
  get nodeGroupChildrenList(): Array<{
135
290
  key: string;
@@ -141,19 +296,41 @@ declare class DynamicRecursiveFormComponent implements OnInit {
141
296
  * group from `form.value`; adding it rebuilds the sub-group from its schema.
142
297
  */
143
298
  togglePresence(key: string, schema: NodeGroup, present: boolean): void;
299
+ /** The key of the presence leaf the user just enabled; its field grabs focus when rendered. */
300
+ protected presenceFocusKey: string | null;
301
+ /**
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.
305
+ */
306
+ toggleLeafPresence(key: string, schema: Leaf, present: boolean): void;
307
+ /**
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.
311
+ */
312
+ toggleNodePresence(key: string, schema: NodeType, present: boolean): void;
144
313
  protected readonly CASE_KEY = "__case";
145
314
  objectKeys(obj: Record<string, unknown>): string[];
146
315
  /** The active case name of a choice control, or null if none is selected. */
147
316
  activeCase(key: string): string | null;
148
317
  /** A synthetic flattened NodeGroup used to render the active case's fields against the choice's FormGroup. */
149
318
  caseAsGroup(choice: NodeChoice, caseName: string): NodeGroup;
150
- /** Swap a choice's field controls when the selected case changes. */
319
+ /** The display label for a case: the schema's `caseLabels` entry, else the case name. */
320
+ caseLabel(choice: NodeChoice, caseName: string): string;
321
+ /**
322
+ * A copy of a group flagged to render its fields inline (no inner panel). Used
323
+ * for a presence group's body, whose container is the presence panel itself, so
324
+ * the group's own section panel would be a redundant second box.
325
+ */
326
+ flatGroup(group: NodeGroup): NodeGroup;
327
+ /** Swap a choice's field controls when the selected case changes. Delegates to {@link switchChoiceCase}. */
151
328
  switchCase(key: string, choice: NodeChoice, caseName: string): void;
152
329
  protected readonly asFormGroup: typeof asFormGroup;
153
330
  protected readonly asFormArray: typeof asFormArray;
154
331
  protected readonly asFormControl: typeof asFormControl;
155
332
  static ɵfac: i0.ɵɵFactoryDeclaration<DynamicRecursiveFormComponent, never>;
156
- static ɵcmp: i0.ɵɵComponentDeclaration<DynamicRecursiveFormComponent, "nff-dynamic-recursive-form", never, { "schema": { "alias": "schema"; "required": true; }; "initialValue": { "alias": "initialValue"; "required": false; }; "formGroup": { "alias": "formGroup"; "required": false; }; "index": { "alias": "index"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "title": { "alias": "title"; "required": false; }; "editable": { "alias": "editable"; "required": false; }; "addButtonCallback": { "alias": "addButtonCallback"; "required": false; }; }, { "remove": "remove"; }, never, never, true, 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>;
157
334
  }
158
335
 
159
336
  /** Metadata on a list-container node that lets the tree add items to its FormArray. */
@@ -163,21 +340,30 @@ interface ListRef {
163
340
  itemLabel: string;
164
341
  minItems: number;
165
342
  }
343
+ /** An absent optional (presence) child, offered by its parent node's "+ Optional field" menu. */
344
+ interface OptionalEntry {
345
+ key: string;
346
+ schema: NodeType;
347
+ label: string;
348
+ /** Index in the parent schema's children iteration; keeps the menu in schema order. */
349
+ order: number;
350
+ }
166
351
  /** A navigable node in the config tree. Groups and list items are tree nodes; leaves are their detail. */
167
352
  interface TreeNode {
168
353
  id: string;
169
354
  label: string;
170
355
  children: TreeNode[];
171
- /** Leaves editable when this node is selected. Empty for list-container nodes. */
356
+ /** Leaves editable when this node is selected. A presence leaf carries its `optional` entry so it can be removed back to the menu. */
172
357
  leaves: {
173
358
  key: string;
174
359
  node: Leaf;
360
+ optional?: OptionalEntry;
175
361
  }[];
176
362
  leafLists: {
177
363
  key: string;
178
364
  node: LeafList;
179
365
  }[];
180
- /** The FormGroup holding this node's leaves, or null for a list-container node. */
366
+ /** The FormGroup holding this node's leaves, or null for a list-container or map node. */
181
367
  group: FormGroup | null;
182
368
  /** Present on a nodeGroupList node: lets a `+` add an item. */
183
369
  list?: ListRef;
@@ -186,26 +372,51 @@ interface TreeNode {
186
372
  array: FormArray;
187
373
  index: number;
188
374
  };
189
- /** Present on an optional (presence) group node: lets a checkbox add/remove the group. */
190
- presence?: {
191
- parentGroup: FormGroup;
375
+ /** Absent optional children of this node, offered by its "+ Optional field" menu. */
376
+ optionals?: OptionalEntry[];
377
+ /** Present on a present optional child node: drives the row's remove control; removal returns `entry` to the parent's menu. */
378
+ presenceRemovable?: {
379
+ entry: OptionalEntry;
380
+ };
381
+ /** Present on a choice node: the choice schema and its FormGroup (holding `__case` + the active case's fields). */
382
+ choice?: {
383
+ schema: NodeChoice;
384
+ group: FormGroup;
385
+ };
386
+ /** Present on a map node. `complex` maps expand entries as child nodes; leaf-valued maps edit inline in the detail pane. */
387
+ map?: {
388
+ schema: NodeMap;
389
+ group: FormGroup;
390
+ complex: boolean;
391
+ };
392
+ /** Present on a complex-map entry node: addresses the entry in its map for remove/rename. */
393
+ mapEntry?: {
394
+ mapGroup: FormGroup;
395
+ mapSchema: NodeMap;
192
396
  key: string;
193
- schema: NodeGroup;
194
397
  };
195
- /** For a presence node: whether the group is currently present. */
196
- present?: boolean;
197
398
  }
198
399
  /**
199
- * A tree/detail editor for a schema-built form: the structure (containers, lists,
200
- * groups) is a tree on the left, and selecting a node shows that node's leaf
201
- * fields for editing on the right. A `+` on a list node adds an entry; a delete
202
- * button on each item removes it. An alternative to the all-in-one
203
- * {@link DynamicRecursiveFormComponent} for large configs.
400
+ * 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.
406
+ *
407
+ * 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.
204
412
  */
205
413
  declare class ConfigEditorComponent implements OnInit {
206
- schema: NodeGroup;
207
- formGroup: FormGroup;
208
- editable: boolean;
414
+ /** The form-description schema whose structure the tree renders. */
415
+ readonly schema: i0.InputSignal<NodeGroup>;
416
+ /** The schema-built reactive group the editor binds to and mutates. */
417
+ readonly formGroup: i0.InputSignal<FormGroup<any>>;
418
+ /** Whether fields accept input and structural controls (add/remove/menus) show. */
419
+ readonly editable: i0.InputSignal<boolean>;
209
420
  root: TreeNode;
210
421
  selected: TreeNode | null;
211
422
  readonly expanded: Set<string>;
@@ -221,32 +432,92 @@ declare class ConfigEditorComponent implements OnInit {
221
432
  */
222
433
  pathTo(target: TreeNode): TreeNode[];
223
434
  toggle(node: TreeNode): void;
435
+ /** Whether the row shows an expand twisty: it has child rows to reveal (children or an optionals menu row). */
436
+ hasExpandableContent(node: TreeNode): boolean;
437
+ /**
438
+ * Whether the node's form subtree holds a validation error. Group/choice, list,
439
+ * and map nodes each check their backing control; `invalid` aggregates over
440
+ * descendants, so an error anywhere below lights every ancestor row.
441
+ */
442
+ hasError(node: TreeNode): boolean;
224
443
  /** Append a new item to a list node's FormArray and to the tree, then select it. */
225
444
  addItem(listNode: TreeNode): void;
226
445
  /** Remove a list item from its FormArray and the tree (down to `minItems`). */
227
446
  removeItem(listNode: TreeNode, item: TreeNode): void;
228
- /** Enable or disable an optional (presence) group by adding/removing its control. */
229
- setPresence(node: TreeNode, present: boolean): void;
447
+ /** The key of the optional leaf the user just added; its detail-pane field grabs focus when rendered. */
448
+ protected focusLeafKey: string | null;
449
+ /** Add an absent optional child from the menu: build its control, place it in the tree, and select it. */
450
+ addOptional(node: TreeNode, entry: OptionalEntry): void;
451
+ /** Remove a present optional child node, returning its entry to the parent's menu. */
452
+ 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
+ /** The active case name of a choice node, or null when none is selected. */
460
+ activeCase(node: TreeNode): string | null;
461
+ /** The display label of a choice node's active case, or null when no case is selected. */
462
+ activeCaseLabel(node: TreeNode): string | null;
463
+ /** 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. */
466
+ switchTreeCase(node: TreeNode, caseName: string): void;
467
+ /** Append a new entry to a complex map node under a generated unique key, then select it. */
468
+ addTreeMapEntry(mapNode: TreeNode): void;
469
+ /** Remove a complex map entry (down to `minEntries`) from the form group and the tree. */
470
+ removeTreeMapEntry(mapNode: TreeNode, entryNode: TreeNode): void;
471
+ /** Commit a rename-on-blur of a map entry's key; on success the node is relabeled. */
472
+ renameTreeMapEntry(entryNode: TreeNode, rawKey: string): void;
473
+ /** Whether a map node is at `maxEntries` (the add control is hidden). */
474
+ mapAtMax(node: TreeNode | undefined): boolean;
475
+ /** Whether a map node is at `minEntries` (entry remove controls are hidden). */
476
+ mapAtMin(node: TreeNode | undefined): boolean;
230
477
  protected readonly asFormControl: typeof asFormControl;
231
478
  protected readonly asFormArray: typeof asFormArray;
232
- private placeholder;
479
+ protected readonly CASE_KEY = "__case";
480
+ objectKeys(obj: Record<string, unknown>): string[];
233
481
  /** Re-index and re-label a list node's item children (just "#n") after add/remove. */
234
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;
235
489
  private buildTree;
490
+ /** Build the tree node for a single non-leaf child, dispatching on its kind. Null when its control is missing. */
491
+ private buildChildNode;
492
+ /** A node's display label: its schema `label`, else its record key. */
493
+ private labelOf;
236
494
  static ɵfac: i0.ɵɵFactoryDeclaration<ConfigEditorComponent, never>;
237
- static ɵcmp: i0.ɵɵComponentDeclaration<ConfigEditorComponent, "nff-config-editor", never, { "schema": { "alias": "schema"; "required": true; }; "formGroup": { "alias": "formGroup"; "required": true; }; "editable": { "alias": "editable"; "required": false; }; }, {}, never, never, true, 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>;
238
496
  }
239
497
 
240
- declare class LeafRendererComponent implements OnInit {
498
+ declare class LeafRendererComponent implements OnInit, AfterViewInit {
499
+ private readonly elementRef;
241
500
  leaf_: Leaf | AnonLeaf;
242
501
  control: FormControl;
243
502
  initialValue?: any;
244
503
  removable: boolean;
245
504
  editable: boolean;
505
+ /** Focus the field once rendered — for fields the user just added (e.g. an enabled presence leaf). */
506
+ autofocus: boolean;
246
507
  remove: EventEmitter<number>;
508
+ constructor(elementRef: ElementRef<HTMLElement>);
247
509
  ngOnInit(): void;
510
+ ngAfterViewInit(): void;
511
+ /** Whether this field accepts input: the form is editable and the leaf is not `readOnly`. */
512
+ get fieldEditable(): boolean;
513
+ /**
514
+ * A human-readable message for the control's active validation error, or `''`
515
+ * when valid. `mat-form-field` only shows it once the field is in an error
516
+ * state (invalid and touched), so it can be bound unconditionally.
517
+ */
518
+ get errorText(): string;
248
519
  static ɵfac: i0.ɵɵFactoryDeclaration<LeafRendererComponent, never>;
249
- 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; }; }, { "remove": "remove"; }, never, never, true, 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>;
250
521
  }
251
522
 
252
523
  declare class LeafListRendererComponent implements OnInit {
@@ -303,5 +574,41 @@ declare class NodeGroupListRendererComponent implements OnInit, AfterViewInit {
303
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>;
304
575
  }
305
576
 
306
- export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, buildControl, buildFormFromSchema, defineSchema };
307
- export type { AnonLeaf, Appearance, DFormControl, DFormGroup, FormGroupType, Leaf, LeafBase, LeafBoolean, LeafEnum, LeafList, LeafNumber, LeafRuntimeType, LeafString, NodeChoice, NodeGroup, NodeGroupList, NodeType };
577
+ /**
578
+ * Renders a {@link NodeMap}: an open, arbitrary-keyed record. Each entry is a row
579
+ * with an editable key and the shared value schema's control; the user can add,
580
+ * remove, and rename entries. The map's control is a `FormGroup` whose control
581
+ * names are the entry keys, so the key is edited as a *rename* (remove + re-add
582
+ * the same control) committed on blur — see {@link renameEntry}.
583
+ */
584
+ declare class NodeMapRendererComponent implements OnInit {
585
+ nodeMap: NodeMap;
586
+ formGroup: FormGroup<any>;
587
+ editable: boolean;
588
+ /** Ordered view of entry keys, kept stable across renames (`addControl` appends). */
589
+ entryKeys: string[];
590
+ ngOnInit(): void;
591
+ /** The value schema as a leaf (used when `value.kind === 'leaf'`). */
592
+ get valueLeaf(): Leaf;
593
+ /** The value schema as a group (used when `value.kind === 'nodeGroup'`). */
594
+ get valueGroup(): NodeGroup;
595
+ get atMax(): boolean;
596
+ get atMin(): boolean;
597
+ /** Append a new entry under a unique placeholder key. Delegates to {@link addMapEntry}. */
598
+ addEntry(): void;
599
+ /** Drop an entry (its control leaves the group, so it drops from the value). Delegates to {@link removeMapEntry}. */
600
+ removeEntry(key: string): void;
601
+ /**
602
+ * Commit an edited key by renaming its control once (the value is preserved).
603
+ * An empty, unchanged, duplicate, or `keyPattern`-violating key is a no-op,
604
+ * leaving the entry under its current name. Delegates to {@link renameMapEntry}.
605
+ */
606
+ renameEntry(oldKey: string, rawKey: string): void;
607
+ protected readonly asFormControl: typeof asFormControl;
608
+ 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>;
611
+ }
612
+
613
+ export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, addMapEntry, buildControl, buildFormFromSchema, caseFields, defineSchema, removeMapEntry, renameMapEntry, resolveChoiceCase, switchChoiceCase };
614
+ export type { AnonLeaf, Appearance, ChoiceCase, DFormControl, DFormGroup, FormGroupType, Leaf, LeafBase, LeafBoolean, LeafEnum, LeafList, LeafNumber, LeafRuntimeType, LeafString, NodeChoice, NodeGroup, NodeGroupList, NodeMap, NodeType };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ng-form-foundry",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Build fully-typed Angular Reactive Forms and Angular Material UI from a declarative, recursive form-description schema.",
5
5
  "keywords": [
6
6
  "angular",