@react-typed-forms/schemas 14.4.1 → 15.0.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/lib/util.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ControlActionHandler, ControlDataVisitor, ControlDefinition, DataControlDefinition, DisplayOnlyRenderOptions, GroupRenderOptions } from "./controlDefinition";
1
+ import { ControlActionHandler, ControlDataVisitor, ControlDefinition, DataControlDefinition, DisplayOnlyRenderOptions, FormTree, GroupRenderOptions } from "./controlDefinition";
2
2
  import { MutableRefObject } from "react";
3
3
  import { CompoundField, FieldOption, SchemaDataNode, SchemaField, SchemaNode } from "./schemaField";
4
4
  import { Control } from "@react-typed-forms/core";
@@ -105,16 +105,25 @@ export declare function findNonDataGroups(controls: ControlDefinition[]): Contro
105
105
  * Adds missing controls to the provided control definitions based on the schema fields.
106
106
  * @param fields - The schema fields to use for adding missing controls.
107
107
  * @param controls - The control definitions to add missing controls to.
108
+ * @param warning - An optional function to call with warning messages.
108
109
  * @returns The control definitions with missing controls added.
109
110
  */
110
- export declare function addMissingControls(fields: SchemaField[], controls: ControlDefinition[]): ControlDefinition[];
111
+ export declare function addMissingControls(fields: SchemaField[], controls: ControlDefinition[], warning?: (msg: string) => void): ControlDefinition[];
111
112
  /**
112
113
  * Adds missing controls to the provided control definitions based on the schema fields.
113
114
  * @param schema - The root schema node to use for adding missing controls.
114
115
  * @param controls - The control definitions to add missing controls to.
116
+ * @param warning - An optional function to call with warning messages.
115
117
  * @returns The control definitions with missing controls added.
116
118
  */
