@react-typed-forms/schemas 15.2.0 → 16.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.
@@ -1,6 +1,5 @@
1
- import React, { ReactNode } from "react";
1
+ import { ReactNode } from "react";
2
2
  import {
3
- ActionRendererProps,
4
3
  AdornmentProps,
5
4
  AdornmentRenderer,
6
5
  ArrayRendererProps,
@@ -27,7 +26,8 @@ import {
27
26
  RendererRegistration,
28
27
  VisibilityRendererRegistration,
29
28
  } from "./renderers";
30
- import { DataRenderType } from "./controlDefinition";
29
+ import { DataRenderType } from "@astroapps/forms-core";
30
+ import { ActionRendererProps } from "./types";
31
31
 
32
32
  export function createFormRenderer(
33
33
  customRenderers: RendererRegistration[] = [],
@@ -59,7 +59,6 @@ export function createFormRenderer(
59
59
  renderLayout,
60
60
  renderVisibility,
61
61
  renderLabelText,
62
- renderText: defaultRenderers.renderText,
63
62
  html: defaultRenderers.html,
64
63
  };
65
64
 
@@ -125,7 +124,7 @@ export function createFormRenderer(
125
124
  if (noMatch === true) return false;
126
125
  const matchCollection =
127
126
  (x.collection ?? false) ===
128
- (props.elementIndex == null && (field.collection ?? false));
127
+ (props.dataNode.elementIndex == null && (field.collection ?? false));
129
128
  const isSchemaAllowed =
130
129
  !!x.schemaType && renderType == DataRenderType.Standard
131
130
  ? isOneOf(x.schemaType, field.type)
package/src/index.ts CHANGED
@@ -1,14 +1,8 @@
1
- export * from "./controlDefinition";
2
- export * from "./schemaBuilder";
1
+ export * from "@astroapps/forms-core";
3
2
  export * from "./controlBuilder";
4
3
  export * from "./controlRender";
5
4
  export * from "./util";
6
5
  export * from "./renderers";
7
- export * from "./validators";
8
- export * from "./hooks";
9
- export * from "./defaultSchemaInterface";
10
6
  export * from "./createFormRenderer";
11
- export * from "./dynamicHooks";
12
- export * from "./schemaValidator";
13
- export * from "./schemaField";
14
- export * from "./entityExpression";
7
+ export * from "./RenderForm";
8
+ export * from "./types";
package/src/renderers.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ReactElement, ReactNode } from "react";
2
2
  import {
3
- ActionRendererProps,
4
3
  AdornmentProps,
5
4
  AdornmentRenderer,
6
5
  ArrayRendererProps,
@@ -23,7 +22,8 @@ import {
23
22
  OptionalAdornment,
24
23
  RenderOptions,
25
24
  SetFieldAdornment,
26
- } from "./controlDefinition";
25
+ } from "@astroapps/forms-core";
26
+ import { ActionRendererProps } from "./types";
27
27
 
28
28
  export interface DefaultRenderers {
29
29
  data: DataRendererRegistration;
@@ -36,7 +36,6 @@ export interface DefaultRenderers {
36
36
  renderLayout: LayoutRendererRegistration;
37
37
  visibility: VisibilityRendererRegistration;
38
38
  extraRenderers: RendererRegistration[];
39
- renderText: (props: ReactNode) => ReactNode;
40
39
  html: HtmlComponents;
41
40
  }
42
41
 
package/src/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ import {
2
+ ActionStyle,
3
+ EntityExpression,
4
+ FormContextData,
5
+ IconPlacement,
6
+ IconReference,
7
+ SchemaDataNode,
8
+ SchemaInterface,
9
+ } from "@astroapps/forms-core";
10
+ import React, { Key, ReactNode } from "react";
11
+ import { CleanupScope } from "@react-typed-forms/core";
12
+
13
+ /**
14
+ * Interface representing the control data context.
15
+ */
16
+ export interface ControlDataContext {
17
+ schemaInterface: SchemaInterface;
18
+ dataNode: SchemaDataNode | undefined;
19
+ parentNode: SchemaDataNode;
20
+ variables: Record<string, any>;
21
+ }
22
+
23
+ export type ControlActionHandler = (
24
+ actionId: string,
25
+ actionData: any,
26
+ ctx: ControlDataContext,
27
+ ) => (() => void) | undefined;
28
+
29
+ export interface ActionRendererProps {
30
+ key?: Key;
31
+ actionId: string;
32
+ actionContent?: ReactNode;
33
+ actionText: string;
34
+ actionData?: any;
35
+ actionStyle?: ActionStyle | null;
36
+ icon?: IconReference | null;
37
+ iconPlacement?: IconPlacement | null;
38
+ onClick: () => void;
39
+ className?: string | null;
40
+ textClass?: string | null;
41
+ style?: React.CSSProperties;
42
+ disabled?: boolean;
43
+ hidden?: boolean;
44
+ inline?: boolean;
45
+ }
46
+
47
+ export type RunExpression = (
48
+ scope: CleanupScope,
49
+ expression: EntityExpression,
50
+ returnResult: (v: unknown) => void,
51
+ bindings?: FormContextData,
52
+ ) => void;
package/src/util.ts CHANGED
@@ -1,49 +1,54 @@
1
1
  import {
2
- ControlActionHandler,
2
+ CompoundField,
3
3
  ControlDataVisitor,
4
4
  ControlDefinition,
5
5
  ControlDefinitionType,
6
- createFormTree,
6
+ createSchemaTree,
7
7
  DataControlDefinition,
8
8
  DataRenderType,
9
9
  DisplayOnlyRenderOptions,
10
+ EntityExpression,
11
+ FieldOption,
10
12
  fieldPathForDefinition,
11
- FormNode,
12
- FormTree,
13
+ findField,
14
+ FormContextData,
15
+ getGroupRendererOptions,
16
+ getTagParam,
13
17
  GroupRenderOptions,
14
18
  isAutoCompleteClasses,
15
19
  isCheckEntryClasses,
20
+ isCompoundField,
21
+ isCompoundNode,
16
22
  isDataControl,
17
23
  isDataGroupRenderer,
18
24
  isDisplayOnlyRenderer,
25
+ isGridRenderer,
19
26
  isGroupControl,
20
- } from "./controlDefinition";
21
- import { MutableRefObject, useRef } from "react";
22
- import clsx from "clsx";
23
- import {
24
- CompoundField,
25
- FieldOption,
26
- findField,
27
- getTagParam,
28
- isCompoundField,
29
- isCompoundNode,
30
27
  isScalarField,
31
28
  relativePath,
32
- rootSchemaNode,
33
29
  SchemaDataNode,
34
30
  SchemaField,
35
31
  schemaForFieldPath,
36
32
  SchemaNode,
37
33
  SchemaTags,
38
- } from "./schemaField";
34
+ } from "@astroapps/forms-core";
35
+ import { MutableRefObject, useRef } from "react";
36
+ import clsx from "clsx";
39
37
  import {
40
38
  Control,
41
39
  ControlChange,
40
+ createScopedEffect,
42
41
  ensureMetaValue,
43
42
  getElementIndex,
44
43
  newControl,
44
+ useControl,
45
45
  } from "@react-typed-forms/core";
46
- import { ActionRendererProps } from "./controlRender";
46
+ import {
47
+ ActionRendererProps,
48
+ ControlActionHandler,
49
+ RunExpression,
50
+ } from "./types";
51
+ import { Expression } from "jsonata";
47
52
 
48
53
  /**
49
54
  * Interface representing the classes for a control.
@@ -52,13 +57,10 @@ export interface ControlClasses {
52
57
  styleClass?: string;
53
58
  layoutClass?: string;
54
59
  labelClass?: string;
60
+ textClass?: string;
61
+ labelTextClass?: string;
55
62
  }
56
63
 
57
- /**
58
- * Type representing a JSON path, which can be a string or a number.
59
- */
60
- export type JsonPath = string | number;
61
-
62
64
  /**
63
65
  * Applies default values to the given record based on the provided schema fields.
64
66
  * @param v - The record to apply default values to.
@@ -146,7 +148,7 @@ export function defaultValueForField(
146
148
  const isRequired = !!(required || sf.required);
147
149
  if (isCompoundField(sf)) {
148
150
  if (isRequired) {
149
- const childValue = defaultValueForFields(sf.children);
151
+ const childValue = defaultValueForFields(sf.children ?? []);
150
152
  return sf.collection ? [childValue] : childValue;
151
153
  }
152
154
  return sf.notNullable ? (sf.collection ? [] : {}) : undefined;
@@ -164,7 +166,7 @@ export function defaultValueForField(
164
166
  */
165
167
  export function elementValueForField(sf: SchemaField): any {
166
168
  if (isCompoundField(sf)) {
167
- return defaultValueForFields(sf.children);
169
+ return defaultValueForFields(sf.children ?? []);
168
170
  }
169
171
  return sf.defaultValue;
170
172
  }
@@ -344,31 +346,18 @@ export function addMissingControls(
344
346
  controls: ControlDefinition[],
345
347
  warning?: (msg: string) => void,
346
348
  ) {
347
- return addMissingControlsForSchema(rootSchemaNode(fields), controls, warning);
349
+ return addMissingControlsForSchema(
350
+ createSchemaTree(fields).rootNode,
351
+ controls,
352
+ warning,
353
+ );
348
354
  }
349
355
 
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 };
356
+ interface ControlAndSchema {
357
+ control: ControlDefinition;
358
+ children: ControlAndSchema[];
359
+ schema?: SchemaNode;
360
+ parent?: ControlAndSchema;
372
361
  }
373
362
 
374
363
  /**
@@ -382,77 +371,56 @@ export function addMissingControlsForSchema(
382
371
  schema: SchemaNode,
383
372
  controls: ControlDefinition[],
384
373
  warning?: (msg: string) => void,
385
- ) {
386
- const tree = createFormTree(controls);
387
- addMissingControlsToForm(schema, tree, warning);
388
- return toDefinition(tree.rootNode).children ?? [];
389
-
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
+ ): ControlDefinition[] {
375
+ const controlMap: { [k: string]: ControlAndSchema } = {};
376
+ const schemaControlMap: { [k: string]: ControlAndSchema[] } = {};
377
+ const rootControls = controls.map((c) => toControlAndSchema(c, schema));
378
+ const rootSchema = { schema, children: rootControls } as ControlAndSchema;
379
+ addSchemaMapEntry("", rootSchema);
380
+ rootControls.forEach(addReferences);
381
+ const fields = schema.getChildNodes();
382
+ fields.forEach(addMissing);
383
+ return rootControls.map(toDefinition);
384
+
385
+ function toDefinition(c: ControlAndSchema): ControlDefinition {
386
+ const children = c.children.length ? c.children.map(toDefinition) : null;
387
+ return { ...c.control, children };
395
388
  }
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;
416
389
 
417
390
  function addMissing(schemaNode: SchemaNode) {
418
391
  if (fieldHasTag(schemaNode.field, SchemaTags.NoControl)) return;
419
392
  let skipChildren = false;
420
- const existingControls = schemaToForm[schemaNode.id];
393
+ const existingControls = schemaControlMap[schemaNode.id];
421
394
  if (!existingControls) {
422
395
  const eligibleParents = getEligibleParents(schemaNode);
423
396
  const desiredGroup = getTagParam(
424
397
  schemaNode.field,
425
398
  SchemaTags.ControlGroup,
426
399
  );
427
- let parentGroup = desiredGroup
428
- ? tree.getByRefId(desiredGroup)
429
- : undefined;
400
+ let parentGroup = desiredGroup ? controlMap[desiredGroup] : undefined;
430
401
  if (!parentGroup && desiredGroup)
431
402
  warning?.("No group '" + desiredGroup + "' for " + schemaNode.id);
432
- if (
433
- parentGroup &&
434
- eligibleParents.indexOf(formToSchema[parentGroup.id]!.id) < 0
435
- ) {
403
+ if (parentGroup && eligibleParents.indexOf(parentGroup.schema!.id) < 0) {
436
404
  warning?.(
437
405
  `Target group '${desiredGroup}' is not an eligible parent for '${schemaNode.id}'`,
438
406
  );
439
407
  parentGroup = undefined;
440
408
  }
441
409
  if (!parentGroup && eligibleParents.length) {
442
- parentGroup = schemaToForm[eligibleParents[0]]?.[0];
410
+ parentGroup = schemaControlMap[eligibleParents[0]]?.[0];
443
411
  }
444
412
  if (parentGroup) {
445
413
  const newControl = defaultControlForField(schemaNode.field, true);
446
414
  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);
415
+ newControl.field = relativePath(parentGroup.schema!, schemaNode);
416
+ parentGroup.children.push(
417
+ toControlAndSchema(newControl, parentGroup.schema!, parentGroup),
418
+ );
451
419
  } else warning?.("Could not find a parent group for: " + schemaNode.id);
452
420
  } else {
453
- skipChildren = existingControls.some((x) => x.definition.childRefId);
421
+ skipChildren = existingControls.some((x) => x.control.childRefId);
454
422
  }
455
- if (!skipChildren) schemaNode.getChildNodes(true).forEach(addMissing);
423
+ if (!skipChildren) schemaNode.getChildNodes().forEach(addMissing);
456
424
  }
457
425
 
458
426
  function getEligibleParents(schemaNode: SchemaNode) {
@@ -461,7 +429,7 @@ export function addMissingControlsToForm(
461
429
  while (parent) {
462
430
  eligibleParents.push(parent.id);
463
431
  if (parent.field.collection) break;
464
- if (!parent.parent) parent.getChildNodes(true).forEach(addCompound);
432
+ if (!parent.parent) parent.getChildNodes().forEach(addCompound);
465
433
  parent = parent.parent;
466
434
  }
467
435
  return eligibleParents;
@@ -469,10 +437,56 @@ export function addMissingControlsToForm(
469
437
  function addCompound(node: SchemaNode) {
470
438
  if (isCompoundNode(node) && !node.field.collection) {
471
439
  eligibleParents.push(node.id);
472
- node.getChildNodes(true).forEach(addCompound);
440
+ node.getChildNodes().forEach(addCompound);
441
+ }
442
+ }
443
+ }
444
+
445
+ function addReferences(c: ControlAndSchema) {
446
+ c.children.forEach(addReferences);
447
+ if (c.control.childRefId) {
448
+ const ref = controlMap[c.control.childRefId];
449
+ if (ref) {
450
+ ref.children.forEach((x) =>
451
+ toControlAndSchema(x.control, c.schema!, c, true),
452
+ );
453
+ return;
473
454
  }
455
+ console.warn("Missing reference", c.control.childRefId);
474
456
  }
475
457
  }
458
+
459
+ function addSchemaMapEntry(schemaId: string, entry: ControlAndSchema) {
460
+ if (!schemaControlMap[schemaId]) schemaControlMap[schemaId] = [];
461
+ schemaControlMap[schemaId].push(entry);
462
+ }
463
+ function toControlAndSchema(
464
+ c: ControlDefinition,
465
+ parentSchema: SchemaNode,
466
+ parentNode?: ControlAndSchema,
467
+ dontRegister?: boolean,
468
+ ): ControlAndSchema {
469
+ const controlPath = fieldPathForDefinition(c);
470
+ let dataSchema = controlPath
471
+ ? schemaForFieldPath(controlPath, parentSchema)
472
+ : undefined;
473
+ if (isGroupControl(c) && dataSchema == null) dataSchema = parentSchema;
474
+ const entry: ControlAndSchema = {
475
+ schema: dataSchema,
476
+ control: c,
477
+ children: [],
478
+ parent: parentNode,
479
+ };
480
+ entry.children =
481
+ c.children?.map((x) =>
482
+ toControlAndSchema(x, dataSchema ?? parentSchema, entry, dontRegister),
483
+ ) ?? [];
484
+ if (!dontRegister && c.id) controlMap[c.id] = entry;
485
+ if (dataSchema) {
486
+ addSchemaMapEntry(dataSchema.id, entry);
487
+ }
488
+ return entry;
489
+ }
476
490
  }
477
491
 
478
492
  /**
@@ -486,54 +500,22 @@ export function useUpdatedRef<A>(a: A): MutableRefObject<A> {
486
500
  return r;
487
501
  }
488
502
 
489
- /**
490
- * Checks if a control definition is readonly.
491
- * @param c - The control definition to check.
492
- * @returns True if the control definition is readonly, false otherwise.
493
- */
494
- export function isControlReadonly(c: ControlDefinition): boolean {
495
- return isDataControl(c) && !!c.readonly;
496
- }
497
-
498
- /**
499
- * Checks if a control definition is disabled.
500
- * @param c - The control definition to check.
501
- * @returns True if the control definition is disabled, false otherwise.
502
- */
503
- export function isControlDisabled(c: ControlDefinition): boolean {
504
- return isDataControl(c) && !!c.disabled;
505
- }
506
-
507
- /**
508
- * Returns the display-only render options for a control definition.
509
- * @param d - The control definition to get the display-only render options for.
510
- * @returns The display-only render options, or undefined if not applicable.
511
- */
512
- export function getDisplayOnlyOptions(
513
- d: ControlDefinition,
514
- ): DisplayOnlyRenderOptions | undefined {
515
- return isDataControl(d) &&
516
- d.renderOptions &&
517
- isDisplayOnlyRenderer(d.renderOptions)
518
- ? d.renderOptions
519
- : undefined;
520
- }
521
-
522
503
  /**
523
504
  * Cleans data for a schema based on the provided schema fields.
524
505
  * @param v - The data to clean.
525
- * @param fields - The schema fields to use for cleaning the data.
506
+ * @param schemaNode
526
507
  * @param removeIfDefault - Flag indicating if default values should be removed.
527
508
  * @returns The cleaned data.
528
509
  */
529
510
  export function cleanDataForSchema(
530
511
  v: { [k: string]: any } | undefined,
531
- fields: SchemaField[],
512
+ schemaNode: SchemaNode,
532
513
  removeIfDefault?: boolean,
533
514
  ): any {
534
515
  if (!v) return v;
535
- const typeField = fields.find((x) => x.isTypeField);
536
- const typeValue = typeField ? v[typeField.field] : undefined;
516
+ const fields = schemaNode.getResolvedFields();
517
+ const typeField = fields.find((x) => x.isTypeField)?.field;
518
+ const typeValue = typeField ? v[typeField] : undefined;
537
519
  const cleanableFields = !removeIfDefault
538
520
  ? fields.filter(
539
521
  (x) => isCompoundField(x) || (x.onlyForTypes?.length ?? 0) > 0,
@@ -551,17 +533,17 @@ export function cleanDataForSchema(
551
533
  return;
552
534
  }
553
535
  if (isCompoundField(x)) {
554
- const childFields = x.treeChildren ? fields : x.children;
536
+ const childNode = schemaNode.createChildNode(x);
555
537
  if (x.collection) {
556
538
  if (Array.isArray(childValue)) {
557
539
  out[x.field] = childValue.map((cv) =>
558
- cleanDataForSchema(cv, childFields, removeIfDefault),
540
+ cleanDataForSchema(cv, childNode, removeIfDefault),
559
541
  );
560
542
  }
561
543
  } else {
562
544
  out[x.field] = cleanDataForSchema(
563
545
  childValue,
564
- childFields,
546
+ childNode,
565
547
  removeIfDefault,
566
548
  );
567
549
  }
@@ -596,6 +578,9 @@ export function getAllReferencedClasses(
596
578
  isDataControl(c) && isCheckEntryClasses(c.renderOptions)
597
579
  ? c.renderOptions
598
580
  : {};
581
+ const groupOptions = isGroupControl(c) ? c.groupOptions : undefined;
582
+ const gridClasses =
583
+ groupOptions && isGridRenderer(groupOptions) ? [groupOptions.rowClass] : [];
599
584
 
600
585
  const {
601
586
  listContainerClass,
@@ -612,6 +597,9 @@ export function getAllReferencedClasses(
612
597
  c.styleClass,
613
598
  c.layoutClass,
614
599
  c.labelClass,
600
+ c.textClass,
601
+ c.labelTextClass,
602
+ ...gridClasses,
615
603
  ...Object.values(go),
616
604
  ...(collectExtra?.(c) ?? []),
617
605
  entryWrapperClass,
@@ -629,28 +617,6 @@ export function getAllReferencedClasses(
629
617
  return [tc];
630
618
  }
631
619
 
632
- /**
633
- * Converts a JSON path array to a string.
634
- * @param jsonPath - The JSON path array to convert.
635
- * @param customIndex - Optional function to customize the index format.
636
- * @returns The JSON path string.
637
- */
638
- export function jsonPathString(
639
- jsonPath: JsonPath[],
640
- customIndex?: (n: number) => string,
641
- ) {
642
- let out = "";
643
- jsonPath.forEach((v, i) => {
644
- if (typeof v === "number") {
645
- out += customIndex?.(v) ?? "[" + v + "]";
646
- } else {
647
- if (i > 0) out += ".";
648
- out += v;
649
- }
650
- });
651
- return out;
652
- }
653
-
654
620
  /**
655
621
  * Finds a child control definition within a parent control definition.
656
622
  * @param parent - The parent control definition.
@@ -796,6 +762,7 @@ export function deepMerge<A>(value: A, fallback: A): A {
796
762
  deepMerge(v1, fv),
797
763
  ) as A;
798
764
  }
765
+
799
766
  /**
800
767
  * Coerces a value to a string.
801
768
  * @param {unknown} v - The value to coerce.
@@ -809,21 +776,6 @@ export function coerceToString(v: unknown) {
809
776
  : v.toString();
810
777
  }
811
778
 
812
- /**
813
- * Returns the group renderer options for a control definition.
814
- * @param {ControlDefinition} def - The control definition to get the group renderer options for.
815
- * @returns {GroupRenderOptions | undefined} - The group renderer options, or undefined if not applicable.
816
- */
817
- export function getGroupRendererOptions(
818
- def: ControlDefinition,
819
- ): GroupRenderOptions | undefined {
820
- return isGroupControl(def)
821
- ? def.groupOptions
822
- : isDataControl(def) && isDataGroupRenderer(def.renderOptions)
823
- ? def.renderOptions.groupOptions
824
- : undefined;
825
- }
826
-
827
779
  /**
828
780
  * Returns the group class overrides for a control definition.
829
781
  * @param {ControlDefinition} def - The control definition to get the group class overrides for.
@@ -841,15 +793,6 @@ export function getGroupClassOverrides(def: ControlDefinition): ControlClasses {
841
793
  return out;
842
794
  }
843
795
 
844
- /**
845
- * Checks if a control definition is display-only.
846
- * @param {ControlDefinition} def - The control definition to check.
847
- * @returns {boolean} - True if the control definition is display-only, false otherwise.
848
- */
849
- export function isControlDisplayOnly(def: ControlDefinition): boolean {
850
- return Boolean(getGroupRendererOptions(def)?.displayOnly);
851
- }
852
-
853
796
  /**
854
797
  * Combines multiple action handlers into a single handler.
855
798
  * @param {...(ControlActionHandler | undefined)[]} handlers - The action handlers to combine.
@@ -857,10 +800,12 @@ export function isControlDisplayOnly(def: ControlDefinition): boolean {
857
800
  */
858
801
  export function actionHandlers(
859
802
  ...handlers: (ControlActionHandler | undefined)[]
860
- ): ControlActionHandler {
803
+ ): ControlActionHandler | undefined {
804
+ const nonNullHandlers = handlers.filter((x) => x != null);
805
+ if (nonNullHandlers.length === 0) return undefined;
861
806
  return (actionId, actionData, ctx) => {
862
- for (let i = 0; i < handlers.length; i++) {
863
- const res = handlers[i]?.(actionId, actionData, ctx);
807
+ for (let i = 0; i < nonNullHandlers.length; i++) {
808
+ const res = nonNullHandlers[i](actionId, actionData, ctx);
864
809
  if (res) return res;
865
810
  }
866
811
  return undefined;
@@ -1051,3 +996,24 @@ export function validationVisitor(
1051
996
  return undefined;
1052
997
  };
1053
998
  }
999
+
1000
+ export function useExpression<T>(
1001
+ defaultValue: T,
1002
+ runExpression: RunExpression,
1003
+ expression: EntityExpression | null | undefined,
1004
+ coerce: (x: any) => T,
1005
+ bindings?: Record<string, any>,
1006
+ ): Control<T> {
1007
+ const value = useControl<T>(defaultValue);
1008
+ createScopedEffect((scope) => {
1009
+ if (expression?.type)
1010
+ runExpression(
1011
+ scope,
1012
+ expression,
1013
+ (x) => (value.value = coerce(x)),
1014
+ bindings,
1015
+ );
1016
+ else value.value = defaultValue;
1017
+ }, value);
1018
+ return value;
1019
+ }