@react-typed-forms/schemas 14.2.0 → 14.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.
@@ -0,0 +1,1252 @@
1
+ import React, {
2
+ FC,
3
+ Fragment,
4
+ Key,
5
+ ReactElement,
6
+ ReactNode,
7
+ useCallback,
8
+ useEffect,
9
+ } from "react";
10
+ import {
11
+ addElement,
12
+ Control,
13
+ newControl,
14
+ removeElement,
15
+ RenderArrayElements,
16
+ trackedValue,
17
+ useComponentTracking,
18
+ useComputed,
19
+ useControl,
20
+ useControlEffect,
21
+ } from "@react-typed-forms/core";
22
+ import {
23
+ AdornmentPlacement,
24
+ ArrayActionOptions,
25
+ ControlActionHandler,
26
+ ControlAdornment,
27
+ ControlAdornmentType,
28
+ ControlDataContext,
29
+ ControlDefinition,
30
+ CustomDisplay,
31
+ DataControlDefinition,
32
+ DataRenderType,
33
+ DisplayData,
34
+ DisplayDataType,
35
+ DynamicPropertyType,
36
+ FormContextData,
37
+ FormNode,
38
+ GroupRenderOptions,
39
+ isActionControl,
40
+ isDataControl,
41
+ isDisplayControl,
42
+ isGroupControl,
43
+ legacyFormNode,
44
+ lookupDataNode,
45
+ RenderOptions,
46
+ } from "./controlDefinition";
47
+ import {
48
+ applyLengthRestrictions,
49
+ ControlClasses,
50
+ elementValueForField,
51
+ ExternalEditAction,
52
+ fieldDisplayName,
53
+ getExternalEditData,
54
+ getGroupClassOverrides,
55
+ isControlDisplayOnly,
56
+ JsonPath,
57
+ rendererClass,
58
+ useUpdatedRef,
59
+ } from "./util";
60
+ import { createAction, dataControl } from "./controlBuilder";
61
+ import {
62
+ defaultUseEvalExpressionHook,
63
+ EvalExpressionHook,
64
+ useEvalActionHook,
65
+ useEvalAllowedOptionsHook,
66
+ useEvalDefaultValueHook,
67
+ useEvalDisabledHook,
68
+ useEvalDisplayHook,
69
+ UseEvalExpressionHook,
70
+ useEvalLabelText,
71
+ useEvalReadonlyHook,
72
+ useEvalStyleHook,
73
+ useEvalVisibilityHook,
74
+ } from "./hooks";
75
+ import { useMakeValidationHook, ValidationContext } from "./validators";
76
+ import { useDynamicHooks } from "./dynamicHooks";
77
+ import { defaultSchemaInterface } from "./defaultSchemaInterface";
78
+ import {
79
+ LengthValidator,
80
+ SchemaValidator,
81
+ ValidatorType,
82
+ } from "./schemaValidator";
83
+ import {
84
+ createSchemaLookup,
85
+ FieldOption,
86
+ makeSchemaDataNode,
87
+ SchemaDataNode,
88
+ SchemaField,
89
+ SchemaInterface,
90
+ } from "./schemaField";
91
+
92
+ /**
93
+ * Interface for rendering different types of form controls.
94
+ */
95
+ export interface FormRenderer {
96
+ /**
97
+ * Renders data control.
98
+ * @param props - Properties for data renderer.
99
+ * @returns A function that takes layout properties and returns layout properties.
100
+ */
101
+ renderData: (
102
+ props: DataRendererProps,
103
+ ) => (layout: ControlLayoutProps) => ControlLayoutProps;
104
+
105
+ /**
106
+ * Renders group control.
107
+ * @param props - Properties for group renderer.
108
+ * @returns A function that takes layout properties and returns layout properties.
109
+ */
110
+ renderGroup: (
111
+ props: GroupRendererProps,
112
+ ) => (layout: ControlLayoutProps) => ControlLayoutProps;
113
+
114
+ /**
115
+ * Renders display control.
116
+ * @param props - Properties for display renderer.
117
+ * @returns A React node.
118
+ */
119
+ renderDisplay: (props: DisplayRendererProps) => ReactNode;
120
+
121
+ /**
122
+ * Renders action control.
123
+ * @param props - Properties for action renderer.
124
+ * @returns A React node.
125
+ */
126
+ renderAction: (props: ActionRendererProps) => ReactNode;
127
+
128
+ /**
129
+ * Renders array control.
130
+ * @param props - Properties for array renderer.
131
+ * @returns A React node.
132
+ */
133
+ renderArray: (props: ArrayRendererProps) => ReactNode;
134
+
135
+ /**
136
+ * Renders adornment.
137
+ * @param props - Properties for adornment renderer.
138
+ * @returns An adornment renderer.
139
+ */
140
+ renderAdornment: (props: AdornmentProps) => AdornmentRenderer;
141
+
142
+ /**
143
+ * Renders label.
144
+ * @param props - Properties for label renderer.
145
+ * @param labelStart - React node to render at the start of the label.
146
+ * @param labelEnd - React node to render at the end of the label.
147
+ * @returns A React node.
148
+ */
149
+ renderLabel: (
150
+ props: LabelRendererProps,
151
+ labelStart: ReactNode,
152
+ labelEnd: ReactNode,
153
+ ) => ReactNode;
154
+
155
+ /**
156
+ * Renders layout.
157
+ * @param props - Properties for control layout.
158
+ * @returns A rendered control.
159
+ */
160
+ renderLayout: (props: ControlLayoutProps) => RenderedControl;
161
+
162
+ /**
163
+ * Renders visibility control.
164
+ * @param props - Properties for visibility renderer.
165
+ * @returns A React node.
166
+ */
167
+ renderVisibility: (props: VisibilityRendererProps) => ReactNode;
168
+
169
+ /**
170
+ * Renders label text.
171
+ * @param props - React node for label text.
172
+ * @returns A React node.
173
+ */
174
+ renderLabelText: (props: ReactNode) => ReactNode;
175
+ }
176
+
177
+ export interface AdornmentProps {
178
+ adornment: ControlAdornment;
179
+ dataContext: ControlDataContext;
180
+ useExpr?: UseEvalExpressionHook;
181
+ designMode?: boolean;
182
+ formOptions: FormContextOptions;
183
+ }
184
+
185
+ export const AppendAdornmentPriority = 0;
186
+ export const WrapAdornmentPriority = 1000;
187
+
188
+ export interface AdornmentRenderer {
189
+ apply(children: RenderedLayout): void;
190
+ adornment?: ControlAdornment;
191
+ priority: number;
192
+ }
193
+
194
+ export interface ArrayRendererProps {
195
+ addAction?: ActionRendererProps;
196
+ required: boolean;
197
+ removeAction?: (elemIndex: number) => ActionRendererProps;
198
+ editAction?: (elemIndex: number) => ActionRendererProps;
199
+ renderElement: (
200
+ elemIndex: number,
201
+ wrapEntry: (children: ReactNode) => ReactNode,
202
+ ) => ReactNode;
203
+ arrayControl: Control<any[] | undefined | null>;
204
+ className?: string;
205
+ style?: React.CSSProperties;
206
+ min?: number | null;
207
+ max?: number | null;
208
+ disabled?: boolean;
209
+ }
210
+ export interface Visibility {
211
+ visible: boolean;
212
+ showing: boolean;
213
+ }
214
+
215
+ export interface RenderedLayout {
216
+ labelStart?: ReactNode;
217
+ labelEnd?: ReactNode;
218
+ controlStart?: ReactNode;
219
+ controlEnd?: ReactNode;
220
+ label?: ReactNode;
221
+ children?: ReactNode;
222
+ errorControl?: Control<any>;
223
+ className?: string;
224
+ style?: React.CSSProperties;
225
+ wrapLayout: (layout: ReactElement) => ReactElement;
226
+ }
227
+
228
+ export interface RenderedControl {
229
+ children: ReactNode;
230
+ className?: string;
231
+ style?: React.CSSProperties;
232
+ divRef?: (cb: HTMLElement | null) => void;
233
+ }
234
+
235
+ export interface VisibilityRendererProps extends RenderedControl {
236
+ visibility: Control<Visibility | undefined>;
237
+ }
238
+
239
+ export interface ControlLayoutProps {
240
+ label?: LabelRendererProps;
241
+ errorControl?: Control<any>;
242
+ adornments?: AdornmentRenderer[];
243
+ children?: ReactNode;
244
+ processLayout?: (props: ControlLayoutProps) => ControlLayoutProps;
245
+ className?: string | null;
246
+ style?: React.CSSProperties;
247
+ }
248
+
249
+ /**
250
+ * Enum representing the types of labels that can be rendered.
251
+ */
252
+ export enum LabelType {
253
+ /**
254
+ * Label for a control.
255
+ */
256
+ Control,
257
+
258
+ /**
259
+ * Label for a group.
260
+ */
261
+ Group,
262
+
263
+ /**
264
+ * Label for text.
265
+ */
266
+ Text,
267
+ }
268
+
269
+ /**
270
+ * Properties for label renderers.
271
+ */
272
+ export interface LabelRendererProps {
273
+ /**
274
+ * The type of the label.
275
+ */
276
+ type: LabelType;
277
+
278
+ /**
279
+ * Whether to hide the label.
280
+ */
281
+ hide?: boolean | null;
282
+
283
+ /**
284
+ * The content of the label.
285
+ */
286
+ label: ReactNode;
287
+
288
+ /**
289
+ * Whether to show the label as being required.
290
+ * E.g. show an asterisk next to the label.
291
+ */
292
+ required?: boolean | null;
293
+
294
+ /**
295
+ * The ID of the element the label is for.
296
+ */
297
+ forId?: string;
298
+
299
+ /**
300
+ * The CSS class name for the label.
301
+ */
302
+ className?: string;
303
+ }
304
+
305
+ /**
306
+ * Properties for display renderers.
307
+ */
308
+ export interface DisplayRendererProps {
309
+ /**
310
+ * The data to be displayed.
311
+ */
312
+ data: DisplayData;
313
+
314
+ /**
315
+ * A control with dynamic value for display.
316
+ */
317
+ display?: Control<string | undefined>;
318
+
319
+ /**
320
+ * The context for the control data.
321
+ */
322
+ dataContext: ControlDataContext;
323
+
324
+ /**
325
+ * The CSS class name for the display renderer.
326
+ */
327
+ className?: string;
328
+
329
+ /**
330
+ * The CSS styles for the display renderer.
331
+ */
332
+ style?: React.CSSProperties;
333
+ }
334
+
335
+ export type ChildVisibilityFunc = (
336
+ child: ControlDefinition,
337
+ parentNode?: SchemaDataNode,
338
+ dontOverride?: boolean,
339
+ ) => EvalExpressionHook<boolean>;
340
+ export interface ParentRendererProps {
341
+ formNode: FormNode;
342
+ renderChild: ChildRenderer;
343
+ className?: string;
344
+ style?: React.CSSProperties;
345
+ dataContext: ControlDataContext;
346
+ useChildVisibility: ChildVisibilityFunc;
347
+ useEvalExpression: UseEvalExpressionHook;
348
+ designMode?: boolean;
349
+ }
350
+
351
+ export interface GroupRendererProps extends ParentRendererProps {
352
+ definition: ControlDefinition;
353
+ renderOptions: GroupRenderOptions;
354
+ }
355
+
356
+ export interface DataRendererProps extends ParentRendererProps {
357
+ renderOptions: RenderOptions;
358
+ definition: DataControlDefinition;
359
+ field: SchemaField;
360
+ elementIndex?: number;
361
+ id: string;
362
+ control: Control<any>;
363
+ readonly: boolean;
364
+ required: boolean;
365
+ options: FieldOption[] | undefined | null;
366
+ hidden: boolean;
367
+ dataNode: SchemaDataNode;
368
+ displayOnly: boolean;
369
+ }
370
+
371
+ export interface ActionRendererProps {
372
+ actionId: string;
373
+ actionText: string;
374
+ actionData?: any;
375
+ onClick: () => void;
376
+ className?: string | null;
377
+ style?: React.CSSProperties;
378
+ disabled?: boolean;
379
+ }
380
+
381
+ export interface ControlRenderProps {
382
+ control: Control<any>;
383
+ parentPath?: JsonPath[];
384
+ }
385
+
386
+ export interface FormContextOptions {
387
+ readonly?: boolean | null;
388
+ hidden?: boolean | null;
389
+ disabled?: boolean | null;
390
+ displayOnly?: boolean;
391
+ }
392
+
393
+ export interface DataControlProps {
394
+ formNode: FormNode;
395
+ definition: DataControlDefinition;
396
+ dataContext: ControlDataContext;
397
+ control: Control<any>;
398
+ formOptions: FormContextOptions;
399
+ style?: React.CSSProperties | undefined;
400
+ renderChild: ChildRenderer;
401
+ elementIndex?: number;
402
+ allowedOptions?: Control<any[] | undefined>;
403
+ useChildVisibility: ChildVisibilityFunc;
404
+ useEvalExpression: UseEvalExpressionHook;
405
+ schemaInterface?: SchemaInterface;
406
+ designMode?: boolean;
407
+ styleClass?: string;
408
+ layoutClass?: string;
409
+ }
410
+
411
+ export type CreateDataProps = (
412
+ controlProps: DataControlProps,
413
+ ) => DataRendererProps;
414
+
415
+ export interface ControlRenderOptions
416
+ extends FormContextOptions,
417
+ ControlClasses {
418
+ useDataHook?: (c: ControlDefinition) => CreateDataProps;
419
+ actionOnClick?: ControlActionHandler;
420
+ customDisplay?: (
421
+ customId: string,
422
+ displayProps: DisplayRendererProps,
423
+ ) => ReactNode;
424
+ useValidationHook?: (
425
+ validator: SchemaValidator,
426
+ ctx: ValidationContext,
427
+ ) => void;
428
+ useEvalExpressionHook?: UseEvalExpressionHook;
429
+ adjustLayout?: (
430
+ context: ControlDataContext,
431
+ layout: ControlLayoutProps,
432
+ ) => ControlLayoutProps;
433
+ clearHidden?: boolean;
434
+ schemaInterface?: SchemaInterface;
435
+ elementIndex?: number;
436
+ formData?: FormContextData;
437
+ }
438
+
439
+ export function useControlRenderer(
440
+ definition: ControlDefinition,
441
+ fields: SchemaField[],
442
+ renderer: FormRenderer,
443
+ options: ControlRenderOptions = {},
444
+ ): FC<ControlRenderProps> {
445
+ const r = useUpdatedRef({ definition, fields, renderer, options });
446
+ return useCallback(
447
+ ({ control, parentPath }) => {
448
+ return (
449
+ <ControlRenderer
450
+ {...r.current}
451
+ control={control}
452
+ parentPath={parentPath}
453
+ />
454
+ );
455
+ },
456
+ [r],
457
+ );
458
+ }
459
+ export function useControlRendererComponent(
460
+ controlOrFormNode: ControlDefinition | FormNode,
461
+ renderer: FormRenderer,
462
+ options: ControlRenderOptions = {},
463
+ parentDataNode: SchemaDataNode,
464
+ ): FC<{}> {
465
+ const [definition, formNode] =
466
+ controlOrFormNode instanceof FormNode
467
+ ? [controlOrFormNode.definition, controlOrFormNode]
468
+ : [controlOrFormNode, legacyFormNode(controlOrFormNode)];
469
+ const dataProps = options.useDataHook?.(definition) ?? defaultDataProps;
470
+ const elementIndex = options.elementIndex;
471
+ const schemaInterface = options.schemaInterface ?? defaultSchemaInterface;
472
+ const useExpr = options.useEvalExpressionHook ?? defaultUseEvalExpressionHook;
473
+
474
+ let dataNode: SchemaDataNode | undefined;
475
+ if (elementIndex != null) {
476
+ dataNode = parentDataNode.getChildElement(elementIndex);
477
+ } else {
478
+ dataNode = lookupDataNode(definition, parentDataNode);
479
+ }
480
+ const useValidation = useMakeValidationHook(
481
+ definition,
482
+ options.useValidationHook,
483
+ );
484
+ const dynamicHooks = useDynamicHooks({
485
+ defaultValueControl: useEvalDefaultValueHook(useExpr, definition),
486
+ visibleControl: useEvalVisibilityHook(useExpr, definition),
487
+ readonlyControl: useEvalReadonlyHook(useExpr, definition),
488
+ disabledControl: useEvalDisabledHook(useExpr, definition),
489
+ allowedOptions: useEvalAllowedOptionsHook(useExpr, definition),
490
+ labelText: useEvalLabelText(useExpr, definition),
491
+ actionData: useEvalActionHook(useExpr, definition),
492
+ customStyle: useEvalStyleHook(
493
+ useExpr,
494
+ DynamicPropertyType.Style,
495
+ definition,
496
+ ),
497
+ layoutStyle: useEvalStyleHook(
498
+ useExpr,
499
+ DynamicPropertyType.LayoutStyle,
500
+ definition,
501
+ ),
502
+ displayControl: useEvalDisplayHook(useExpr, definition),
503
+ });
504
+
505
+ const r = useUpdatedRef({
506
+ options,
507
+ definition,
508
+ elementIndex,
509
+ parentDataNode,
510
+ dataNode,
511
+ formNode,
512
+ });
513
+
514
+ const Component = useCallback(() => {
515
+ const stopTracking = useComponentTracking();
516
+
517
+ try {
518
+ const {
519
+ definition: c,
520
+ options,
521
+ elementIndex,
522
+ parentDataNode: pdn,
523
+ dataNode: dn,
524
+ formNode,
525
+ } = r.current;
526
+ const formData = options.formData ?? {};
527
+ const dataContext: ControlDataContext = {
528
+ schemaInterface,
529
+ dataNode: dn,
530
+ parentNode: pdn,
531
+ formData,
532
+ };
533
+ const {
534
+ readonlyControl,
535
+ disabledControl,
536
+ visibleControl,
537
+ displayControl,
538
+ layoutStyle,
539
+ labelText,
540
+ customStyle,
541
+ allowedOptions,
542
+ defaultValueControl,
543
+ actionData,
544
+ } = dynamicHooks(dataContext);
545
+
546
+ const visible = visibleControl.current.value;
547
+ const visibility = useControl<Visibility | undefined>(() =>
548
+ visible != null
549
+ ? {
550
+ visible,
551
+ showing: visible,
552
+ }
553
+ : undefined,
554
+ );
555
+ useControlEffect(
556
+ () => visibleControl.value,
557
+ (visible) => {
558
+ if (visible != null)
559
+ visibility.setValue((ex) => ({
560
+ visible,
561
+ showing: ex ? ex.showing : visible,
562
+ }));
563
+ },
564
+ );
565
+
566
+ const parentControl = parentDataNode.control!;
567
+ const control = dataNode?.control;
568
+ useControlEffect(
569
+ () => [
570
+ visibility.value,
571
+ defaultValueControl.value,
572
+ control?.isNull,
573
+ isDataControl(definition) && definition.dontClearHidden,
574
+ definition.adornments?.some(
575
+ (x) => x.type === ControlAdornmentType.Optional,
576
+ ) ||
577
+ (isDataControl(definition) &&
578
+ definition.renderOptions?.type == DataRenderType.NullToggle),
579
+ parentControl.isNull,
580
+ options.hidden,
581
+ readonlyControl.value,
582
+ ],
583
+ ([vc, dv, _, dontClear, dontDefault, parentNull, hidden, ro]) => {
584
+ if (!ro) {
585
+ if (control) {
586
+ if (vc && vc.visible === vc.showing) {
587
+ if (hidden || !vc.visible) {
588
+ control.setValue((x) =>
589
+ options.clearHidden && !dontClear
590
+ ? undefined
591
+ : x == null && dontClear && !dontDefault
592
+ ? dv
593
+ : x,
594
+ );
595
+ } else if (!dontDefault)
596
+ control.setValue((x) => (x != null ? x : dv));
597
+ }
598
+ } else if (parentNull) {
599
+ parentControl.setValue((x) => x ?? {});
600
+ }
601
+ }
602
+ },
603
+ true,
604
+ );
605
+ const myOptionsControl = useComputed<FormContextOptions>(() => ({
606
+ hidden: options.hidden || !visibility.fields?.showing.value,
607
+ readonly: options.readonly || readonlyControl.value,
608
+ disabled: options.disabled || disabledControl.value,
609
+ displayOnly: options.displayOnly || isControlDisplayOnly(c),
610
+ }));
611
+ const myOptions = trackedValue(myOptionsControl);
612
+ useValidation({
613
+ control: control ?? newControl(null),
614
+ hiddenControl: myOptionsControl.fields.hidden,
615
+ dataContext,
616
+ });
617
+ const childOptions: ControlRenderOptions = {
618
+ ...options,
619
+ ...myOptions,
620
+ elementIndex: undefined,
621
+ formData,
622
+ };
623
+
624
+ useEffect(() => {
625
+ if (
626
+ control &&
627
+ typeof myOptions.disabled === "boolean" &&
628
+ control.disabled != myOptions.disabled
629
+ )
630
+ control.disabled = myOptions.disabled;
631
+ }, [control, myOptions.disabled]);
632
+ if (parentControl.isNull) return <></>;
633
+
634
+ const adornments =
635
+ definition.adornments?.map((x) =>
636
+ renderer.renderAdornment({
637
+ adornment: x,
638
+ dataContext,
639
+ useExpr,
640
+ formOptions: myOptions,
641
+ }),
642
+ ) ?? [];
643
+ const labelAndChildren = renderControlLayout({
644
+ formNode,
645
+ definition: c,
646
+ renderer,
647
+ renderChild: (k, child, options) => {
648
+ const overrideClasses = getGroupClassOverrides(c);
649
+ const { parentDataNode, ...renderOptions } = options ?? {};
650
+ const dContext =
651
+ parentDataNode ?? dataContext.dataNode ?? dataContext.parentNode;
652
+
653
+ return (
654
+ <NewControlRenderer
655
+ key={k}
656
+ definition={child}
657
+ renderer={renderer}
658
+ parentDataNode={dContext}
659
+ options={{
660
+ ...childOptions,
661
+ ...overrideClasses,
662
+ ...renderOptions,
663
+ }}
664
+ />
665
+ );
666
+ },
667
+ createDataProps: dataProps,
668
+ formOptions: myOptions,
669
+ dataContext,
670
+ control: displayControl ?? control,
671
+ elementIndex,
672
+ schemaInterface,
673
+ labelText,
674
+ displayControl,
675
+ style: customStyle.value,
676
+ allowedOptions,
677
+ customDisplay: options.customDisplay,
678
+ actionDataControl: actionData,
679
+ actionOnClick: options.actionOnClick,
680
+ styleClass: options.styleClass,
681
+ labelClass: options.labelClass,
682
+ useEvalExpression: useExpr,
683
+ useChildVisibility: (childDef, parentNode, dontOverride) => {
684
+ return useEvalVisibilityHook(
685
+ useExpr,
686
+ childDef,
687
+ !dontOverride
688
+ ? lookupDataNode(
689
+ childDef,
690
+ parentNode ?? dataNode ?? parentDataNode,
691
+ )
692
+ : undefined,
693
+ );
694
+ },
695
+ });
696
+ const layoutProps: ControlLayoutProps = {
697
+ ...labelAndChildren,
698
+ adornments,
699
+ className: rendererClass(options.layoutClass, c.layoutClass),
700
+ style: layoutStyle.value,
701
+ };
702
+ const renderedControl = renderer.renderLayout(
703
+ options.adjustLayout?.(dataContext, layoutProps) ?? layoutProps,
704
+ );
705
+ return renderer.renderVisibility({ visibility, ...renderedControl });
706
+ } finally {
707
+ stopTracking();
708
+ }
709
+ }, [r, dataProps, useValidation, renderer, schemaInterface, dynamicHooks]);
710
+ (Component as any).displayName = "RenderControl";
711
+ return Component;
712
+ }
713
+
714
+ export function ControlRenderer({
715
+ definition,
716
+ fields,
717
+ renderer,
718
+ options,
719
+ control,
720
+ parentPath,
721
+ }: {
722
+ definition: ControlDefinition;
723
+ fields: SchemaField[];
724
+ renderer: FormRenderer;
725
+ options?: ControlRenderOptions;
726
+ control: Control<any>;
727
+ parentPath?: JsonPath[];
728
+ }) {
729
+ const schemaDataNode = makeSchemaDataNode(
730
+ createSchemaLookup({ "": fields }).getSchema("")!,
731
+ control,
732
+ );
733
+ const Render = useControlRendererComponent(
734
+ definition,
735
+ renderer,
736
+ options,
737
+ schemaDataNode,
738
+ );
739
+ return <Render />;
740
+ }
741
+
742
+ export function NewControlRenderer({
743
+ definition,
744
+ renderer,
745
+ options,
746
+ parentDataNode,
747
+ }: {
748
+ definition: ControlDefinition | FormNode;
749
+ renderer: FormRenderer;
750
+ options?: ControlRenderOptions;
751
+ parentDataNode: SchemaDataNode;
752
+ }) {
753
+ const Render = useControlRendererComponent(
754
+ definition,
755
+ renderer,
756
+ options,
757
+ parentDataNode,
758
+ );
759
+ return <Render />;
760
+ }
761
+
762
+ export function defaultDataProps({
763
+ definition,
764
+ control,
765
+ formOptions,
766
+ style,
767
+ allowedOptions,
768
+ schemaInterface = defaultSchemaInterface,
769
+ styleClass,
770
+ ...props
771
+ }: DataControlProps): DataRendererProps {
772
+ const dataNode = props.dataContext.dataNode!;
773
+ const field = dataNode.schema.field;
774
+ const className = rendererClass(styleClass, definition.styleClass);
775
+ const displayOnly = !!formOptions.displayOnly;
776
+ const required = !!definition.required && !displayOnly;
777
+ const fieldOptions = schemaInterface.getDataOptions(dataNode);
778
+ const _allowed = allowedOptions?.value ?? [];
779
+ const allowed = Array.isArray(_allowed) ? _allowed : [_allowed];
780
+ return {
781
+ dataNode,
782
+ definition,
783
+ control,
784
+ field,
785
+ id: "c" + control.uniqueId,
786
+ options:
787
+ allowed.length > 0
788
+ ? allowed
789
+ .map((x) =>
790
+ typeof x === "object"
791
+ ? x
792
+ : (fieldOptions?.find((y) => y.value == x) ?? {
793
+ name: x.toString(),
794
+ value: x,
795
+ }),
796
+ )
797
+ .filter((x) => x != null)
798
+ : fieldOptions,
799
+ readonly: !!formOptions.readonly,
800
+ displayOnly,
801
+ renderOptions: definition.renderOptions ?? { type: "Standard" },
802
+ required,
803
+ hidden: !!formOptions.hidden,
804
+ className,
805
+ style,
806
+ ...props,
807
+ };
808
+ }
809
+
810
+ export interface ChildRendererOptions {
811
+ elementIndex?: number;
812
+ parentDataNode?: SchemaDataNode;
813
+ formData?: FormContextData;
814
+ displayOnly?: boolean;
815
+ styleClass?: string;
816
+ layoutClass?: string;
817
+ labelClass?: string;
818
+ }
819
+
820
+ export type ChildRenderer = (
821
+ k: Key,
822
+ child: FormNode,
823
+ options?: ChildRendererOptions,
824
+ ) => ReactNode;
825
+
826
+ export interface RenderControlProps {
827
+ definition: ControlDefinition;
828
+ formNode: FormNode;
829
+ renderer: FormRenderer;
830
+ renderChild: ChildRenderer;
831
+ createDataProps: CreateDataProps;
832
+ formOptions: FormContextOptions;
833
+ dataContext: ControlDataContext;
834
+ control?: Control<any>;
835
+ labelText?: Control<string | null | undefined>;
836
+ elementIndex?: number;
837
+ displayControl?: Control<string | undefined>;
838
+ style?: React.CSSProperties;
839
+ allowedOptions?: Control<any[] | undefined>;
840
+ actionDataControl?: Control<any | undefined | null>;
841
+ useChildVisibility: ChildVisibilityFunc;
842
+ useEvalExpression: UseEvalExpressionHook;
843
+ actionOnClick?: ControlActionHandler;
844
+ schemaInterface?: SchemaInterface;
845
+ designMode?: boolean;
846
+ customDisplay?: (
847
+ customId: string,
848
+ displayProps: DisplayRendererProps,
849
+ ) => ReactNode;
850
+ labelClass?: string;
851
+ styleClass?: string;
852
+ }
853
+ export function renderControlLayout(
854
+ props: RenderControlProps,
855
+ ): ControlLayoutProps {
856
+ const {
857
+ definition: c,
858
+ renderer,
859
+ renderChild,
860
+ control,
861
+ dataContext,
862
+ createDataProps: dataProps,
863
+ displayControl,
864
+ style,
865
+ labelText,
866
+ useChildVisibility,
867
+ designMode,
868
+ customDisplay,
869
+ useEvalExpression,
870
+ labelClass,
871
+ styleClass,
872
+ formNode,
873
+ } = props;
874
+
875
+ if (isDataControl(c)) {
876
+ return renderData(c);
877
+ }
878
+ if (isGroupControl(c)) {
879
+ if (c.compoundField) {
880
+ return renderData(
881
+ dataControl(c.compoundField, c.title, {
882
+ children: c.children,
883
+ hideTitle: c.groupOptions?.hideTitle,
884
+ }),
885
+ );
886
+ }
887
+
888
+ return {
889
+ processLayout: renderer.renderGroup({
890
+ formNode,
891
+ definition: c,
892
+ renderChild,
893
+ useEvalExpression,
894
+ dataContext,
895
+ renderOptions: c.groupOptions ?? { type: "Standard" },
896
+ className: rendererClass(styleClass, c.styleClass),
897
+ useChildVisibility,
898
+ style,
899
+ designMode,
900
+ }),
901
+ label: {
902
+ label: labelText?.value ?? c.title,
903
+ className: rendererClass(labelClass, c.labelClass),
904
+ type: LabelType.Group,
905
+ hide: c.groupOptions?.hideTitle,
906
+ },
907
+ };
908
+ }
909
+ if (isActionControl(c)) {
910
+ const actionData = props.actionDataControl?.value ?? c.actionData;
911
+ return {
912
+ children: renderer.renderAction({
913
+ actionText: labelText?.value ?? c.title ?? c.actionId,
914
+ actionId: c.actionId,
915
+ actionData,
916
+ onClick:
917
+ props.actionOnClick?.(c.actionId, actionData, dataContext) ??
918
+ (() => {}),
919
+ className: rendererClass(styleClass, c.styleClass),
920
+ style,
921
+ }),
922
+ };
923
+ }
924
+ if (isDisplayControl(c)) {
925
+ const data = c.displayData ?? {};
926
+ const displayProps = {
927
+ data,
928
+ className: rendererClass(styleClass, c.styleClass),
929
+ style,
930
+ display: displayControl,
931
+ dataContext,
932
+ };
933
+ if (data.type === DisplayDataType.Custom && customDisplay) {
934
+ return {
935
+ children: customDisplay((data as CustomDisplay).customId, displayProps),
936
+ };
937
+ }
938
+ return {
939
+ children: renderer.renderDisplay(displayProps),
940
+ };
941
+ }
942
+ return {};
943
+
944
+ function renderData(c: DataControlDefinition): ControlLayoutProps {
945
+ if (!control) return { children: "No control for: " + c.field };
946
+ const rendererProps = dataProps(
947
+ props as RenderControlProps & {
948
+ definition: DataControlDefinition;
949
+ control: Control<any>;
950
+ },
951
+ );
952
+
953
+ const label = !c.hideTitle
954
+ ? controlTitle(
955
+ labelText?.value ?? c.title,
956
+ props.dataContext.dataNode!.schema.field,
957
+ )
958
+ : undefined;
959
+ return {
960
+ processLayout: renderer.renderData(rendererProps),
961
+ label: {
962
+ type:
963
+ (c.children?.length ?? 0) > 0 ? LabelType.Group : LabelType.Control,
964
+ label,
965
+ forId: rendererProps.id,
966
+ required: c.required && !props.formOptions.displayOnly,
967
+ hide: c.hideTitle,
968
+ className: rendererClass(labelClass, c.labelClass),
969
+ },
970
+ errorControl: control,
971
+ };
972
+ }
973
+ }
974
+
975
+ type MarkupKeys = keyof Omit<
976
+ RenderedLayout,
977
+ | "errorControl"
978
+ | "style"
979
+ | "className"
980
+ | "wrapLayout"
981
+ | "readonly"
982
+ | "disabled"
983
+ >;
984
+ export function appendMarkup(
985
+ k: MarkupKeys,
986
+ markup: ReactNode,
987
+ ): (layout: RenderedLayout) => void {
988
+ return (layout) =>
989
+ (layout[k] = (
990
+ <>
991
+ {layout[k]}
992
+ {markup}
993
+ </>
994
+ ));
995
+ }
996
+
997
+ export function wrapMarkup(
998
+ k: MarkupKeys,
999
+ wrap: (ex: ReactNode) => ReactNode,
1000
+ ): (layout: RenderedLayout) => void {
1001
+ return (layout) => (layout[k] = wrap(layout[k]));
1002
+ }
1003
+
1004
+ export function layoutKeyForPlacement(pos: AdornmentPlacement): MarkupKeys {
1005
+ switch (pos) {
1006
+ case AdornmentPlacement.ControlEnd:
1007
+ return "controlEnd";
1008
+ case AdornmentPlacement.ControlStart:
1009
+ return "controlStart";
1010
+ case AdornmentPlacement.LabelStart:
1011
+ return "labelStart";
1012
+ case AdornmentPlacement.LabelEnd:
1013
+ return "labelEnd";
1014
+ }
1015
+ }
1016
+
1017
+ export function wrapLayout(
1018
+ wrap: (layout: ReactElement) => ReactElement,
1019
+ ): (renderedLayout: RenderedLayout) => void {
1020
+ return (rl) => {
1021
+ const orig = rl.wrapLayout;
1022
+ rl.wrapLayout = (x) => wrap(orig(x));
1023
+ };
1024
+ }
1025
+
1026
+ export function appendMarkupAt(
1027
+ pos: AdornmentPlacement,
1028
+ markup: ReactNode,
1029
+ ): (layout: RenderedLayout) => void {
1030
+ return appendMarkup(layoutKeyForPlacement(pos), markup);
1031
+ }
1032
+
1033
+ export function wrapMarkupAt(
1034
+ pos: AdornmentPlacement,
1035
+ wrap: (ex: ReactNode) => ReactNode,
1036
+ ): (layout: RenderedLayout) => void {
1037
+ return wrapMarkup(layoutKeyForPlacement(pos), wrap);
1038
+ }
1039
+
1040
+ export function renderLayoutParts(
1041
+ props: ControlLayoutProps,
1042
+ renderer: FormRenderer,
1043
+ ): RenderedLayout {
1044
+ const { className, children, style, errorControl, label, adornments } =
1045
+ props.processLayout?.(props) ?? props;
1046
+ const layout: RenderedLayout = {
1047
+ children,
1048
+ errorControl,
1049
+ style,
1050
+ className: className!,
1051
+ wrapLayout: (x) => x,
1052
+ };
1053
+ (adornments ?? [])
1054
+ .sort((a, b) => a.priority - b.priority)
1055
+ .forEach((x) => x.apply(layout));
1056
+ layout.label =
1057
+ label && !label.hide
1058
+ ? renderer.renderLabel(label, layout.labelStart, layout.labelEnd)
1059
+ : undefined;
1060
+ return layout;
1061
+ }
1062
+
1063
+ export function controlTitle(
1064
+ title: string | undefined | null,
1065
+ field: SchemaField,
1066
+ ) {
1067
+ return title ? title : fieldDisplayName(field);
1068
+ }
1069
+
1070
+ export function getLengthRestrictions(definition: DataControlDefinition) {
1071
+ const lengthVal = definition.validators?.find(
1072
+ (x) => x.type === ValidatorType.Length,
1073
+ ) as LengthValidator | undefined;
1074
+
1075
+ return { min: lengthVal?.min, max: lengthVal?.max };
1076
+ }
1077
+
1078
+ export function createArrayActions(
1079
+ control: Control<any[]>,
1080
+ field: SchemaField,
1081
+ options?: ArrayActionOptions,
1082
+ ): Pick<
1083
+ ArrayRendererProps,
1084
+ "addAction" | "removeAction" | "editAction" | "arrayControl"
1085
+ > {
1086
+ const noun = field.displayName ?? field.field;
1087
+ const {
1088
+ addText,
1089
+ noAdd,
1090
+ removeText,
1091
+ noRemove,
1092
+ removeActionId,
1093
+ addActionId,
1094
+ editActionId,
1095
+ editText,
1096
+ disabled,
1097
+ readonly,
1098
+ designMode,
1099
+ editExternal,
1100
+ } = options ?? {};
1101
+ return {
1102
+ arrayControl: control,
1103
+ addAction:
1104
+ !readonly && !noAdd
1105
+ ? makeAdd(() => {
1106
+ if (!designMode) {
1107
+ const newValue = elementValueForField(field);
1108
+
1109
+ if (editExternal) {
1110
+ const editData = getExternalEditData(control);
1111
+ editData.value = {
1112
+ data: [elementValueForField(field)],
1113
+ actions: [
1114
+ makeCancel(),
1115
+ {
1116
+ action: makeAdd(() => {
1117
+ const newValue = (
1118
+ editData.fields.data.value as any[]
1119
+ )[0];
1120
+ addElement(control, newValue);
1121
+ editData.value = undefined;
1122
+ }),
1123
+ },
1124
+ ],
1125
+ };
1126
+ } else {
1127
+ addElement(control, newValue);
1128
+ }
1129
+ }
1130
+ })
1131
+ : undefined,
1132
+ editAction: editExternal
1133
+ ? (i: number) => ({
1134
+ actionId: editActionId ? editActionId : "edit",
1135
+ actionText: editText ? editText : "Edit",
1136
+ onClick: () => {
1137
+ if (!designMode) {
1138
+ const editData = getExternalEditData(control);
1139
+ const elementToEdit = control.as<any[]>().elements[i];
1140
+ editData.value = {
1141
+ data: [elementToEdit.current.value],
1142
+ actions: [
1143
+ makeCancel(),
1144
+ {
1145
+ action: createAction(
1146
+ "apply",
1147
+ () => {
1148
+ elementToEdit.value = (
1149
+ editData.fields.data.value as any[]
1150
+ )[0];
1151
+ editData.value = undefined;
1152
+ },
1153
+ "Apply",
1154
+ ),
1155
+ },
1156
+ ],
1157
+ };
1158
+ }
1159
+ },
1160
+ })
1161
+ : undefined,
1162
+ removeAction:
1163
+ !readonly && !noRemove
1164
+ ? (i: number) => ({
1165
+ actionId: removeActionId ? removeActionId : "remove",
1166
+ actionText: removeText ? removeText : "Remove",
1167
+ onClick: () => {
1168
+ if (!designMode) {
1169
+ removeElement(control, i);
1170
+ }
1171
+ },
1172
+ disabled,
1173
+ })
1174
+ : undefined,
1175
+ };
1176
+
1177
+ function makeAdd(onClick: () => void): ActionRendererProps {
1178
+ return createAction(
1179
+ addActionId ? addActionId : "add",
1180
+ onClick,
1181
+ addText ? addText : "Add " + noun,
1182
+ { disabled },
1183
+ );
1184
+ }
1185
+
1186
+ function makeCancel(): ExternalEditAction {
1187
+ return {
1188
+ dontValidate: true,
1189
+ action: {
1190
+ actionId: "cancel",
1191
+ actionText: "Cancel",
1192
+ onClick: () => {
1193
+ getExternalEditData(control).value = undefined;
1194
+ },
1195
+ disabled,
1196
+ },
1197
+ };
1198
+ }
1199
+ }
1200
+
1201
+ export function applyArrayLengthRestrictions(
1202
+ {
1203
+ arrayControl,
1204
+ min,
1205
+ max,
1206
+ editAction,
1207
+ addAction: aa,
1208
+ removeAction: ra,
1209
+ required,
1210
+ }: Pick<
1211
+ ArrayRendererProps,
1212
+ | "addAction"
1213
+ | "removeAction"
1214
+ | "editAction"
1215
+ | "arrayControl"
1216
+ | "min"
1217
+ | "max"
1218
+ | "required"
1219
+ >,
1220
+ disable?: boolean,
1221
+ ): Pick<ArrayRendererProps, "addAction" | "removeAction" | "editAction"> & {
1222
+ addDisabled: boolean;
1223
+ removeDisabled: boolean;
1224
+ } {
1225
+ const [removeAllowed, addAllowed] = applyLengthRestrictions(
1226
+ arrayControl.elements?.length ?? 0,
1227
+ min == null && required ? 1 : min,
1228
+ max,
1229
+ true,
1230
+ true,
1231
+ );
1232
+ return {
1233
+ addAction: disable || addAllowed ? aa : undefined,
1234
+ removeAction: disable || removeAllowed ? ra : undefined,
1235
+ removeDisabled: !removeAllowed,
1236
+ addDisabled: !addAllowed,
1237
+ editAction,
1238
+ };
1239
+ }
1240
+
1241
+ export function fieldOptionAdornment(p: DataRendererProps) {
1242
+ return (o: FieldOption, i: number, selected: boolean) => (
1243
+ <RenderArrayElements array={p.formNode.getChildNodes()}>
1244
+ {(cd, i) =>
1245
+ p.renderChild(i, cd, {
1246
+ parentDataNode: p.dataContext.parentNode,
1247
+ formData: { option: o, optionSelected: selected },
1248
+ })
1249
+ }
1250
+ </RenderArrayElements>
1251
+ );
1252
+ }