@react-typed-forms/schemas 1.0.0-dev.2 → 1.0.0-dev.21

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,468 +1,278 @@
1
1
  import {
2
2
  ActionControlDefinition,
3
- AnyControlDefinition,
3
+ AdornmentPlacement,
4
4
  CompoundField,
5
+ ControlAdornment,
5
6
  ControlDefinition,
6
7
  ControlDefinitionType,
7
8
  DataControlDefinition,
8
9
  DisplayControlDefinition,
10
+ EntityExpression,
9
11
  FieldOption,
10
- FieldType,
11
12
  GroupedControlsDefinition,
13
+ RenderOptions,
12
14
  SchemaField,
15
+ SchemaValidator,
16
+ visitControlDefinition,
13
17
  } from "./types";
14
- import React, { createContext, Key, ReactElement, useContext } from "react";
15
- import { Control, newControl, useControlEffect } from "@react-typed-forms/core";
18
+ import React, { Key, ReactElement, ReactNode } from "react";
19
+ import { Control, newControl } from "@react-typed-forms/core";
20
+ import {
21
+ fieldDisplayName,
22
+ findCompoundField,
23
+ findField,
24
+ findScalarField,
25
+ isDataControl,
26
+ isGroupControl,
27
+ } from "./util";
28
+
29
+ export interface SchemaHooks {
30
+ useExpression(
31
+ expr: EntityExpression,
32
+ formState: FormEditState,
33
+ ): Control<any | undefined>;
34
+ useValidators(
35
+ formState: FormEditState,
36
+ isVisible: boolean,
37
+ control: Control<any>,
38
+ required: boolean,
39
+ validations?: SchemaValidator[] | null,
40
+ ): void;
41
+ }
16
42
 
17
43
  export interface FormEditHooks {
18
44
  useDataProperties(
19
45
  formState: FormEditState,
20
46
  definition: DataControlDefinition,
21
- field: SchemaField
22
- ): DataControlProperties;
47
+ field: SchemaField,
48
+ ): DataRendererProps;
23
49
  useGroupProperties(
24
50
  formState: FormEditState,
25
51
  definition: GroupedControlsDefinition,
26
- currentHooks: FormEditHooks
27
- ): GroupControlProperties;
52
+ ): GroupRendererProps;
28
53
  useDisplayProperties(
29
54
  formState: FormEditState,
30
- definition: DisplayControlDefinition
31
- ): DisplayControlProperties;
55
+ definition: DisplayControlDefinition,
56
+ ): DisplayRendererProps;
32
57
  useActionProperties(
33
58
  formState: FormEditState,
34
- definition: ActionControlDefinition
35
- ): ActionControlProperties;
59
+ definition: ActionControlDefinition,
60
+ ): ActionRendererProps;
61
+ schemaHooks: SchemaHooks;
36
62
  }
37
63
 