117
- export declare function addMissingControlsForSchema(schema: SchemaNode, controls: ControlDefinition[]): ControlDefinition[];
119
+ export declare function addMissingControlsForSchema(schema: SchemaNode, controls: ControlDefinition[], warning?: (msg: string) => void): ControlDefinition[];
120
+ /**
121
+ * Adds missing controls to the provided form tree based on the schema fields.
122
+ * @param schema - The root schema node to use for adding missing controls.
123
+ * @param tree - The form tree to add missing controls to.
124
+ * @param warning - An optional function to call with warning messages.
125
+ */
126
+ export declare function addMissingControlsToForm(schema: SchemaNode, tree: FormTree, warning?: (msg: string) => void): void;
118
127
  /**
119
128
  * Custom hook to use an updated reference.
120
129
  * @param a - The value to create a reference for.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-typed-forms/schemas",
3
- "version": "14.4.1",
3
+ "version": "15.0.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -227,7 +227,7 @@ export function useControlDefinitionForSchema(
227
227
 
228
228
  export interface EditorGroup {
229
229
  parent: string;
230
- group: GroupedControlsDefinition;
230
+ group: ControlDefinition;
231
231
  }
232
232
 
233
233
  export interface CustomRenderOptions {
@@ -551,60 +551,99 @@ export interface FormNode {
551
551
  definition: ControlDefinition;
552
552
  tree: FormTree;
553
553
  parent?: FormNode;
554
- getChildNodes(dontFollowRef?: boolean): FormNode[];
554
+ getChildNodes(): FormNode[];
555
555
  }
556
556
 
557
- export function defaultGetChildNodes(
558
- parent: FormNode,
559
- children?: ControlDefinition[] | null,
560
- ) {
561
- return (
562
- children?.map((x, i) => nodeForControl(x, parent.tree, i, parent)) ?? []
563
- );
557
+ class FormNodeImpl implements FormNode {
558
+ public children: FormNode[] = [];
559
+ constructor(
560
+ public id: string,
561
+ public definition: ControlDefinition,
562
+ public tree: FormTree,
563
+ public parent?: FormNode,
564
+ ) {}
565
+
566
+ getChildNodes() {
567
+ return this.children;
568
+ }
564
569
  }
565
570
 
566
571
  export interface FormTreeLookup {
567
572
  getForm(formId: string): FormTree | undefined;
568
573
  }
569
- export interface FormTree extends FormTreeLookup {
570
- rootNode: FormNode;
571
- getChildrenForId(id: string, parent: FormNode): FormNode[];
572
- }
574
+ export abstract class FormTree implements FormTreeLookup {
575
+ abstract rootNode: FormNode;
576
+ abstract getByRefId(id: string): FormNode | undefined;
577
+ abstract addChild(parent: FormNode, control: ControlDefinition): FormNode;
578
+
579
+ abstract getForm(formId: string): FormTree | undefined;
580
+ createTempNode(
581
+ id: string,
582
+ definition: ControlDefinition,
583
+ children?: FormNode[],
584
+ parent?: FormNode,
585
+ ): FormNode {
586
+ const tempNode = {
587
+ id,
588
+ definition,
589
+ tree: this,
590
+ parent,
591
+ getChildNodes: () =>
592
+ children ?? this.createChildNodes(tempNode, definition.children),
593
+ } as FormNode;
594
+ return tempNode;
595
+ }
573
596
 
574
- export function wrapFormNode(
575
- node: FormNode,
576
- getChildren?: (dontFollowRef?: boolean) => FormNode[],
577
- ): FormNode {
578
- return {
579
- ...node,
580
- getChildNodes: getChildren ?? ((dfr) => node.getChildNodes(dfr)),
581
- };
597
+ createChildNodes(
598
+ parent: FormNode,
599
+ definitions: ControlDefinition[] | undefined | null,
600
+ ): FormNode[] {
601
+ return (
602
+ definitions?.map((x, i) =>
603
+ this.createTempNode(parent.id + "_" + i, x, undefined, parent),
604
+ ) ?? []
605
+ );
606
+ }
582
607
  }
583
- export function nodeForControl(
584
- definition: ControlDefinition,
585
- tree: FormTree,
586
- indexOrId?: number | string,
587
- parent?: FormNode,
588
- ): FormNode {
589
- return {
590
- id: parent ? parent.id + "/" + indexOrId : "",
591
- definition,
592
- tree,
593
- parent,
594
- getChildNodes(dontFollowRef?: boolean): FormNode[] {
595
- let children = this.definition.children;
596
- if (!dontFollowRef && this.definition.childRefId) {
597
- return this.tree.getChildrenForId(this.definition.childRefId, this);
598
- }
599
- return defaultGetChildNodes(this, children);
600
- },
601
- };
608
+
609
+ class FormTreeImpl extends FormTree {
610
+ controlMap: Record<string, FormNode> = {};
611
+ rootNode: FormNode;
612
+ idCount = 1;
613
+
614
+ constructor(private forms: FormTreeLookup) {
615
+ super();
616
+ this.rootNode = new FormNodeImpl("", { type: "Group" }, this);
617
+ }
618
+
619
+ getByRefId(id: string): FormNode | undefined {
620
+ return this.controlMap[id];
621
+ }
622
+
623
+ register(node: FormNode) {
624
+ this.controlMap[node.id] = node;
625
+ node.getChildNodes().forEach((x) => this.register(x));
626
+ }
627
+ addChild(parent: FormNode, control: ControlDefinition): FormNode {
628
+ const node = new FormNodeImpl(
629
+ control.id ? control.id : "c" + this.idCount++,
630
+ control,
631
+ this,
632
+ parent,
633
+ );
634
+ control.children?.forEach((x) => this.addChild(node, x));
635
+ parent.getChildNodes().push(node);
636
+ this.register(node);
637
+ return node;
638
+ }
639
+
640
+ getForm(formId: string): FormTree | undefined {
641
+ return this.forms.getForm(formId);
642
+ }
602
643
  }
603
644
 
604
645
  export function legacyFormNode(definition: ControlDefinition) {
605
- return createFormLookup({ $legacy: [definition] })
606
- .getForm("$legacy")!
607
- .rootNode.getChildNodes()[0];
646
+ return createFormTree([definition]).rootNode.getChildNodes()[0];
608
647
  }
609
648
 
610
649
  function getControlIds(
@@ -616,40 +655,13 @@ function getControlIds(
616
655
  : [[definition.id, definition], ...childEntries];
617
656
  }
618
657
 
619
- export function createFormTreeWithRoot(
620
- createRootNode: (tree: FormTree) => FormNode,
621
- getChildrenForId: (id: string, parent: FormNode) => FormNode[],
622
- getForm: FormTreeLookup = { getForm: () => undefined },
623
- ): FormTree {
624
- const tree = {
625
- ...getForm,
626
- getChildrenForId,
627
- rootNode: undefined! as FormNode,
628
- } satisfies FormTree;
629
- tree.rootNode = createRootNode(tree);
630
- return tree;
631
- }
632
-
633
658
  export function createFormTree(
634
659
  controls: ControlDefinition[],
635
660
  getForm: FormTreeLookup = { getForm: () => undefined },
636
661
  ): FormTree {
637
- const controlMap = Object.fromEntries(controls.flatMap(getControlIds));
638
- return createFormTreeWithRoot(
639
- (tree) =>
640
- nodeForControl(
641
- { children: controls, type: ControlDefinitionType.Group },
642
- tree,
643
- ),
644
- (id: string, parent: FormNode) => {
645
- return (
646
- controlMap[id]?.children?.map((x, i) =>
647
- nodeForControl(x, parent.tree, i, parent),
648
- ) ?? []
649
- );
650
- },
651
- getForm,
652
- );
662
+ const tree = new FormTreeImpl(getForm);
663
+ controls.forEach((x) => tree.addChild(tree.rootNode, x));
664
+ return tree;
653
665
  }
654
666
 
655
667
  export function createFormLookup<A extends Record<string, ControlDefinition[]>>(
@@ -514,6 +514,7 @@ export function useControlRendererComponent(
514
514
  formNode,
515
515
  });
516
516
 
517
+ if (formNode == null) debugger;
517
518
  const Component = useCallback(() => {
518
519
  const stopTracking = useComponentTracking();
519
520
 
@@ -643,8 +644,18 @@ export function useControlRendererComponent(
643
644
  formOptions: myOptions,
644
645
  }),
645
646
  ) ?? [];
647
+ const otherChildNodes =
648
+ definition.childRefId &&
649
+ formNode.tree.getByRefId(definition.childRefId)?.getChildNodes();
650
+
646
651
  const labelAndChildren = renderControlLayout({
647
- formNode,
652
+ formNode: otherChildNodes
653
+ ? formNode.tree.createTempNode(
654
+ formNode.id,
655
+ definition,
656
+ otherChildNodes,
657
+ )
658
+ : formNode,
648
659
  definition: c,
649
660
  renderer,
650
661
  renderChild: (k, child, options) => {
package/src/hooks.tsx CHANGED
@@ -55,6 +55,16 @@ export type UseEvalExpressionHook = (
55
55
  coerce: (v: any) => any,
56
56
  ) => DynamicHookGenerator<Control<any> | undefined, ControlDataContext>;
57
57
 
58
+ export function optionalHook(
59
+ expr: EntityExpression | undefined | null,
60
+ useHook: UseEvalExpressionHook,
61
+ coerce: (v: any) => any,
62
+ ):
63
+ | DynamicHookGenerator<Control<any> | undefined, ControlDataContext>
64
+ | undefined {
65
+ return expr && expr.type ? useHook(expr, coerce) : undefined;
66
+ }
67
+
58
68
  export function useEvalVisibilityHook(
59
69
  useEvalExpressionHook: UseEvalExpressionHook,
60
70
  definition: ControlDefinition,
package/src/util.ts CHANGED
@@ -3,10 +3,13 @@ import {
3
3
  ControlDataVisitor,
4
4
  ControlDefinition,
5
5
  ControlDefinitionType,
6
+ createFormTree,
6
7
  DataControlDefinition,
7
8
  DataRenderType,
8
9
  DisplayOnlyRenderOptions,
9
10
  fieldPathForDefinition,
11
+ FormNode,
12
+ FormTree,
10
13
  GroupRenderOptions,
11
14
  isAutoCompleteClasses,
12
15
  isCheckEntryClasses,
@@ -333,77 +336,123 @@ export function findNonDataGroups(
333
336
  * Adds missing controls to the provided control definitions based on the schema fields.
334
337
  * @param fields - The schema fields to use for adding missing controls.
335
338
  * @param controls - The control definitions to add missing controls to.
339
+ * @param warning - An optional function to call with warning messages.
336
340
  * @returns The control definitions with missing controls added.
337
341
  */
