@react-typed-forms/schemas 1.0.0-dev.16 → 1.0.0-dev.18

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,855 @@
1
+ import React, {
2
+ CSSProperties,
3
+ Fragment,
4
+ ReactElement,
5
+ ReactNode,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
9
+ import {
10
+ ActionRendererProps,
11
+ AdornmentProps,
12
+ AdornmentRenderer,
13
+ ArrayRendererProps,
14
+ controlTitle,
15
+ DataRendererProps,
16
+ DisplayRendererProps,
17
+ FormRenderer,
18
+ GroupRendererProps,
19
+ LabelRendererProps,
20
+ Visibility,
21
+ } from "./controlRender";
22
+ import clsx from "clsx";
23
+ import {
24
+ AdornmentPlacement,
25
+ ControlDefinition,
26
+ DataRenderType,
27
+ DisplayDataType,
28
+ FieldOption,
29
+ FieldType,
30
+ GridRenderer,
31
+ HtmlDisplay,
32
+ isGridRenderer,
33
+ TextDisplay,
34
+ } from "./types";
35
+ import { Control, Fcheckbox, formControlProps } from "@react-typed-forms/core";
36
+ import { hasOptions } from "./util";
37
+
38
+ export interface DefaultRenderers {
39
+ data: DataRendererRegistration;
40
+ label: LabelRendererRegistration;
41
+ action: ActionRendererRegistration;
42
+ array: ArrayRendererRegistration;
43
+ group: GroupRendererRegistration;
44
+ display: DisplayRendererRegistration;
45
+ visibility: VisibilityRendererRegistration;
46
+ adornment: AdornmentRendererRegistration;
47
+ }
48
+ export interface DataRendererRegistration {
49
+ type: "data";
50
+ schemaType?: string | string[];
51
+ renderType?: string | string[];
52
+ options?: boolean;
53
+ collection?: boolean;
54
+ match?: (props: DataRendererProps) => boolean;
55
+ render: (
56
+ props: DataRendererProps,
57
+ defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
58
+ renderers: FormRenderer,
59
+ ) => ReactElement;
60
+ }
61
+
62
+ export interface LabelRendererRegistration {
63
+ type: "label";
64
+ render: (
65
+ labelProps: LabelRendererProps,
66
+ elem: ReactElement,
67
+ renderers: FormRenderer,
68
+ ) => ReactElement;
69
+ }
70
+
71
+ export interface ActionRendererRegistration {
72
+ type: "action";
73
+ render: (props: ActionRendererProps, renderers: FormRenderer) => ReactElement;
74
+ }
75
+
76
+ export interface ArrayRendererRegistration {
77
+ type: "array";
78
+ render: (props: ArrayRendererProps, renderers: FormRenderer) => ReactElement;
79
+ }
80
+
81
+ export interface GroupRendererRegistration {
82
+ type: "group";
83
+ render: (
84
+ props: GroupRendererProps,
85
+ defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
86
+ renderers: FormRenderer,
87
+ ) => ReactElement;
88
+ }
89
+
90
+ export interface DisplayRendererRegistration {
91
+ type: "display";
92
+ render: (
93
+ props: DisplayRendererProps,
94
+ renderers: FormRenderer,
95
+ ) => ReactElement;
96
+ }
97
+
98
+ export interface VisibilityRendererRegistration {
99
+ type: "visibility";
100
+ render: (visible: Visibility, elem: ReactElement) => ReactElement;
101
+ }
102
+
103
+ export interface AdornmentRendererRegistration {
104
+ type: "adornment";
105
+ adornmentType?: string | string[];
106
+ render: (props: AdornmentProps) => AdornmentRenderer;
107
+ }
108
+
109
+ export type AnyRendererRegistration =
110
+ | DataRendererRegistration
111
+ | GroupRendererRegistration
112
+ | DisplayRendererRegistration
113
+ | ActionRendererRegistration
114
+ | LabelRendererRegistration
115
+ | ArrayRendererRegistration
116
+ | AdornmentRendererRegistration
117
+ | VisibilityRendererRegistration;
118
+
119
+ export function createFormRenderer(
120
+ customRenderers: AnyRendererRegistration[] = [],
121
+ defaultRenderers: DefaultRenderers = createClassStyledRenderers(),
122
+ ): FormRenderer {
123
+ const dataRegistrations = customRenderers.filter(isDataRegistration);
124
+ const adornmentRegistrations = customRenderers.filter(
125
+ isAdornmentRegistration,
126
+ );
127
+ const labelRenderer =
128
+ customRenderers.find(isLabelRegistration) ?? defaultRenderers.label;
129
+ const renderVisibility = (
130
+ customRenderers.find(isVisibilityRegistration) ??
131
+ defaultRenderers.visibility
132
+ ).render;
133
+
134
+ const formRenderers = {
135
+ renderAction,
136
+ renderData,
137
+ renderGroup,
138
+ renderDisplay,
139
+ renderLabel,
140
+ renderArray,
141
+ renderVisibility,
142
+ renderAdornment,
143
+ };
144
+
145
+ function renderAdornment(props: AdornmentProps): AdornmentRenderer {
146
+ const renderer =
147
+ adornmentRegistrations.find((x) =>
148
+ isOneOf(x.adornmentType, props.definition.type),
149
+ ) ?? defaultRenderers.adornment;
150
+ return renderer.render(props);
151
+ }
152
+
153
+ function renderArray(props: ArrayRendererProps) {
154
+ return defaultRenderers.array.render(props, formRenderers);
155
+ }
156
+
157
+ function renderLabel(props: LabelRendererProps, elem: ReactElement) {
158
+ return labelRenderer.render(props, elem, formRenderers);
159
+ }
160
+
161
+ function withAdornments(
162
+ definition: ControlDefinition,
163
+ adornments?: AdornmentRenderer[],
164
+ ): [
165
+ AdornmentRenderer[],
166
+ (placement: AdornmentPlacement) => ReactElement,
167
+ (elem: ReactElement) => ReactElement,
168
+ ] {
169
+ const rAdornments = adornments
170
+ ? adornments
171
+ : definition.adornments?.map((x, i) =>
172
+ renderAdornment({ definition: x, key: i }),
173
+ ) ?? [];
174
+ function combineAdornments(placement: AdornmentPlacement) {
175
+ return (
176
+ <>
177
+ {rAdornments
178
+ .filter((x) => x.child && x.child[0] === placement)
179
+ .map((x) => x.child![1])}
180
+ </>
181
+ );
182
+ }
183
+ return [
184
+ rAdornments,
185
+ combineAdornments,
186
+ (mainElem) =>
187
+ !adornments
188
+ ? mainElem
189
+ : rAdornments.reduce((e, n) => n.wrap?.(e) ?? e, mainElem),
190
+ ];
191
+ }
192
+
193
+ function renderData(
194
+ props: DataRendererProps,
195
+ adornments?: AdornmentRenderer[],
196
+ ): ReactElement {
197
+ const {
198
+ definition,
199
+ renderOptions: { type: renderType },
200
+ visible,
201
+ required,
202
+ control,
203
+ field,
204
+ } = props;
205
+
206
+ const options = hasOptions(props);
207
+ const renderer =
208
+ dataRegistrations.find(
209
+ (x) =>
210
+ (x.collection ?? false) === (field.collection ?? false) &&
211
+ (x.options ?? false) === options &&
212
+ isOneOf(x.schemaType, field.type) &&
213
+ isOneOf(x.renderType, renderType) &&
214
+ (!x.match || x.match(props)),
215
+ ) ?? defaultRenderers.data;
216
+
217
+ const [rAdornments, renderAdornment, wrapElem] = withAdornments(
218
+ definition,
219
+ adornments,
220
+ );
221
+ return wrapElem(
222
+ renderer.render(props, createLabel, {
223
+ ...formRenderers,
224
+ renderData: (p) => renderData(p, rAdornments),
225
+ }),
226
+ );
227
+
228
+ function createLabel(labelProps?: Partial<LabelRendererProps>) {
229
+ return {
230
+ visible,
231
+ required,
232
+ control,
233
+ forId: "c" + control.uniqueId,
234
+ renderAdornment,
235
+ ...labelProps,
236
+ title: labelProps?.title ?? controlTitle(definition.title, field),
237
+ };
238
+ }
239
+ }
240
+
241
+ function renderGroup(
242
+ props: GroupRendererProps,
243
+ adornments?: AdornmentRenderer[],
244
+ ): ReactElement {
245
+ const { definition, visible, field } = props;
246
+
247
+ const [rAdornments, renderAdornment, wrapElem] = withAdornments(
248
+ props.definition,
249
+ adornments,
250
+ );
251
+
252
+ const title = props.hideTitle
253
+ ? undefined
254
+ : field
255
+ ? controlTitle(definition.title, field)
256
+ : definition.title;
257
+
258
+ return wrapElem(
259
+ defaultRenderers.group.render(props, createLabel, {
260
+ ...formRenderers,
261
+ renderGroup: (p) => renderGroup(p, rAdornments),
262
+ }),
263
+ );
264
+
265
+ function createLabel(
266
+ labelProps?: Partial<LabelRendererProps>,
267
+ ): LabelRendererProps {
268
+ return {
269
+ required: false,
270
+ visible,
271
+ group: true,
272
+ renderAdornment,
273
+ title,
274
+ ...labelProps,
275
+ };
276
+ }
277
+ }
278
+
279
+ function renderAction(
280
+ props: ActionRendererProps,
281
+ adornments?: AdornmentRenderer[],
282
+ ) {
283
+ const renderer =
284
+ customRenderers.find(isActionRegistration) ?? defaultRenderers.action;
285
+ const [rAdornments, renderAdornment, wrapElem] = withAdornments(
286
+ props.definition,
287
+ adornments,
288
+ );
289
+ return wrapElem(renderer.render(props, formRenderers));
290
+ }
291
+
292
+ function renderDisplay(
293
+ props: DisplayRendererProps,
294
+ adornments?: AdornmentRenderer[],
295
+ ) {
296
+ const [rAdornments, renderAdornment, wrapElem] = withAdornments(
297
+ props.definition,
298
+ adornments,
299
+ );
300
+ return wrapElem(defaultRenderers.display.render(props, formRenderers));
301
+ }
302
+
303
+ return formRenderers;
304
+ }
305
+
306
+ interface DefaultLabelRendererOptions {
307
+ className?: string;
308
+ groupLabelClass?: string;
309
+ requiredElement?: ReactNode;
310
+ labelClass?: string;
311
+ }
312
+
313
+ interface DefaultActionRendererOptions {
314
+ className?: string;
315
+ }
316
+
317
+ export function createDefaultActionRenderer(
318
+ options: DefaultActionRendererOptions = {},
319
+ ): ActionRendererRegistration {
320
+ function render(
321
+ { visible, onClick, definition: { title } }: ActionRendererProps,
322
+ { renderVisibility }: Pick<FormRenderer, "renderVisibility">,
323
+ ) {
324
+ return renderVisibility(
325
+ visible,
326
+ <button className={options.className} onClick={onClick}>
327
+ {title}
328
+ </button>,
329
+ );
330
+ }
331
+ return { render, type: "action" };
332
+ }
333
+ export function createDefaultLabelRenderer(
334
+ options: DefaultLabelRendererOptions = { requiredElement: <span> *</span> },
335
+ ): LabelRendererRegistration {
336
+ return {
337
+ render: (p, elem, { renderVisibility }) =>
338
+ renderVisibility(
339
+ p.visible,
340
+ <DefaultLabelRenderer {...p} {...options} children={elem} />,
341
+ ),
342
+ type: "label",
343
+ };
344
+ }
345
+
346
+ export function DefaultLabelRenderer({
347
+ className,
348
+ labelClass,
349
+ title,
350
+ forId,
351
+ required,
352
+ children,
353
+ group,
354
+ groupLabelClass,
355
+ renderAdornment,
356
+ requiredElement,
357
+ }: LabelRendererProps &
358
+ DefaultLabelRendererOptions & { children: ReactElement }) {
359
+ return title ? (
360
+ <div className={className}>
361
+ {renderAdornment(AdornmentPlacement.LabelStart)}
362
+ <label
363
+ htmlFor={forId}
364
+ className={clsx(labelClass, group && groupLabelClass)}
365
+ >
366
+ {title}
367
+ {required && requiredElement}
368
+ </label>
369
+ {renderAdornment(AdornmentPlacement.LabelEnd)}
370
+ {renderAdornment(AdornmentPlacement.ControlStart)}
371
+ {children}
372
+ {renderAdornment(AdornmentPlacement.ControlEnd)}
373
+ </div>
374
+ ) : (
375
+ <>{children}</>
376
+ );
377
+ }
378
+
379
+ interface DefaultArrayRendererOptions {
380
+ className?: string;
381
+ removableClass?: string;
382
+ childClass?: string;
383
+ removableChildClass?: string;
384
+ removeActionClass?: string;
385
+ addActionClass?: string;
386
+ }
387
+
388
+ export function createDefaultArrayRenderer(
389
+ options?: DefaultArrayRendererOptions,
390
+ ): ArrayRendererRegistration {
391
+ const {
392
+ className,
393
+ removableClass,
394
+ childClass,
395
+ removableChildClass,
396
+ removeActionClass,
397
+ addActionClass,
398
+ } = options ?? {};
399
+ function render(
400
+ {
401
+ childCount,
402
+ renderChild,
403
+ addAction,
404
+ removeAction,
405
+ childKey,
406
+ }: ArrayRendererProps,
407
+ { renderAction }: Pick<FormRenderer, "renderAction">,
408
+ ) {
409
+ return (
410
+ <>
411
+ <div className={clsx(className, removeAction && removableClass)}>
412
+ {Array.from({ length: childCount }, (_, x) =>
413
+ removeAction ? (
414
+ <Fragment key={childKey(x)}>
415
+ <div className={clsx(childClass, removableChildClass)}>
416
+ {renderChild(x)}
417
+ </div>
418
+ <div className={removeActionClass}>
419
+ {renderAction(removeAction(x))}
420
+ </div>
421
+ </Fragment>
422
+ ) : (
423
+ <div key={childKey(x)} className={childClass}>
424
+ {renderChild(x)}
425
+ </div>
426
+ ),
427
+ )}
428
+ </div>
429
+ {addAction && (
430
+ <div className={addActionClass}>{renderAction(addAction)}</div>
431
+ )}
432
+ </>
433
+ );
434
+ }
435
+ return { render, type: "array" };
436
+ }
437
+
438
+ interface StyleProps {
439
+ className?: string;
440
+ style?: CSSProperties;
441
+ }
442
+
443
+ interface DefaultGroupRendererOptions {
444
+ className?: string;
445
+ standardClassName?: string;
446
+ gridStyles?: (columns: GridRenderer) => StyleProps;
447
+ gridClassName?: string;
448
+ defaultGridColumns?: number;
449
+ }
450
+
451
+ export function createDefaultGroupRenderer(
452
+ options?: DefaultGroupRendererOptions,
453
+ ): GroupRendererRegistration {
454
+ const {
455
+ className,
456
+ gridStyles = defaultGridStyles,
457
+ defaultGridColumns = 2,
458
+ gridClassName,
459
+ standardClassName,
460
+ } = options ?? {};
461
+
462
+ function defaultGridStyles({
463
+ columns = defaultGridColumns,
464
+ }: GridRenderer): StyleProps {
465
+ return {
466
+ className: gridClassName,
467
+ style: {
468
+ display: "grid",
469
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
470
+ },
471
+ };
472
+ }
473
+
474
+ function render(
475
+ props: GroupRendererProps,
476
+ defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
477
+ {
478
+ renderLabel,
479
+ renderArray,
480
+ }: Pick<FormRenderer, "renderLabel" | "renderArray" | "renderGroup">,
481
+ ) {
482
+ const { childCount, renderChild, definition } = props;
483
+
484
+ return renderLabel(
485
+ defaultLabel(),
486
+ props.array ? renderArray(props.array) : renderChildren(),
487
+ );
488
+
489
+ function renderChildren() {
490
+ const { groupOptions } = definition;
491
+ const { style, className: gcn } = isGridRenderer(groupOptions)
492
+ ? gridStyles(groupOptions)
493
+ : ({ className: standardClassName } satisfies StyleProps);
494
+ return (
495
+ <div className={clsx(className, gcn)} style={style}>
496
+ {Array.from({ length: childCount }, (_, x) => renderChild(x))}
497
+ </div>
498
+ );
499
+ }
500
+ }
501
+ return { type: "group", render };
502
+ }
503
+
504
+ export interface DefaultDisplayRendererOptions {
505
+ textClassName?: string;
506
+ htmlClassName?: string;
507
+ }
508
+ export function createDefaultDisplayRenderer(
509
+ options: DefaultDisplayRendererOptions = {},
510
+ ): DisplayRendererRegistration {
511
+ function doRender({ definition: { displayData } }: DisplayRendererProps) {
512
+ switch (displayData.type) {
513
+ case DisplayDataType.Text:
514
+ return (
515
+ <div className={options.textClassName}>
516
+ {(displayData as TextDisplay).text}
517
+ </div>
518
+ );
519
+ case DisplayDataType.Html:
520
+ return (
521
+ <div
522
+ className={options.htmlClassName}
523
+ dangerouslySetInnerHTML={{
524
+ __html: (displayData as HtmlDisplay).html,
525
+ }}
526
+ />
527
+ );
528
+ default:
529
+ return <h1>Unknown display type: {displayData.type}</h1>;
530
+ }
531
+ }
532
+ return {
533
+ render: (p, { renderVisibility }) =>
534
+ renderVisibility(p.visible, doRender(p)),
535
+ type: "display",
536
+ };
537
+ }
538
+
539
+ export const DefaultBoolOptions: FieldOption[] = [
540
+ { name: "Yes", value: true },
541
+ { name: "No", value: false },
542
+ ];
543
+ interface DefaultDataRendererOptions {
544
+ inputClass?: string;
545
+ selectOptions?: SelectRendererOptions;
546
+ booleanOptions?: FieldOption[];
547
+ optionRenderer?: DataRendererRegistration;
548
+ }
549
+
550
+ export function createDefaultDataRenderer(
551
+ options: DefaultDataRendererOptions = {},
552
+ ): DataRendererRegistration {
553
+ const selectRenderer = createSelectRenderer(options.selectOptions ?? {});
554
+ const { inputClass, booleanOptions, optionRenderer } = {
555
+ optionRenderer: selectRenderer,
556
+ booleanOptions: DefaultBoolOptions,
557
+ ...options,
558
+ };
559
+ return createDataRenderer((props, defaultLabel, renderers) => {
560
+ if (props.array) {
561
+ return renderers.renderArray(props.array);
562
+ }
563
+ let renderType = props.renderOptions.type;
564
+ const fieldType = props.field.type;
565
+ const isBool = fieldType === FieldType.Bool;
566
+ if (booleanOptions != null && isBool && props.options == null) {
567
+ return renderers.renderData({ ...props, options: booleanOptions });
568
+ }
569
+ if (renderType === DataRenderType.Standard && hasOptions(props)) {
570
+ return optionRenderer.render(props, defaultLabel, renderers);
571
+ }
572
+ switch (renderType) {
573
+ case DataRenderType.Dropdown:
574
+ return selectRenderer.render(props, defaultLabel, renderers);
575
+ }
576
+ const l = defaultLabel();
577
+ return renderers.renderLabel(
578
+ l,
579
+ renderType === DataRenderType.Checkbox ? (
580
+ <Fcheckbox control={props.control} />
581
+ ) : (
582
+ <ControlInput
583
+ className={inputClass}
584
+ id={l.forId}
585
+ readOnly={props.readonly}
586
+ control={props.control}
587
+ convert={createInputConversion(props.field.type)}
588
+ />
589
+ ),
590
+ );
591
+ });
592
+ }
593
+
594
+ export function ControlInput({
595
+ control,
596
+ convert,
597
+ ...props
598
+ }: React.InputHTMLAttributes<HTMLInputElement> & {
599
+ control: Control<any>;
600
+ convert: InputConversion;
601
+ }) {
602
+ const { errorText, value, onChange, ...inputProps } =
603
+ formControlProps(control);
604
+ return (
605
+ <input
606
+ {...inputProps}
607
+ type={convert[0]}
608
+ value={value == null ? "" : convert[2](value)}
609
+ onChange={(e) => {
610
+ control.value = convert[1](e.target.value);
611
+ }}
612
+ {...props}
613
+ />
614
+ );
615
+ }
616
+
617
+ export interface DefaultVisibilityRendererOptions {}
618
+
619
+ export interface DefaultAdornmentRendererOptions {}
620
+
621
+ export function createDefaultAdornmentRenderer(
622
+ options: DefaultAdornmentRendererOptions = {},
623
+ ): AdornmentRendererRegistration {
624
+ return { type: "adornment", render: () => ({}) };
625
+ }
626
+ export function createDefaultVisibilityRenderer(
627
+ options: DefaultVisibilityRendererOptions = {},
628
+ ): VisibilityRendererRegistration {
629
+ return {
630
+ type: "visibility",
631
+ render: (visible, children) => (visible.value ? children : <></>),
632
+ };
633
+ }
634
+
635
+ export interface DefaultRendererOptions {
636
+ data?: DefaultDataRendererOptions;
637
+ display?: DefaultDisplayRendererOptions;
638
+ action?: DefaultActionRendererOptions;
639
+ array?: DefaultArrayRendererOptions;
640
+ group?: DefaultGroupRendererOptions;
641
+ label?: DefaultLabelRendererOptions;
642
+ visibility?: DefaultVisibilityRendererOptions;
643
+ adornment?: DefaultAdornmentRendererOptions;
644
+ }
645
+
646
+ export function createDefaultRenderers(
647
+ options: DefaultRendererOptions = {},
648
+ ): DefaultRenderers {
649
+ return {
650
+ data: createDefaultDataRenderer(options.data),
651
+ display: createDefaultDisplayRenderer(options.display),
652
+ action: createDefaultActionRenderer(options.action),
653
+ array: createDefaultArrayRenderer(options.array),
654
+ group: createDefaultGroupRenderer(options.group),
655
+ label: createDefaultLabelRenderer(options.label),
656
+ visibility: createDefaultVisibilityRenderer(options.visibility),
657
+ adornment: createDefaultAdornmentRenderer(options.adornment),
658
+ };
659
+ }
660
+
661
+ function createClassStyledRenderers() {
662
+ return createDefaultRenderers({
663
+ label: { className: "control" },
664
+ group: { className: "group" },
665
+ array: { className: "control-array" },
666
+ action: { className: "action" },
667
+ data: { inputClass: "data" },
668
+ display: { htmlClassName: "html", textClassName: "text" },
669
+ });
670
+ }
671
+
672
+ function isAdornmentRegistration(
673
+ x: AnyRendererRegistration,
674
+ ): x is AdornmentRendererRegistration {
675
+ return x.type === "adornment";
676
+ }
677
+
678
+ function isDataRegistration(
679
+ x: AnyRendererRegistration,
680
+ ): x is DataRendererRegistration {
681
+ return x.type === "data";
682
+ }
683
+
684
+ function isLabelRegistration(
685
+ x: AnyRendererRegistration,
686
+ ): x is LabelRendererRegistration {
687
+ return x.type === "label";
688
+ }
689
+
690
+ function isActionRegistration(
691
+ x: AnyRendererRegistration,
692
+ ): x is ActionRendererRegistration {
693
+ return x.type === "action";
694
+ }
695
+
696
+ function isVisibilityRegistration(
697
+ x: AnyRendererRegistration,
698
+ ): x is VisibilityRendererRegistration {
699
+ return x.type === "visibility";
700
+ }
701
+
702
+ function isOneOf(x: string | string[] | undefined, v: string) {
703
+ return x == null ? true : Array.isArray(x) ? x.includes(v) : v === x;
704
+ }
705
+
706
+ export function createDataRenderer(
707
+ render: DataRendererRegistration["render"],
708
+ options?: Partial<DataRendererRegistration>,
709
+ ): DataRendererRegistration {
710
+ return { type: "data", render, ...options };
711
+ }
712
+
713
+ export function createDataRendererLabelled(
714
+ render: (
715
+ props: DataRendererProps,
716
+ id: string,
717
+ renderers: FormRenderer,
718
+ ) => ReactElement,
719
+ options?: Partial<DataRendererRegistration>,
720
+ ): DataRendererRegistration {
721
+ return {
722
+ type: "data",
723
+ render: (props, defaultLabel, renderers) => {
724
+ const dl = defaultLabel();
725
+ return renderers.renderLabel(dl, render(props, dl.forId!, renderers));
726
+ },
727
+ ...options,
728
+ };
729
+ }
730
+
731
+ export function createLabelRenderer(
732
+ options: Omit<LabelRendererRegistration, "type">,
733
+ ): LabelRendererRegistration {
734
+ return { type: "label", ...options };
735
+ }
736
+
737
+ export function createAdornmentRenderer(
738
+ render: (props: AdornmentProps) => AdornmentRenderer,
739
+ options?: Omit<AdornmentRendererRegistration, "type">,
740
+ ): AdornmentRendererRegistration {
741
+ return { type: "adornment", ...options, render };
742
+ }
743
+
744
+ export interface SelectRendererOptions {
745
+ className?: string;
746
+ emptyText?: string;
747
+ requiredText?: string;
748
+ }
749
+
750
+ export function createSelectRenderer(options: SelectRendererOptions = {}) {
751
+ return createDataRendererLabelled(
752
+ (props, id) => (
753
+ <SelectDataRenderer
754
+ className={options.className}
755
+ state={props.control}
756
+ id={id}
757
+ options={props.options!}
758
+ required={props.required}
759
+ emptyText={options.emptyText}
760
+ requiredText={options.requiredText}
761
+ convert={createSelectConversion(props.field.type)}
762
+ />
763
+ ),
764
+ {
765
+ options: true,
766
+ },
767
+ );
768
+ }
769
+
770
+ type SelectConversion = (a: any) => string | number;
771
+
772
+ interface SelectDataRendererProps {
773
+ id?: string;
774
+ className?: string;
775
+ options: {
776
+ name: string;
777
+ value: any;
778
+ disabled?: boolean;
779
+ }[];
780
+ emptyText?: string;
781
+ requiredText?: string;
782
+ required: boolean;
783
+ state: Control<any>;
784
+ convert: SelectConversion;
785
+ }
786
+
787
+ export function SelectDataRenderer({
788
+ state,
789
+ options,
790
+ className,
791
+ convert,
792
+ required,
793
+ emptyText = "N/A",
794
+ requiredText = "<please select>",
795
+ ...props
796
+ }: SelectDataRendererProps) {
797
+ const { value, disabled } = state;
798
+ const [showEmpty] = useState(!required || value == null);
799
+ const optionStringMap = useMemo(
800
+ () => Object.fromEntries(options.map((x) => [convert(x.value), x.value])),
801
+ [options],
802
+ );
803
+ return (
804
+ <select
805
+ {...props}
806
+ className={className}
807
+ onChange={(v) => (state.value = optionStringMap[v.target.value])}
808
+ value={convert(value)}
809
+ disabled={disabled}
810
+ >
811
+ {showEmpty && (
812
+ <option value="">{required ? requiredText : emptyText}</option>
813
+ )}
814
+ {options.map((x, i) => (
815
+ <option key={i} value={convert(x.value)} disabled={x.disabled}>
816
+ {x.name}
817
+ </option>
818
+ ))}
819
+ </select>
820
+ );
821
+ }
822
+
823
+ export function createSelectConversion(ft: string): SelectConversion {
824
+ switch (ft) {
825
+ case FieldType.String:
826
+ case FieldType.Int:
827
+ case FieldType.Double:
828
+ return (a) => a;
829
+ default:
830
+ return (a) => a?.toString() ?? "";
831
+ }
832
+ }
833
+
834
+ type InputConversion = [string, (s: any) => any, (a: any) => string | number];
835
+
836
+ export function createInputConversion(ft: string): InputConversion {
837
+ switch (ft) {
838
+ case FieldType.String:
839
+ return ["text", (a) => a, (a) => a];
840
+ case FieldType.Bool:
841
+ return ["text", (a) => a === "true", (a) => a?.toString() ?? ""];
842
+ case FieldType.Int:
843
+ return [
844
+ "number",
845
+ (a) => (a !== "" ? parseInt(a) : null),
846
+ (a) => (a == null ? "" : a),
847
+ ];
848
+ case FieldType.Date:
849
+ return ["date", (a) => a, (a) => a];
850
+ case FieldType.Double:
851
+ return ["number", (a) => parseFloat(a), (a) => a];
852
+ default:
853
+ return ["text", (a) => a, (a) => a];
854
+ }
855
+ }