38
- export interface DataControlProperties {
64
+ export interface DataRendererProps {
65
+ definition: DataControlDefinition;
66
+ renderOptions: RenderOptions;
67
+ visible: Visibility;
68
+ control: Control<any>;
69
+ field: SchemaField;
70
+ array?: ArrayRendererProps;
39
71
  readonly: boolean;
40
- visible: boolean;
41
- options: FieldOption[] | undefined;
42
72
  defaultValue: any;
43
73
  required: boolean;
44
- customRender?: (
45
- props: DataRendererProps,
46
- control: Control<any>
47
- ) => ReactElement;
74
+ options: FieldOption[] | undefined | null;
75
+ customRender?: (props: DataRendererProps) => ReactElement;
76
+ formState: FormEditState;
48
77
  }
49
78
 
50
- export interface GroupControlProperties {
51
- visible: boolean;
79
+ export interface GroupRendererProps {
80
+ definition: Omit<GroupedControlsDefinition, "children">;
81
+ visible: Visibility;
82
+ field?: CompoundField;
83
+ array?: ArrayRendererProps;
84
+ hideTitle: boolean;
52
85
  hooks: FormEditHooks;
53
- }
54
-
55
- export interface DisplayControlProperties {
56
- visible: boolean;
57
- }
58
-
59
- export interface ActionControlProperties {
60
- visible: boolean;
61
- onClick: () => void;
86
+ childCount: number;
87
+ renderChild: (child: number) => ReactElement;
62
88
  }
63
89
 
64
90
  export interface ControlData {
65
91
  [field: string]: any;
66
92
  }
67
93
 
68
- export interface FormEditState {
94
+ export interface FormDataContext {
69
95
  fields: SchemaField[];
70
96
  data: Control<ControlData>;
71
- readonly?: boolean;
72
97
  }
73
-
74
- export interface FormRendererComponents {
75
- renderData: (
76
- props: DataRendererProps,
77
- control: Control<any>,
78
- element: boolean,
79
- renderers: FormRendererComponents
80
- ) => ReactElement;
81
- renderCompound: (
82
- props: CompoundGroupRendererProps,
83
- control: Control<any>,
84
- renderers: FormRendererComponents
85
- ) => ReactElement;
86
- renderGroup: (props: GroupRendererProps) => ReactElement;
87
- renderDisplay: (props: DisplayRendererProps) => ReactElement;
88
- renderAction: (props: ActionRendererProps) => ReactElement;
98
+ export interface FormEditState extends FormDataContext {
99
+ hooks: FormEditHooks;
100
+ renderer: FormRenderer;
101
+ readonly?: boolean;
102
+ invisible?: boolean;
89
103
  }
90
104
 
91
- export const FormRendererComponentsContext = createContext<
92
- FormRendererComponents | undefined
93
- >(undefined);
105
+ export type RenderControlOptions = Omit<FormEditState, "data">;
94
106
 
95
- export function useFormRendererComponents() {
96
- const c = useContext(FormRendererComponentsContext);
97
- if (!c) {
98
- throw "Need to use FormRendererComponentContext.Provider";
99
- }
100
- return c;
107
+ export interface ArrayRendererProps {
108
+ definition: DataControlDefinition | GroupedControlsDefinition;
109
+ control: Control<any[]>;
110
+ field: SchemaField;
111
+ addAction?: ActionRendererProps;
112
+ removeAction?: (childCount: number) => ActionRendererProps;
113
+ childCount: number;
114
+ renderChild: (childCount: number) => ReactElement;
115
+ childKey: (childCount: number) => Key;
101
116
  }
102
117
 
103
- export interface DisplayRendererProps {
104
- definition: DisplayControlDefinition;
105
- properties: DisplayControlProperties;
118
+ export interface AdornmentProps {
119
+ key: Key;
120
+ definition: ControlAdornment;
106
121
  }
107
122
 
108
- export interface ActionRendererProps {
109
- definition: ActionControlDefinition;
110
- properties: ActionControlProperties;
123
+ export interface AdornmentRenderer {
124
+ wrap?: (children: ReactElement) => ReactElement;
125
+ child?: [AdornmentPlacement, ReactNode];
111
126
  }
112
127
 
113
- export interface DataRendererProps {
114
- definition: DataControlDefinition;
115
- properties: DataControlProperties;
116
- field: SchemaField;
117
- formEditState?: FormEditState;
128
+ export interface FormRenderer {
129
+ renderData: (props: DataRendererProps) => ReactElement;
130
+ renderGroup: (props: GroupRendererProps) => ReactElement;
131
+ renderDisplay: (props: DisplayRendererProps) => ReactElement;
132
+ renderAction: (props: ActionRendererProps) => ReactElement;
133
+ renderArray: (props: ArrayRendererProps) => ReactElement;
134
+ renderLabel: (props: LabelRendererProps, elem: ReactElement) => ReactElement;
135
+ renderVisibility: (visible: Visibility, elem: ReactElement) => ReactElement;
136
+ renderAdornment: (props: AdornmentProps) => AdornmentRenderer;
118
137
  }
119
138
 
120
- export interface GroupRendererProps {
121
- definition: Omit<GroupedControlsDefinition, "children">;
122
- properties: GroupControlProperties;
123
- childCount: number;
124
- renderChild: (
125
- child: number,
126
- wrapChild: (key: Key, childElem: ReactElement) => ReactElement
127
- ) => ReactElement;
139
+ export interface Visibility {
140
+ value: boolean;
141
+ canChange: boolean;
128
142
  }
129
-
130
- export interface CompoundGroupRendererProps {
131
- definition: GroupedControlsDefinition;
132
- field: CompoundField;
133
- properties: GroupControlProperties;
134
- renderChild: (
135
- key: Key,
136
- control: ControlDefinition,
137
- data: Control<{
138
- [field: string]: any;
139
- }>,
140
- wrapChild: (key: Key, childElem: ReactElement) => ReactElement
141
- ) => ReactElement;
143
+ export interface LabelRendererProps {
144
+ visible: Visibility;
145
+ title?: ReactNode;
146
+ forId?: string;
147
+ required: boolean;
148
+ control?: Control<any>;
149
+ group?: boolean;
150
+ renderAdornment: (placement: AdornmentPlacement) => ReactElement;
142
151
  }
143
152
 
144
- export function isScalarField(sf: SchemaField): sf is SchemaField {
145
- return !isCompoundField(sf);
153
+ export interface DisplayRendererProps {
154
+ definition: DisplayControlDefinition;
155
+ visible: Visibility;
146
156
  }
147
157
 
148
- export function isCompoundField(sf: SchemaField): sf is CompoundField {
149
- return sf.type === FieldType.Compound;
158
+ export interface ActionRendererProps {
159
+ definition: ActionControlDefinition;
160
+ visible: Visibility;
161
+ onClick: () => void;
150
162
  }
151
163
 
152
164
  export type AnySchemaFields =
153
165
  | SchemaField
154
166
  | (Omit<CompoundField, "children"> & { children: AnySchemaFields[] });
155
167
 
156
- export function applyDefaultValues(
157
- v: { [k: string]: any } | undefined,
158
- fields: SchemaField[]
159
- ): any {
160
- if (!v) return defaultValueForFields(fields);
161
- const applyValue = fields.filter(
162
- (x) => isCompoundField(x) || !(x.field in v)
163
- );
164
- if (!applyValue.length) return v;
165
- const out = { ...v };
166
- applyValue.forEach((x) => {
167
- out[x.field] =
168
- x.field in v
169
- ? applyDefaultForField(v[x.field], x, fields)
170
- : defaultValueForField(x);
171
- });
172
- return out;
173
- }
174
-
175
- export function applyDefaultForField(
176
- v: any,
168
+ export function controlTitle(
169
+ title: string | undefined | null,
177
170
  field: SchemaField,
178
- parent: SchemaField[],
179
- notElement?: boolean
180
- ): any {
181
- if (field.collection && !notElement) {
182
- return ((v as any[]) ?? []).map((x) =>
183
- applyDefaultForField(x, field, parent, true)
184
- );
185
- }
186
- if (isCompoundField(field)) {
187
- if (!v && !field.required) return v;
188
- return applyDefaultValues(v, field.treeChildren ? parent : field.children);
189
- }
190
- return defaultValueForField(field);
191
- }
192
-
193
- export function defaultValueForFields(fields: SchemaField[]): any {
194
- return Object.fromEntries(
195
- fields.map((x) => [x.field, defaultValueForField(x)])
196
- );
197
- }
198
-
199
- export function defaultValueForField(sf: SchemaField): any {
200
- if (isCompoundField(sf)) {
201
- return sf.required
202
- ? sf.collection
203
- ? []
204
- : defaultValueForFields(sf.children)
205
- : undefined;
206
- }
207
- if (sf.collection) return [];
208
- return sf.defaultValue;
209
- }
210
-
211
- export function elementValueForField(sf: SchemaField): any {
212
- if (isCompoundField(sf)) {
213
- return defaultValueForFields(sf.children);
214
- }
215
- return sf.defaultValue;
216
- }
217
-
218
- export function findScalarField(
219
- fields: SchemaField[],
220
- field: string
221
- ): SchemaField | undefined {
222
- return findField(fields, field);
223
- }
224
-
225
- export function findCompoundField(
226
- fields: SchemaField[],
227
- field: string
228
- ): CompoundField | undefined {
229
- return findField(fields, field) as CompoundField | undefined;
230
- }
231
-
232
- export function findField(
233
- fields: SchemaField[],
234
- field: string
235
- ): SchemaField | undefined {
236
- return fields.find((x) => x.field === field);
237
- }
238
-
239
- export function fieldDisplayName(sf: SchemaField): string {
240
- return sf.displayName ? sf.displayName : sf.field;
241
- }
242
-
243
- export function controlTitle(title: string | undefined, field: SchemaField) {
171
+ ) {
244
172
  return title ? title : fieldDisplayName(field);
245
173
  }
246
174
 
247
- export function renderControl(
248
- definition: AnyControlDefinition,
249
- formState: FormEditState,
250
- hooks: FormEditHooks,
251
- key: Key,
252
- wrapChild?: (key: Key, db: ReactElement) => ReactElement
175
+ export function renderControl<S extends ControlDefinition>(
176
+ definition: S,
177
+ data: Control<any>,
178
+ options: RenderControlOptions,
179
+ key?: Key,
253
180
  ): ReactElement {
254
- const { fields } = formState;
255
- switch (definition.type) {
256
- case ControlDefinitionType.Data:
257
- const def = definition as DataControlDefinition;
258
- const fieldData = findScalarField(fields, def.field);
259
- if (!fieldData) return <h1>No schema field for: {def.field}</h1>;
260
- return (
261
- <DataRenderer
262
- key={key}
263
- wrapElem={wrapElem}
264
- formState={formState}
265
- hooks={hooks}
266
- controlDef={def}
267
- fieldData={fieldData}
268
- />
269
- );
270
- case ControlDefinitionType.Group:
271
- return (
272
- <GroupRenderer
273
- key={key}
274
- hooks={hooks}
275
- groupDef={definition as GroupedControlsDefinition}
276
- formState={formState}
277
- wrapElem={wrapElem}
278
- />
279
- );
280
- case ControlDefinitionType.Display:
281
- return (
282
- <DisplayRenderer
283
- key={key}
284
- hooks={hooks}
285
- formState={formState}
286
- wrapElem={wrapElem}
287
- displayDef={definition as DisplayControlDefinition}
288
- />
289
- );
290
- case ControlDefinitionType.Action:
291
- return (
292
- <ActionRenderer
293
- key={key}
294
- hooks={hooks}
295
- formState={formState}
296
- wrapElem={wrapElem}
297
- actionDef={definition as ActionControlDefinition}
298
- />
299
- );
300
- default:
301
- return <h1>Unknown control: {(definition as any).type}</h1>;
302
- }
303
-
304
- function wrapElem(e: ReactElement): ReactElement {
305
- return wrapChild?.(key, e) ?? e;
306
- }
181
+ const { fields } = options;
182
+ const formState = { ...options, data };
183
+ return visitControlDefinition(
184
+ definition,
185
+ {
186
+ data: (def) => {
187
+ const fieldData = findScalarField(fields, def.field);
188
+ if (!fieldData)
189
+ return <h1 key={key}>No schema field for: {def.field}</h1>;
190
+ return (
191
+ <DataRenderer
192
+ key={key}
193
+ formState={formState}
194
+ controlDef={def}
195
+ fieldData={fieldData}
196
+ />
197
+ );
198
+ },
199
+ group: (d: GroupedControlsDefinition) => (
200
+ <GroupRenderer key={key} groupDef={d} formState={formState} />
201
+ ),
202
+ action: (d: ActionControlDefinition) => (
203
+ <ActionRenderer key={key} formState={formState} actionDef={d} />
204
+ ),
205
+ display: (d: DisplayControlDefinition) => (
206
+ <DisplayRenderer key={key} formState={formState} displayDef={d} />
207
+ ),
208
+ },
209
+ () => <h1>Unknown control: {(definition as any).type}</h1>,
210
+ );
307
211
  }
308
212
 
213
+ /** @trackControls */
309
214
  function DataRenderer({
310
- hooks,
311
215
  formState,
312
216
  controlDef,
313
- wrapElem,
314
217
  fieldData,
315
218
  }: {
316
- hooks: FormEditHooks;
317
219
  controlDef: DataControlDefinition;
318
220
  formState: FormEditState;
319
221
  fieldData: SchemaField;
320
- wrapElem: (db: ReactElement) => ReactElement;
321
222
  }) {
322
- const renderer = useFormRendererComponents();
323
- const props = hooks.useDataProperties(formState, controlDef, fieldData);
324
- const scalarControl =
325
- formState.data.fields[fieldData.field] ?? newControl(undefined);
326
- useControlEffect(
327
- () => scalarControl.value,
328
- (v) => {
329
- if (props.defaultValue && !v) {
330
- scalarControl.value = props.defaultValue;
331
- }
332
- },
333
- true
334
- );
335
- if (!props.visible) {
336
- return <></>;
337
- }
338
- const scalarProps: DataRendererProps = {
339
- formEditState: formState,
340
- field: fieldData,
341
- definition: controlDef,
342
- properties: props,
343
- };
344
- return wrapElem(
345
- (props.customRender ?? renderer.renderData)(
346
- scalarProps,
347
- scalarControl,
348
- false,
349
- renderer
350
- )
223
+ const props = formState.hooks.useDataProperties(
224
+ formState,
225
+ controlDef,
226
+ fieldData,
351
227
  );
228
+ return (props.customRender ?? formState.renderer.renderData)(props);
352
229
  }
353
230
 
231
+ /** @trackControls */
354
232
  function ActionRenderer({
355
- hooks,
356
233
  formState,
357
- wrapElem,
358
234
  actionDef,
359
235
  }: {
360
- hooks: FormEditHooks;
361
236
  actionDef: ActionControlDefinition;
362
237
  formState: FormEditState;
363
- wrapElem: (db: ReactElement) => ReactElement;
364
238
  }) {
365
- const { renderAction } = useFormRendererComponents();
366
- const actionControlProperties = hooks.useActionProperties(
239
+ const actionControlProperties = formState.hooks.useActionProperties(
367
240
  formState,
368
- actionDef
369
- );
370
- if (!actionControlProperties.visible) {
371
- return <></>;
372
- }
373
-
374
- return wrapElem(
375
- renderAction({ definition: actionDef, properties: actionControlProperties })
241
+ actionDef,
376
242
  );
243
+ return formState.renderer.renderAction(actionControlProperties);
377
244
  }
378
245
 
246
+ /** @trackControls */
379
247
  function GroupRenderer({
380
- hooks,
381
248
  formState,
382
249
  groupDef,
383
- wrapElem,
384
250
  }: {
385
- hooks: FormEditHooks;
386
251
  groupDef: GroupedControlsDefinition;
387
252
  formState: FormEditState;
388
- wrapElem: (db: ReactElement) => ReactElement;
389
253
  }) {
390
- const renderers = useFormRendererComponents();
391
-
392
- const groupProps = hooks.useGroupProperties(formState, groupDef, hooks);
393
- if (!groupProps.visible) {
394
- return <></>;
395
- }
396
- const compoundField = groupDef.compoundField
397
- ? findCompoundField(formState.fields, groupDef.compoundField)
398
- : undefined;
399
- if (compoundField) {
400
- return wrapElem(
401
- renderers.renderCompound(
402
- {
403
- definition: groupDef,
404
- field: compoundField,
405
- properties: groupProps,
406
- renderChild: (k, c, data, wrapChild) =>
407
- renderControl(
408
- c as AnyControlDefinition,
409
- {
410
- ...formState,
411
- fields: compoundField!.children,
412
- data,
413
- },
414
- groupProps.hooks,
415
- k,
416
- wrapChild
417
- ),
418
- },
419
- formState.data.fields[compoundField.field],
420
- renderers
421
- )
422
- );
423
- }
424
- return wrapElem(
425
- renderers.renderGroup({
426
- definition: groupDef,
427
- childCount: groupDef.children.length,
428
- properties: groupProps,
429
- renderChild: (c, wrapChild) =>
430
- renderControl(
431
- groupDef.children[c],
432
- formState,
433
- groupProps.hooks,
434
- c,
435
- wrapChild
436
- ),
437
- })
438
- );
254
+ const groupProps = formState.hooks.useGroupProperties(formState, groupDef);
255
+ return formState.renderer.renderGroup(groupProps);
439
256
  }
440
257
 
258
+ /** @trackControls */
441
259
  function DisplayRenderer({
442
- hooks,
443
- wrapElem,
444
260
  formState,
445
261
  displayDef,
446
262
  }: {
447
- hooks: FormEditHooks;
448
263
  displayDef: DisplayControlDefinition;
449
264
  formState: FormEditState;
450
- wrapElem: (db: ReactElement) => ReactElement;
451
265
  }) {
452
- const { renderDisplay } = useFormRendererComponents();
453
-
454
- const displayProps = hooks.useDisplayProperties(formState, displayDef);
455
- if (!displayProps.visible) {
456
- return <></>;
457
- }
458
- return wrapElem(
459
- renderDisplay({ definition: displayDef, properties: displayProps })
266
+ const displayProps = formState.hooks.useDisplayProperties(
267
+ formState,
268
+ displayDef,
460
269
  );
270
+ return formState.renderer.renderDisplay(displayProps);
461
271
  }
462
272
 
463
273
  export function controlForField(
464
274
  field: string,
465
- formState: FormEditState
275
+ formState: FormEditState,
466
276
  ): Control<any> {
467
277
  const refField = findField(formState.fields, field);
468
278
  return (
@@ -478,14 +288,61 @@ export function fieldForControl(c: ControlDefinition) {
478
288
  : undefined;
479
289
  }
480
290
 
481
- export function isDataControl(
482
- c: ControlDefinition
483
- ): c is DataControlDefinition {
484
- return c.type === ControlDefinitionType.Data;
291
+ export const AlwaysVisible: Visibility = { value: true, canChange: false };
292
+
293
+ export function createAction(
294
+ label: string,
295
+ onClick: () => void,
296
+ actionId?: string,
297
+ ): ActionRendererProps {
298
+ return {
299
+ definition: {
300
+ type: ControlDefinitionType.Action,
301
+ actionId: actionId ?? label,
302
+ title: label,
303
+ },
304
+ visible: AlwaysVisible,
305
+ onClick,
306
+ };
485
307
  }
486
308
 
487
- export function isGroupControl(
488
- c: ControlDefinition
489
- ): c is GroupedControlsDefinition {
490
- return c.type === ControlDefinitionType.Group;
309
+ export function visitControlData<S extends ControlDefinition, A>(
310
+ definition: S,
311
+ { fields, data }: FormDataContext,
312
+ cb: (
313
+ definition: DataControlDefinition,
314
+ control: Control<any>,
315
+ ) => A | undefined,
316
+ ): A | undefined {
317
+ return visitControlDefinition<A | undefined>(
318
+ definition,
319
+ {
320
+ data(def: DataControlDefinition) {
321
+ const fieldData = findScalarField(fields, def.field);
322
+ if (!fieldData) return undefined;
323
+ return cb(def, data.fields[fieldData.field]);
324
+ },
325
+ group(d: GroupedControlsDefinition) {
326
+ if (d.compoundField) {
327
+ const compound = findCompoundField(fields, d.compoundField);
328
+ if (!compound) return;
329
+ fields = compound.children;
330
+ data = data.fields[compound.field];
331
+ }
332
+ const childState = { fields, data };
333
+ for (let c of d.children) {
334
+ const res = visitControlData(c, childState, cb);
335
+ if (res !== undefined) return res;
336
+ }
337
+ return undefined;
338
+ },
339
+ action(d: ActionControlDefinition) {
340
+ return undefined;
341
+ },
342
+ display(d: DisplayControlDefinition) {
343
+ return undefined;
344
+ },
345
+ },
346
+ () => undefined,
347
+ );
491
348
  }