@react-typed-forms/schemas 5.0.3 → 7.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/src/renderers.tsx CHANGED
@@ -3,7 +3,6 @@ import React, {
3
3
  Fragment,
4
4
  ReactElement,
5
5
  ReactNode,
6
- useCallback,
7
6
  useEffect,
8
7
  useMemo,
9
8
  useState,
@@ -14,6 +13,7 @@ import {
14
13
  ActionRendererProps,
15
14
  AdornmentProps,
16
15
  AdornmentRenderer,
16
+ appendMarkupAt,
17
17
  ArrayRendererProps,
18
18
  ControlLayoutProps,
19
19
  DataRendererProps,
@@ -22,18 +22,29 @@ import {
22
22
  GroupRendererProps,
23
23
  LabelRendererProps,
24
24
  LabelType,
25
+ RenderedControl,
25
26
  RenderedLayout,
26
27
  renderLayoutParts,
27
- Visibility,
28
+ VisibilityRendererProps,
28
29
  } from "./controlRender";
29
30
  import {
31
+ AdornmentPlacement,
32
+ ControlAdornment,
33
+ ControlAdornmentType,
30
34
  DataRenderType,
31
35
  DisplayDataType,
32
36
  FieldOption,
33
37
  FieldType,
38
+ FlexRenderer,
34
39
  GridRenderer,
35
40
  HtmlDisplay,
41
+ IconAdornment,
42
+ IconDisplay,
43
+ isDisplayOnlyRenderer,
44
+ isFlexRenderer,
36
45
  isGridRenderer,
46
+ SchemaField,
47
+ SchemaInterface,
37
48
  TextDisplay,
38
49
  } from "./types";
39
50
  import { hasOptions } from "./util";
@@ -53,7 +64,10 @@ export interface DefaultRenderers {
53
64
  export interface LayoutRendererRegistration {
54
65
  type: "layout";
55
66
  match?: (props: ControlLayoutProps) => boolean;
56
- render: (props: ControlLayoutProps, renderers: FormRenderer) => ReactNode;
67
+ render: (
68
+ props: ControlLayoutProps,
69
+ renderers: FormRenderer,
70
+ ) => RenderedControl;
57
71
  }
58
72
  export interface DataRendererRegistration {
59
73
  type: "data";
@@ -64,7 +78,6 @@ export interface DataRendererRegistration {
64
78
  match?: (props: DataRendererProps) => boolean;
65
79
  render: (
66
80
  props: DataRendererProps,
67
- asArray: (() => ReactNode) | undefined,
68
81
  renderers: FormRenderer,
69
82
  ) => ReactNode | ((layout: ControlLayoutProps) => ControlLayoutProps);
70
83
  }
@@ -94,7 +107,10 @@ export interface ArrayRendererRegistration {
94
107
  export interface GroupRendererRegistration {
95
108
  type: "group";
96
109
  renderType?: string | string[];
97
- render: (props: GroupRendererProps, renderers: FormRenderer) => ReactElement;
110
+ render: (
111
+ props: GroupRendererProps,
112
+ renderers: FormRenderer,
113
+ ) => ReactElement | ((layout: ControlLayoutProps) => ControlLayoutProps);
98
114
  }
99
115
 
100
116
  export interface DisplayRendererRegistration {
@@ -114,10 +130,7 @@ export interface AdornmentRendererRegistration {
114
130
 
115
131
  export interface VisibilityRendererRegistration {
116
132
  type: "visibility";
117
- render: (
118
- visibility: Control<Visibility | undefined>,
119
- children: () => ReactNode,
120
- ) => ReactNode;
133
+ render: (props: VisibilityRendererProps) => ReactNode;
121
134
  }
122
135
 
123
136
  export type RendererRegistration =
@@ -196,7 +209,6 @@ export function createFormRenderer(
196
209
 
197
210
  function renderData(
198
211
  props: DataRendererProps,
199
- asArray: (() => ReactNode) | undefined,
200
212
  ): (layout: ControlLayoutProps) => ControlLayoutProps {
201
213
  const {
202
214
  renderOptions: { type: renderType },
@@ -214,17 +226,21 @@ export function createFormRenderer(
214
226
  (!x.match || x.match(props)),
215
227
  ) ?? defaultRenderers.data;
216
228
 
217
- const result = renderer.render(props, asArray, formRenderers);
229
+ const result = renderer.render(props, formRenderers);
218
230
  if (typeof result === "function") return result;
219
231
  return (l) => ({ ...l, children: result });
220
232
  }
221
233
 
222
- function renderGroup(props: GroupRendererProps): ReactNode {
234
+ function renderGroup(
235
+ props: GroupRendererProps,
236
+ ): (layout: ControlLayoutProps) => ControlLayoutProps {
223
237
  const renderType = props.renderOptions.type;
224
238
  const renderer =
225
239
  groupRegistrations.find((x) => isOneOf(x.renderType, renderType)) ??
226
240
  defaultRenderers.group;
227
- return renderer.render(props, formRenderers);
241
+ const result = renderer.render(props, formRenderers);
242
+ if (typeof result === "function") return result;
243
+ return (l) => ({ ...l, children: result });
228
244
  }
229
245
 
230
246
  function renderAction(props: ActionRendererProps) {
@@ -314,32 +330,32 @@ export function createDefaultArrayRenderer(
314
330
  } = options ?? {};
315
331
  function render(
316
332
  {
317
- childCount,
318
- renderChild,
333
+ elementCount,
334
+ renderElement,
319
335
  addAction,
320
336
  removeAction,
321
- childKey,
337
+ elementKey,
322
338
  required,
323
339
  }: ArrayRendererProps,
324
340
  { renderAction }: FormRenderer,
325
341
  ) {
326
- const showRemove = !required || childCount > 1;
342
+ const showRemove = !required || elementCount > 1;
327
343
  return (
328
344
  <div>
329
345
  <div className={clsx(className, removeAction && removableClass)}>
330
- {Array.from({ length: childCount }, (_, x) =>
346
+ {Array.from({ length: elementCount }, (_, x) =>
331
347
  removeAction ? (
332
- <Fragment key={childKey(x)}>
348
+ <Fragment key={elementKey(x)}>
333
349
  <div className={clsx(childClass, removableChildClass)}>
334
- {renderChild(x)}
350
+ {renderElement(x)}
335
351
  </div>
336
352
  <div className={removeActionClass}>
337
353
  {showRemove && renderAction(removeAction(x))}
338
354
  </div>
339
355
  </Fragment>
340
356
  ) : (
341
- <div key={childKey(x)} className={childClass}>
342
- {renderChild(x)}
357
+ <div key={elementKey(x)} className={childClass}>
358
+ {renderElement(x)}
343
359
  </div>
344
360
  ),
345
361
  )}
@@ -364,6 +380,8 @@ interface DefaultGroupRendererOptions {
364
380
  gridStyles?: (columns: GridRenderer) => StyleProps;
365
381
  gridClassName?: string;
366
382
  defaultGridColumns?: number;
383
+ flexClassName?: string;
384
+ defaultFlexGap?: string;
367
385
  }
368
386
 
369
387
  export function createDefaultGroupRenderer(
@@ -375,6 +393,8 @@ export function createDefaultGroupRenderer(
375
393
  defaultGridColumns = 2,
376
394
  gridClassName,
377
395
  standardClassName,
396
+ flexClassName,
397
+ defaultFlexGap,
378
398
  } = options ?? {};
379
399
 
380
400
  function defaultGridStyles({
@@ -389,17 +409,38 @@ export function createDefaultGroupRenderer(
389
409
  };
390
410
  }
391
411
 
412
+ function flexStyles(options: FlexRenderer): StyleProps {
413
+ return {
414
+ className: flexClassName,
415
+ style: {
416
+ display: "flex",
417
+ gap: options.gap ? options.gap : defaultFlexGap,
418
+ flexDirection: options.direction
419
+ ? (options.direction as any)
420
+ : undefined,
421
+ },
422
+ };
423
+ }
424
+
392
425
  function render(props: GroupRendererProps) {
393
426
  const { childCount, renderChild, renderOptions } = props;
394
427
 
395
428
  const { style, className: gcn } = isGridRenderer(renderOptions)
396
429
  ? gridStyles(renderOptions)
430
+ : isFlexRenderer(renderOptions)
431
+ ? flexStyles(renderOptions)
397
432
  : ({ className: standardClassName } as StyleProps);
398
- return (
399
- <div className={clsx(className, gcn)} style={style}>
400
- {Array.from({ length: childCount }, (_, x) => renderChild(x))}
401
- </div>
402
- );
433
+
434
+ return (cp: ControlLayoutProps) => {
435
+ return {
436
+ ...cp,
437
+ children: (
438
+ <div className={clsx(props.className, className, gcn)} style={style}>
439
+ {Array.from({ length: childCount }, (_, x) => renderChild(x))}
440
+ </div>
441
+ ),
442
+ };
443
+ };
403
444
  }
404
445
  return { type: "group", render };
405
446
  }
@@ -412,37 +453,57 @@ export function createDefaultDisplayRenderer(
412
453
  options: DefaultDisplayRendererOptions = {},
413
454
  ): DisplayRendererRegistration {
414
455
  return {
415
- render: ({ data }) => {
416
- switch (data.type) {
417
- case DisplayDataType.Text:
418
- return (
419
- <div className={options.textClassName}>
420
- {(data as TextDisplay).text}
421
- </div>
422
- );
423
- case DisplayDataType.Html:
424
- return (
425
- <div
426
- className={options.htmlClassName}
427
- dangerouslySetInnerHTML={{
428
- __html: (data as HtmlDisplay).html,
429
- }}
430
- />
431
- );
432
- default:
433
- return <h1>Unknown display type: {data.type}</h1>;
434
- }
435
- },
456
+ render: (props) => <DefaultDisplay {...options} {...props} />,
436
457
  type: "display",
437
458
  };
438
459
  }
439
460
 
461
+ export function DefaultDisplay({
462
+ data,
463
+ display,
464
+ className,
465
+ style,
466
+ ...options
467
+ }: DefaultDisplayRendererOptions & DisplayRendererProps) {
468
+ switch (data.type) {
469
+ case DisplayDataType.Icon:
470
+ return (
471
+ <i
472
+ style={style}
473
+ className={clsx(
474
+ className,
475
+ display ? display.value : (data as IconDisplay).iconClass,
476
+ )}
477
+ />
478
+ );
479
+ case DisplayDataType.Text:
480
+ return (
481
+ <div style={style} className={clsx(className, options.textClassName)}>
482
+ {display ? display.value : (data as TextDisplay).text}
483
+ </div>
484
+ );
485
+ case DisplayDataType.Html:
486
+ return (
487
+ <div
488
+ style={style}
489
+ className={clsx(className, options.htmlClassName)}
490
+ dangerouslySetInnerHTML={{
491
+ __html: display ? display.value ?? "" : (data as HtmlDisplay).html,
492
+ }}
493
+ />
494
+ );
495
+ default:
496
+ return <h1>Unknown display type: {data.type}</h1>;
497
+ }
498
+ }
499
+
440
500
  export const DefaultBoolOptions: FieldOption[] = [
441
501
  { name: "Yes", value: true },
442
502
  { name: "No", value: false },
443
503
  ];
444
504
  interface DefaultDataRendererOptions {
445
505
  inputClass?: string;
506
+ displayOnlyClass?: string;
446
507
  selectOptions?: SelectRendererOptions;
447
508
  booleanOptions?: FieldOption[];
448
509
  optionRenderer?: DataRendererRegistration;
@@ -452,37 +513,67 @@ export function createDefaultDataRenderer(
452
513
  options: DefaultDataRendererOptions = {},
453
514
  ): DataRendererRegistration {
454
515
  const selectRenderer = createSelectRenderer(options.selectOptions ?? {});
455
- const { inputClass, booleanOptions, optionRenderer } = {
516
+ const { inputClass, booleanOptions, optionRenderer, displayOnlyClass } = {
456
517
  optionRenderer: selectRenderer,
457
518
  booleanOptions: DefaultBoolOptions,
458
519
  ...options,
459
520
  };
460
- return createDataRenderer((props, asArray, renderers) => {
461
- if (asArray) {
462
- return asArray();
463
- }
464
- let renderType = props.renderOptions.type;
521
+ return createDataRenderer((props, renderers) => {
465
522
  const fieldType = props.field.type;
523
+ if (props.toArrayProps) {
524
+ return (p) => ({
525
+ ...p,
526
+ children: renderers.renderArray(props.toArrayProps!()),
527
+ });
528
+ }
529
+ if (fieldType === FieldType.Compound) {
530
+ return renderers.renderGroup({
531
+ style: props.style,
532
+ className: props.className,
533
+ renderOptions: { type: "Standard", hideTitle: true },
534
+ renderChild: (i) => props.renderChild(i, i, { control: props.control }),
535
+ childCount: props.childCount,
536
+ });
537
+ }
538
+ const renderOptions = props.renderOptions;
539
+ let renderType = renderOptions.type;
466
540
  if (fieldType == FieldType.Any) return <>No control for Any</>;
541
+ if (isDisplayOnlyRenderer(renderOptions))
542
+ return (p) => ({
543
+ ...p,
544
+ className: displayOnlyClass,
545
+ children: (
546
+ <DefaultDisplayOnly
547
+ field={props.field}
548
+ schemaInterface={props.dataContext.schemaInterface}
549
+ control={props.control}
550
+ className={props.className}
551
+ style={props.style}
552
+ emptyText={renderOptions.emptyText}
553
+ />
554
+ ),
555
+ });
467
556
  const isBool = fieldType === FieldType.Bool;
468
557
  if (booleanOptions != null && isBool && props.options == null) {
469
- return renderers.renderData(
470
- { ...props, options: booleanOptions },
471
- undefined,
472
- );
558
+ return renderers.renderData({ ...props, options: booleanOptions });
473
559
  }
474
560
  if (renderType === DataRenderType.Standard && hasOptions(props)) {
475
- return optionRenderer.render(props, undefined, renderers);
561
+ return optionRenderer.render(props, renderers);
476
562
  }
477
563
  switch (renderType) {
478
564
  case DataRenderType.Dropdown:
479
- return selectRenderer.render(props, undefined, renderers);
565
+ return selectRenderer.render(props, renderers);
480
566
  }
481
567
  return renderType === DataRenderType.Checkbox ? (
482
- <Fcheckbox control={props.control} />
568
+ <Fcheckbox
569
+ style={props.style}
570
+ className={props.className}
571
+ control={props.control}
572
+ />
483
573
  ) : (
484
574
  <ControlInput
485
- className={inputClass}
575
+ className={clsx(props.className, inputClass)}
576
+ style={props.style}
486
577
  id={props.id}
487
578
  readOnly={props.readonly}
488
579
  control={props.control}
@@ -492,6 +583,33 @@ export function createDefaultDataRenderer(
492
583
  });
493
584
  }
494
585
 
586
+ export function DefaultDisplayOnly({
587
+ control,
588
+ className,
589
+ emptyText,
590
+ schemaInterface,
591
+ field,
592
+ style,
593
+ }: {
594
+ control: Control<any>;
595
+ field: SchemaField;
596
+ schemaInterface: SchemaInterface;
597
+ className?: string;
598
+ style?: React.CSSProperties;
599
+ emptyText?: string | null;
600
+ }) {
601
+ const v = control.value;
602
+ const text =
603
+ (schemaInterface.isEmptyValue(field, v)
604
+ ? emptyText
605
+ : schemaInterface.textValue(field, v)) ?? "";
606
+ return (
607
+ <div style={style} className={className}>
608
+ {text}
609
+ </div>
610
+ );
611
+ }
612
+
495
613
  export function ControlInput({
496
614
  control,
497
615
  convert,
@@ -522,7 +640,18 @@ export function createDefaultAdornmentRenderer(
522
640
  ): AdornmentRendererRegistration {
523
641
  return {
524
642
  type: "adornment",
525
- render: ({ adornment }) => ({ apply: () => {}, priority: 0, adornment }),
643
+ render: ({ adornment }) => ({
644
+ apply: (rl) => {
645
+ if (isIconAdornment(adornment)) {
646
+ return appendMarkupAt(
647
+ adornment.placement ?? AdornmentPlacement.ControlStart,
648
+ <i className={adornment.iconClass} />,
649
+ )(rl);
650
+ }
651
+ },
652
+ priority: 0,
653
+ adornment,
654
+ }),
526
655
  };
527
656
  }
528
657
 
@@ -562,12 +691,19 @@ function createDefaultLayoutRenderer(
562
691
  options: DefaultLayoutRendererOptions = {},
563
692
  ) {
564
693
  return createLayoutRenderer((props, renderers) => {
565
- return (
566
- <DefaultLayout
567
- layout={renderLayoutParts(props, renderers)}
568
- {...options}
569
- />
694
+ const layout = renderLayoutParts(
695
+ { ...props, className: clsx(props.className, options.className) },
696
+ renderers,
570
697
  );
698
+ return {
699
+ children: <DefaultLayout layout={layout} {...options} />,
700
+ className: layout.className,
701
+ style: layout.style,
702
+ divRef: (e) =>
703
+ e && props.errorControl
704
+ ? (props.errorControl.meta.scrollElement = e)
705
+ : undefined,
706
+ };
571
707
  });
572
708
  }
573
709
 
@@ -639,6 +775,10 @@ function isOneOf<A>(x: A | A[] | undefined, v: A) {
639
775
  return x == null ? true : Array.isArray(x) ? x.includes(v) : v === x;
640
776
  }
641
777
 
778
+ export function isIconAdornment(a: ControlAdornment): a is IconAdornment {
779
+ return a.type === ControlAdornmentType.Icon;
780
+ }
781
+
642
782
  export function createLayoutRenderer(
643
783
  render: LayoutRendererRegistration["render"],
644
784
  options?: Partial<LayoutRendererRegistration>,
@@ -691,7 +831,7 @@ export function createSelectRenderer(options: SelectRendererOptions = {}) {
691
831
  return createDataRenderer(
692
832
  (props, asArray) => (
693
833
  <SelectDataRenderer
694
- className={options.className}
834
+ className={clsx(props.className, options.className)}
695
835
  state={props.control}
696
836
  id={props.id}
697
837
  options={props.options!}
@@ -795,49 +935,47 @@ export function createInputConversion(ft: string): InputConversion {
795
935
  }
796
936
 
797
937
  export function createDefaultVisibilityRenderer() {
798
- return createVisibilityRenderer((cv, ch) => (
799
- <DefaultVisibility visibility={cv} children={ch} />
800
- ));
938
+ return createVisibilityRenderer((props) => <DefaultVisibility {...props} />);
801
939
  }
802
940
 
803
941
  export function DefaultVisibility({
804
942
  visibility,
805
943
  children,
806
- }: {
807
- visibility: Control<Visibility | undefined>;
808
- children: () => ReactNode;
809
- }) {
944
+ className,
945
+ style,
946
+ divRef,
947
+ }: VisibilityRendererProps) {
810
948
  const v = visibility.value;
811
949
  useEffect(() => {
812
950
  if (v) {
813
951
  visibility.setValue((ex) => ({ visible: v.visible, showing: v.visible }));
814
952
  }
815
953
  }, [v?.visible]);
816
- return v?.visible ? children() : <></>;
954
+ return v?.visible ? (
955
+ <div className={clsx(className)} style={style} ref={divRef}>
956
+ {children}
957
+ </div>
958
+ ) : (
959
+ <></>
960
+ );
817
961
  }
818
962
 
819
963
  export function DefaultLayout({
820
- className,
821
964
  errorClass,
965
+ className,
822
966
  layout: { controlEnd, controlStart, label, children, errorControl },
823
967
  }: DefaultLayoutRendererOptions & {
824
968
  layout: RenderedLayout;
825
969
  }) {
826
970
  const ec = errorControl;
827
971
  const errorText = ec && ec.touched ? ec.error : undefined;
828
- const refCb = useCallback(
829
- (e: HTMLDivElement | null) => {
830
- if (ec) ec.meta.scrollElement = e;
831
- },
832
- [ec],
833
- );
834
972
  return (
835
- <div className={className} ref={refCb}>
973
+ <>
836
974
  {label}
837
975
  {controlStart}
838
976
  {children}
839
977
  {errorText && <div className={errorClass}>{errorText}</div>}
840
978
  {controlEnd}
841
- </div>
979
+ </>
842
980
  );
843
981
  }
@@ -0,0 +1,31 @@
1
+ import { FieldType, SchemaField, SchemaInterface } from "./types";
2
+
3
+ export const defaultSchemaInterface: SchemaInterface = {
4
+ isEmptyValue: defaultIsEmpty,
5
+ textValue: defaultTextValue,
6
+ };
7
+
8
+ export function defaultIsEmpty(f: SchemaField, value: any): boolean {
9
+ if (f.collection)
10
+ return Array.isArray(value) ? value.length === 0 : value == null;
11
+ switch (f.type) {
12
+ case FieldType.String:
13
+ return !value;
14
+ default:
15
+ return value == null;
16
+ }
17
+ }
18
+
19
+ export function defaultTextValue(
20
+ f: SchemaField,
21
+ value: any,
22
+ ): string | undefined {
23
+ switch (f.type) {
24
+ case FieldType.DateTime:
25
+ return new Date(value).toLocaleDateString();
26
+ case FieldType.Date:
27
+ return new Date(value).toLocaleDateString();
28
+ default:
29
+ return value != null ? value.toString() : undefined;
30
+ }
31
+ }
package/src/tailwind.tsx CHANGED
@@ -12,8 +12,9 @@ export const defaultTailwindTheme: DefaultRendererOptions = {
12
12
  addActionClass: "my-2",
13
13
  },
14
14
  group: {
15
- standardClassName: "space-y-4",
15
+ standardClassName: "flex flex-col gap-4",
16
16
  gridClassName: "gap-x-2 gap-y-4",
17
+ flexClassName: "gap-2",
17
18
  },
18
19
  action: {
19
20
  className: "bg-primary rounded-lg p-3 text-white",
@@ -22,4 +23,7 @@ export const defaultTailwindTheme: DefaultRendererOptions = {
22
23
  className: "flex flex-col",
23
24
  errorClass: "text-sm text-danger-500",
24
25
  },
26
+ data: {
27
+ displayOnlyClass: "flex flex-row items-center gap-2",
28
+ },
25
29
  };