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

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,491 @@
1
+ import {
2
+ ActionControlDefinition,
3
+ AnyControlDefinition,
4
+ CompoundField,
5
+ ControlDefinition,
6
+ ControlDefinitionType,
7
+ DataControlDefinition,
8
+ DisplayControlDefinition,
9
+ FieldOption,
10
+ FieldType,
11
+ GroupedControlsDefinition,
12
+ SchemaField,
13
+ } from "./types";
14
+ import React, { createContext, Key, ReactElement, useContext } from "react";
15
+ import { Control, newControl, useControlEffect } from "@react-typed-forms/core";
16
+
17
+ export interface FormEditHooks {
18
+ useDataProperties(
19
+ formState: FormEditState,
20
+ definition: DataControlDefinition,
21
+ field: SchemaField
22
+ ): DataControlProperties;
23
+ useGroupProperties(
24
+ formState: FormEditState,
25
+ definition: GroupedControlsDefinition,
26
+ currentHooks: FormEditHooks
27
+ ): GroupControlProperties;
28
+ useDisplayProperties(
29
+ formState: FormEditState,
30
+ definition: DisplayControlDefinition
31
+ ): DisplayControlProperties;
32
+ useActionProperties(
33
+ formState: FormEditState,
34
+ definition: ActionControlDefinition
35
+ ): ActionControlProperties;
36
+ }
37
+
38
+ export interface DataControlProperties {
39
+ readonly: boolean;
40
+ visible: boolean;
41
+ options: FieldOption[] | undefined;
42
+ defaultValue: any;
43
+ required: boolean;
44
+ customRender?: (
45
+ props: DataRendererProps,
46
+ control: Control<any>
47
+ ) => ReactElement;
48
+ }
49
+
50
+ export interface GroupControlProperties {
51
+ visible: boolean;
52
+ hooks: FormEditHooks;
53
+ }
54
+
55
+ export interface DisplayControlProperties {
56
+ visible: boolean;
57
+ }
58
+
59
+ export interface ActionControlProperties {
60
+ visible: boolean;
61
+ onClick: () => void;
62
+ }
63
+
64
+ export interface ControlData {
65
+ [field: string]: any;
66
+ }
67
+
68
+ export interface FormEditState {
69
+ fields: SchemaField[];
70
+ data: Control<ControlData>;
71
+ readonly?: boolean;
72
+ }
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;
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;
116
+ field: SchemaField;
117
+ formEditState?: FormEditState;
118
+ }
119
+
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;
128
+ }
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;
142
+ }
143
+
144
+ export function isScalarField(sf: SchemaField): sf is SchemaField {
145
+ return !isCompoundField(sf);
146
+ }
147
+
148
+ export function isCompoundField(sf: SchemaField): sf is CompoundField {
149
+ return sf.type === FieldType.Compound;
150
+ }
151
+
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
+ }
174
+
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)
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) {
244
+ return title ? title : fieldDisplayName(field);
245
+ }
246
+
247
+ export function renderControl(
248
+ definition: AnyControlDefinition,
249
+ formState: FormEditState,
250
+ hooks: FormEditHooks,
251
+ key: Key,
252
+ wrapChild?: (key: Key, db: ReactElement) => ReactElement
253
+ ): 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
+ }
307
+ }
308
+
309
+ function DataRenderer({
310
+ hooks,
311
+ formState,
312
+ controlDef,
313
+ wrapElem,
314
+ fieldData,
315
+ }: {
316
+ hooks: FormEditHooks;
317
+ controlDef: DataControlDefinition;
318
+ formState: FormEditState;
319
+ fieldData: SchemaField;
320
+ wrapElem: (db: ReactElement) => ReactElement;
321
+ }) {
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
+ )
351
+ );
352
+ }
353
+
354
+ function ActionRenderer({
355
+ hooks,
356
+ formState,
357
+ wrapElem,
358
+ actionDef,
359
+ }: {
360
+ hooks: FormEditHooks;
361
+ actionDef: ActionControlDefinition;
362
+ formState: FormEditState;
363
+ wrapElem: (db: ReactElement) => ReactElement;
364
+ }) {
365
+ const { renderAction } = useFormRendererComponents();
366
+ const actionControlProperties = hooks.useActionProperties(
367
+ formState,
368
+ actionDef
369
+ );
370
+ if (!actionControlProperties.visible) {
371
+ return <></>;
372
+ }
373
+
374
+ return wrapElem(
375
+ renderAction({ definition: actionDef, properties: actionControlProperties })
376
+ );
377
+ }
378
+
379
+ function GroupRenderer({
380
+ hooks,
381
+ formState,
382
+ groupDef,
383
+ wrapElem,
384
+ }: {
385
+ hooks: FormEditHooks;
386
+ groupDef: GroupedControlsDefinition;
387
+ formState: FormEditState;
388
+ wrapElem: (db: ReactElement) => ReactElement;
389
+ }) {
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
+ );
439
+ }
440
+
441
+ function DisplayRenderer({
442
+ hooks,
443
+ wrapElem,
444
+ formState,
445
+ displayDef,
446
+ }: {
447
+ hooks: FormEditHooks;
448
+ displayDef: DisplayControlDefinition;
449
+ formState: FormEditState;
450
+ wrapElem: (db: ReactElement) => ReactElement;
451
+ }) {
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 })
460
+ );
461
+ }
462
+
463
+ export function controlForField(
464
+ field: string,
465
+ formState: FormEditState
466
+ ): Control<any> {
467
+ const refField = findField(formState.fields, field);
468
+ return (
469
+ (refField && formState.data.fields[refField.field]) ?? newControl(undefined)
470
+ );
471
+ }
472
+
473
+ export function fieldForControl(c: ControlDefinition) {
474
+ return isDataControl(c)
475
+ ? c.field
476
+ : isGroupControl(c)
477
+ ? c.compoundField
478
+ : undefined;
479
+ }
480
+
481
+ export function isDataControl(
482
+ c: ControlDefinition
483
+ ): c is DataControlDefinition {
484
+ return c.type === ControlDefinitionType.Data;
485
+ }
486
+
487
+ export function isGroupControl(
488
+ c: ControlDefinition
489
+ ): c is GroupedControlsDefinition {
490
+ return c.type === ControlDefinitionType.Group;
491
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,164 @@
1
+ import {
2
+ ActionControlDefinition,
3
+ DataControlDefinition,
4
+ DynamicPropertyType,
5
+ EntityExpression,
6
+ ExpressionType,
7
+ FieldOption,
8
+ FieldValueExpression,
9
+ ControlDefinition,
10
+ SchemaField,
11
+ } from "./types";
12
+ import {
13
+ ActionControlProperties,
14
+ controlForField,
15
+ DataControlProperties,
16
+ fieldForControl,
17
+ findField,
18
+ FormEditHooks,
19
+ FormEditState,
20
+ isGroupControl,
21
+ isScalarField,
22
+ } from "./controlRender";
23
+ import { useMemo } from "react";
24
+ import { Control, newControl } from "@react-typed-forms/core";
25
+
26
+ export type ExpressionHook = (
27
+ expr: EntityExpression,
28
+ formState: FormEditState
29
+ ) => any;
30
+ export function useDefaultValue(
31
+ definition: DataControlDefinition,
32
+ field: SchemaField,
33
+ formState: FormEditState,
34
+ useExpression: ExpressionHook
35
+ ) {
36
+ const valueExpression = definition.dynamic?.find(
37
+ (x) => x.type === DynamicPropertyType.DefaultValue
38
+ );
39
+ if (valueExpression) {
40
+ return useExpression(valueExpression.expr, formState);
41
+ }
42
+ return field.defaultValue;
43
+ }
44
+
45
+ export function useIsControlVisible(
46
+ definition: ControlDefinition,
47
+ formState: FormEditState,
48
+ useExpression: ExpressionHook
49
+ ) {
50
+ const visibleExpression = definition.dynamic?.find(
51
+ (x) => x.type === DynamicPropertyType.Visible
52
+ );
53
+ if (visibleExpression && visibleExpression.expr) {
54
+ return Boolean(useExpression(visibleExpression.expr, formState));
55
+ }
56
+ const schemaFields = formState.fields;
57
+
58
+ const { typeControl, compoundField } = useMemo(() => {
59
+ const typeField = schemaFields.find(
60
+ (x) => isScalarField(x) && x.isTypeField
61
+ ) as SchemaField | undefined;
62
+
63
+ const typeControl = ((typeField &&
64
+ formState.data.fields?.[typeField.field]) ??
65
+ newControl(undefined)) as Control<string | undefined>;
66
+ const compoundField =
67
+ isGroupControl(definition) && definition.compoundField
68
+ ? formState.data.fields[definition.compoundField]
69
+ : undefined;
70
+ return { typeControl, compoundField };
71
+ }, [schemaFields, formState.data]);
72
+
73
+ const fieldName = fieldForControl(definition);
74
+ const onlyForTypes = (
75
+ fieldName ? findField(schemaFields, fieldName) : undefined
76
+ )?.onlyForTypes;
77
+
78
+ return (
79
+ (!compoundField || compoundField.value != null) &&
80
+ (!onlyForTypes ||
81
+ onlyForTypes.length === 0 ||
82
+ Boolean(typeControl.value && onlyForTypes.includes(typeControl.value)))
83
+ );
84
+ }
85
+ export function getDefaultScalarControlProperties(
86
+ control: DataControlDefinition,
87
+ field: SchemaField,
88
+ visible: boolean,
89
+ defaultValue: any,
90
+ readonly?: boolean
91
+ ): DataControlProperties {
92
+ return {
93
+ defaultValue,
94
+ options: getOptionsForScalarField(field),
95
+ required: control.required ?? false,
96
+ visible,
97
+ readonly: readonly ?? control.readonly ?? false,
98
+ };
99
+ }
100
+
101
+ export function getOptionsForScalarField(
102
+ field: SchemaField
103
+ ): FieldOption[] | undefined {
104
+ const opts = field.options ?? field.restrictions?.options;
105
+ if (opts?.length ?? 0 > 0) {
106
+ return opts;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ export const defaultExpressionHook: ExpressionHook = (
112
+ expr: EntityExpression,
113
+ formState: FormEditState
114
+ ) => {
115
+ switch (expr.type) {
116
+ case ExpressionType.FieldValue:
117
+ const fvExpr = expr as FieldValueExpression;
118
+ return controlForField(fvExpr.field, formState).value === fvExpr.value;
119
+ default:
120
+ return undefined;
121
+ }
122
+ };
123
+
124
+ export function createFormEditHooks(
125
+ useExpression: ExpressionHook
126
+ ): FormEditHooks {
127
+ return {
128
+ useDataProperties(
129
+ formState: FormEditState,
130
+ definition: DataControlDefinition,
131
+ field: SchemaField
132
+ ): DataControlProperties {
133
+ const visible = useIsControlVisible(definition, formState, useExpression);
134
+ const defaultValue = useDefaultValue(
135
+ definition,
136
+ field,
137
+ formState,
138
+ useExpression
139
+ );
140
+ return getDefaultScalarControlProperties(
141
+ definition,
142
+ field,
143
+ visible,
144
+ defaultValue,
145
+ formState.readonly
146
+ );
147
+ },
148
+ useDisplayProperties: (fs, definition) => {
149
+ const visible = useIsControlVisible(definition, fs, useExpression);
150
+ return { visible };
151
+ },
152
+ useGroupProperties: (fs, definition, hooks) => {
153
+ const visible = useIsControlVisible(definition, fs, useExpression);
154
+ return { visible, hooks };
155
+ },
156
+ useActionProperties(
157
+ formState: FormEditState,
158
+ definition: ActionControlDefinition
159
+ ): ActionControlProperties {
160
+ const visible = useIsControlVisible(definition, formState, useExpression);
161
+ return { visible, onClick: () => {} };
162
+ },
163
+ };
164
+ }
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";