@react-typed-forms/schemas 3.0.0-dev.73

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