338
342
  export function addMissingControls(
339
343
  fields: SchemaField[],
340
344
  controls: ControlDefinition[],
345
+ warning?: (msg: string) => void,
341
346
  ) {
342
- return addMissingControlsForSchema(rootSchemaNode(fields), controls);
347
+ return addMissingControlsForSchema(rootSchemaNode(fields), controls, warning);
343
348
  }
344
349
 
345
- interface ControlAndSchema {
346
- control: ControlDefinition;
347
- children: ControlAndSchema[];
348
- schema?: SchemaNode;
349
- parent?: ControlAndSchema;
350
+ function registerSchemaEntries(formNode: FormNode, parentSchema: SchemaNode) {
351
+ const formToSchema: Record<string, SchemaNode> = {};
352
+ const schemaToForm: Record<string, FormNode[]> = {};
353
+ function register(node: FormNode, parentSchema: SchemaNode) {
354
+ const c = node.definition;
355
+ const controlPath = fieldPathForDefinition(c);
356
+ let dataSchema = controlPath
357
+ ? schemaForFieldPath(controlPath, parentSchema)
358
+ : undefined;
359
+ if (isGroupControl(c) && dataSchema == null) dataSchema = parentSchema;
360
+ if (dataSchema) {
361
+ formToSchema[node.id] = dataSchema;
362
+ const formNodes = schemaToForm[dataSchema.id] ?? [];
363
+ formNodes.push(node);
364
+ schemaToForm[dataSchema.id] = formNodes;
365
+ }
366
+ node
367
+ .getChildNodes()
368
+ .forEach((x) => register(x, dataSchema ?? parentSchema));
369
+ }
370
+ register(formNode, parentSchema);
371
+ return { formToSchema, schemaToForm, register };
350
372
  }
373
+
351
374
  /**
352
375
  * Adds missing controls to the provided control definitions based on the schema fields.
353
376
  * @param schema - The root schema node to use for adding missing controls.
354
377
  * @param controls - The control definitions to add missing controls to.
378
+ * @param warning - An optional function to call with warning messages.
355
379
  * @returns The control definitions with missing controls added.
356
380
  */
357
381
  export function addMissingControlsForSchema(
358
382
  schema: SchemaNode,
359
383
  controls: ControlDefinition[],
384
+ warning?: (msg: string) => void,
360
385
  ) {
361
- const controlMap: { [k: string]: ControlAndSchema } = {};
362
- const schemaControlMap: { [k: string]: ControlAndSchema[] } = {};
363
- const rootControls = controls.map((c) => toControlAndSchema(c, schema));
364
- const rootSchema = { schema, children: rootControls } as ControlAndSchema;
365
- addSchemaMapEntry("", rootSchema);
366
- rootControls.forEach(addReferences);
367
- const fields = schema.getChildNodes();
368
- fields.forEach(addMissing);
369
- return rootControls.map(toDefinition);
386
+ const tree = createFormTree(controls);
387
+ addMissingControlsToForm(schema, tree, warning);
388
+ return toDefinition(tree.rootNode).children ?? [];
370
389
 
371
- function toDefinition(c: ControlAndSchema): ControlDefinition {
372
- const children = c.children.length ? c.children.map(toDefinition) : null;
373
- return { ...c.control, children };
390
+ function toDefinition(c: FormNode): ControlDefinition {
391
+ const children = c.getChildNodes().length
392
+ ? c.getChildNodes().map(toDefinition)
393
+ : null;
394
+ return { ...c.definition, children };
374
395
  }
396
+ }
397
+
398
+ /**
399
+ * Adds missing controls to the provided form tree based on the schema fields.
400
+ * @param schema - The root schema node to use for adding missing controls.
401
+ * @param tree - The form tree to add missing controls to.
402
+ * @param warning - An optional function to call with warning messages.
403
+ */
404
+ export function addMissingControlsToForm(
405
+ schema: SchemaNode,
406
+ tree: FormTree,
407
+ warning?: (msg: string) => void,
408
+ ): void {
409
+ const { formToSchema, schemaToForm, register } = registerSchemaEntries(
410
+ tree.rootNode,
411
+ schema,
412
+ );
413
+
414
+ schema.getChildNodes().forEach(addMissing);
415
+ return;
375
416
 
376
417
  function addMissing(schemaNode: SchemaNode) {
377
418
  if (fieldHasTag(schemaNode.field, SchemaTags.NoControl)) return;
378
- const existingControls = schemaControlMap[schemaNode.id];
419
+ let skipChildren = false;
420
+ const existingControls = schemaToForm[schemaNode.id];
379
421
  if (!existingControls) {
380
422
  const eligibleParents = getEligibleParents(schemaNode);
381
423
  const desiredGroup = getTagParam(
382
424
  schemaNode.field,
383
425
  SchemaTags.ControlGroup,
384
426
  );
385
- let parentGroup = desiredGroup ? controlMap[desiredGroup] : undefined;
427
+ let parentGroup = desiredGroup
428
+ ? tree.getByRefId(desiredGroup)
429
+ : undefined;
386
430
  if (!parentGroup && desiredGroup)
387
- console.warn("No group '" + desiredGroup + "' for " + schemaNode.id);
388
- if (parentGroup && eligibleParents.indexOf(parentGroup.schema!.id) < 0) {
389
- console.warn(
431
+ warning?.("No group '" + desiredGroup + "' for " + schemaNode.id);
432
+ if (
433
+ parentGroup &&
434
+ eligibleParents.indexOf(formToSchema[parentGroup.id]!.id) < 0
435
+ ) {
436
+ warning?.(
390
437
  `Target group '${desiredGroup}' is not an eligible parent for '${schemaNode.id}'`,
391
438
  );
392
439
  parentGroup = undefined;
393
440
  }
394
441
  if (!parentGroup && eligibleParents.length) {
395
- parentGroup = schemaControlMap[eligibleParents[0]]?.[0];
442
+ parentGroup = schemaToForm[eligibleParents[0]]?.[0];
396
443
  }
397
444
  if (parentGroup) {
398
445
  const newControl = defaultControlForField(schemaNode.field, true);
399
- newControl.field = relativePath(parentGroup.schema!, schemaNode);
400
- parentGroup.children.push(
401
- toControlAndSchema(newControl, parentGroup.schema!, parentGroup),
402
- );
403
- } else
404
- console.warn("Could not find a parent group for: " + schemaNode.id);
446
+ skipChildren = !!newControl.childRefId;
447
+ const parentSchemaNode = formToSchema[parentGroup.id];
448
+ newControl.field = relativePath(parentSchemaNode, schemaNode);
449
+ const newNode = tree.addChild(parentGroup, newControl);
450
+ register(newNode, parentSchemaNode);
451
+ } else warning?.("Could not find a parent group for: " + schemaNode.id);
452
+ } else {
453
+ skipChildren = existingControls.some((x) => x.definition.childRefId);
405
454
  }
406
- schemaNode.getChildNodes(true).forEach(addMissing);
455
+ if (!skipChildren) schemaNode.getChildNodes(true).forEach(addMissing);
407
456
  }
408
457
 
409
458
  function getEligibleParents(schemaNode: SchemaNode) {
@@ -424,52 +473,6 @@ export function addMissingControlsForSchema(
424
473
  }
425
474
  }
426
475
  }
427
-
428
- function addReferences(c: ControlAndSchema) {
429
- c.children.forEach(addReferences);
430
- if (c.control.childRefId) {
431
- const ref = controlMap[c.control.childRefId];
432
- if (ref) {
433
- ref.children.forEach((x) =>
434
- toControlAndSchema(x.control, c.schema!, c, true),
435
- );
436
- return;
437
- }
438
- console.warn("Missing reference", c.control.childRefId);
439
- }
440
- }
441
-
442
- function addSchemaMapEntry(schemaId: string, entry: ControlAndSchema) {
443
- if (!schemaControlMap[schemaId]) schemaControlMap[schemaId] = [];
444
- schemaControlMap[schemaId].push(entry);
445
- }
446
- function toControlAndSchema(
447
- c: ControlDefinition,
448
- parentSchema: SchemaNode,
449
- parentNode?: ControlAndSchema,
450
- dontRegister?: boolean,
451
- ): ControlAndSchema {
452
- const controlPath = fieldPathForDefinition(c);
453
- let dataSchema = controlPath
454
- ? schemaForFieldPath(controlPath, parentSchema)
455
- : undefined;
456
- if (isGroupControl(c) && dataSchema == null) dataSchema = parentSchema;
457
- const entry: ControlAndSchema = {
458
- schema: dataSchema,
459
- control: c,
460
- children: [],
461
- parent: parentNode,
462
- };
463
- entry.children =
464
- c.children?.map((x) =>
465
- toControlAndSchema(x, dataSchema ?? parentSchema, entry, dontRegister),
466
- ) ?? [];
467
- if (!dontRegister && c.id) controlMap[c.id] = entry;
468
- if (dataSchema) {
469
- addSchemaMapEntry(dataSchema.id, entry);
470
- }
471
- return entry;
472
- }
473
476
  }
474
477
 
475
478
  /**
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "react",
17
+ "outDir": "lib"
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
20
+ "exclude": ["node_modules", "lib"]
21
+ }