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

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