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

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