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

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,464 @@
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
+ visitControlDefinition,
14
+ } from "./types";
15
+ import React, { createContext, Key, ReactElement, useContext } from "react";
16
+ import { Control, newControl } from "@react-typed-forms/core";
17
+ import { fieldDisplayName } from "./index";
18
+
19
+ export interface FormEditHooks {
20
+ useDataProperties(
21
+ formState: FormEditState,
22
+ definition: DataControlDefinition,
23
+ field: SchemaField
24
+ ): DataControlProperties;
25
+ useGroupProperties(
26
+ formState: FormEditState,
27
+ definition: GroupedControlsDefinition,
28
+ currentHooks: FormEditHooks
29
+ ): GroupControlProperties;
30
+ useDisplayProperties(
31
+ formState: FormEditState,
32
+ definition: DisplayControlDefinition
33
+ ): DisplayControlProperties;
34
+ useActionProperties(
35
+ formState: FormEditState,
36
+ definition: ActionControlDefinition
37
+ ): ActionControlProperties;
38
+ }
39
+
40
+ export interface DataControlProperties {
41
+ control: Control<any>;
42
+ visible: boolean;
43
+ readonly: boolean;
44
+ defaultValue: any;
45
+ required: boolean;
46
+ options: FieldOption[] | undefined | null;
47
+ customRender?: (props: DataRendererProps) => 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
+ export function controlTitle(
239
+ title: string | undefined | null,
240
+ field: SchemaField
241
+ ) {
242
+ return title ? title : fieldDisplayName(field);
243
+ }
244
+
245
+ export function renderControl<S extends ControlDefinition>(
246
+ definition: S,
247
+ formState: FormEditState,
248
+ hooks: FormEditHooks,
249
+ key: Key,
250
+ wrapChild?: (key: Key, db: ReactElement) => ReactElement
251
+ ): ReactElement {
252
+ const { fields } = formState;
253
+ return visitControlDefinition(
254
+ definition,
255
+ {
256
+ data: (def) => {
257
+ const fieldData = findScalarField(fields, def.field);
258
+ if (!fieldData) return <h1>No schema field for: {def.field}</h1>;
259
+ return (
260
+ <DataRenderer
261
+ key={key}
262
+ wrapElem={wrapElem}
263
+ formState={formState}
264
+ hooks={hooks}
265
+ controlDef={def}
266
+ fieldData={fieldData}
267
+ />
268
+ );
269
+ },
270
+ group: (d: GroupedControlsDefinition) => (
271
+ <GroupRenderer
272
+ key={key}
273
+ hooks={hooks}
274
+ groupDef={d}
275
+ formState={formState}
276
+ wrapElem={wrapElem}
277
+ />
278
+ ),
279
+ action: (d: ActionControlDefinition) => (
280
+ <ActionRenderer
281
+ key={key}
282
+ hooks={hooks}
283
+ formState={formState}
284
+ wrapElem={wrapElem}
285
+ actionDef={d}
286
+ />
287
+ ),
288
+ display: (d: DisplayControlDefinition) => (
289
+ <DisplayRenderer
290
+ key={key}
291
+ hooks={hooks}
292
+ formState={formState}
293
+ wrapElem={wrapElem}
294
+ displayDef={d}
295
+ />
296
+ ),
297
+ },
298
+ () => <h1>Unknown control: {(definition as any).type}</h1>
299
+ );
300
+
301
+ function wrapElem(e: ReactElement): ReactElement {
302
+ return wrapChild?.(key, e) ?? e;
303
+ }
304
+ }
305
+
306
+ function DataRenderer({
307
+ hooks,
308
+ formState,
309
+ controlDef,
310
+ wrapElem,
311
+ fieldData,
312
+ }: {
313
+ hooks: FormEditHooks;
314
+ controlDef: DataControlDefinition;
315
+ formState: FormEditState;
316
+ fieldData: SchemaField;
317
+ wrapElem: (db: ReactElement) => ReactElement;
318
+ }) {
319
+ const renderer = useFormRendererComponents();
320
+ const props = hooks.useDataProperties(formState, controlDef, fieldData);
321
+ const scalarProps: DataRendererProps = {
322
+ formEditState: formState,
323
+ field: fieldData,
324
+ definition: controlDef,
325
+ properties: props,
326
+ };
327
+ return wrapElem(
328
+ (props.customRender ?? renderer.renderData)(
329
+ scalarProps,
330
+ props.control,
331
+ false,
332
+ renderer
333
+ )
334
+ );
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
+ return wrapElem(
354
+ renderAction({ definition: actionDef, properties: actionControlProperties })
355
+ );
356
+ }
357
+
358
+ function GroupRenderer({
359
+ hooks,
360
+ formState,
361
+ groupDef,
362
+ wrapElem,
363
+ }: {
364
+ hooks: FormEditHooks;
365
+ groupDef: GroupedControlsDefinition;
366
+ formState: FormEditState;
367
+ wrapElem: (db: ReactElement) => ReactElement;
368
+ }) {
369
+ const renderers = useFormRendererComponents();
370
+
371
+ const groupProps = hooks.useGroupProperties(formState, groupDef, hooks);
372
+ const compoundField = groupDef.compoundField
373
+ ? findCompoundField(formState.fields, groupDef.compoundField)
374
+ : undefined;
375
+ if (compoundField) {
376
+ return wrapElem(
377
+ renderers.renderCompound(
378
+ {
379
+ definition: groupDef,
380
+ field: compoundField,
381
+ properties: groupProps,
382
+ renderChild: (k, c, data, wrapChild) =>
383
+ renderControl(
384
+ c as AnyControlDefinition,
385
+ {
386
+ ...formState,
387
+ fields: compoundField!.children,
388
+ data,
389
+ },
390
+ groupProps.hooks,
391
+ k,
392
+ wrapChild
393
+ ),
394
+ },
395
+ formState.data.fields[compoundField.field],
396
+ renderers
397
+ )
398
+ );
399
+ }
400
+ return wrapElem(
401
+ renderers.renderGroup({
402
+ definition: groupDef,
403
+ childCount: groupDef.children.length,
404
+ properties: groupProps,
405
+ renderChild: (c, wrapChild) =>
406
+ renderControl(
407
+ groupDef.children[c],
408
+ formState,
409
+ groupProps.hooks,
410
+ c,
411
+ wrapChild
412
+ ),
413
+ })
414
+ );
415
+ }
416
+
417
+ function DisplayRenderer({
418
+ hooks,
419
+ wrapElem,
420
+ formState,
421
+ displayDef,
422
+ }: {
423
+ hooks: FormEditHooks;
424
+ displayDef: DisplayControlDefinition;
425
+ formState: FormEditState;
426
+ wrapElem: (db: ReactElement) => ReactElement;
427
+ }) {
428
+ const { renderDisplay } = useFormRendererComponents();
429
+
430
+ const displayProps = hooks.useDisplayProperties(formState, displayDef);
431
+ return wrapElem(
432
+ renderDisplay({ definition: displayDef, properties: displayProps })
433
+ );
434
+ }
435
+
436
+ export function controlForField(
437
+ field: string,
438
+ formState: FormEditState
439
+ ): Control<any> {
440
+ const refField = findField(formState.fields, field);
441
+ return (
442
+ (refField && formState.data.fields[refField.field]) ?? newControl(undefined)
443
+ );
444
+ }
445
+
446
+ export function fieldForControl(c: ControlDefinition) {
447
+ return isDataControl(c)
448
+ ? c.field
449
+ : isGroupControl(c)
450
+ ? c.compoundField
451
+ : undefined;
452
+ }
453
+
454
+ export function isDataControl(
455
+ c: ControlDefinition
456
+ ): c is DataControlDefinition {
457
+ return c.type === ControlDefinitionType.Data;
458
+ }
459
+
460
+ export function isGroupControl(
461
+ c: ControlDefinition
462
+ ): c is GroupedControlsDefinition {
463
+ return c.type === ControlDefinitionType.Group;
464
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,175 @@
1
+ import {
2
+ ActionControlDefinition,
3
+ ControlDefinition,
4
+ DataControlDefinition,
5
+ DynamicPropertyType,
6
+ EntityExpression,
7
+ ExpressionType,
8
+ FieldOption,
9
+ FieldValueExpression,
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 { useEffect, 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
+ definition: DataControlDefinition,
87
+ field: SchemaField,
88
+ visible: boolean,
89
+ defaultValue: any,
90
+ control: Control<any>,
91
+ readonly?: boolean
92
+ ): DataControlProperties {
93
+ return {
94
+ defaultValue,
95
+ options: getOptionsForScalarField(field),
96
+ required: definition.required ?? false,
97
+ visible,
98
+ readonly: readonly ?? definition.readonly ?? false,
99
+ control,
100
+ };
101
+ }
102
+
103
+ export function getOptionsForScalarField(
104
+ field: SchemaField
105
+ ): FieldOption[] | undefined | null {
106
+ const opts = field.options ?? field.restrictions?.options;
107
+ if (opts?.length ?? 0 > 0) {
108
+ return opts;
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ export const defaultExpressionHook: ExpressionHook = (
114
+ expr: EntityExpression,
115
+ formState: FormEditState
116
+ ) => {
117
+ switch (expr.type) {
118
+ case ExpressionType.FieldValue:
119
+ const fvExpr = expr as FieldValueExpression;
120
+ return controlForField(fvExpr.field, formState).value === fvExpr.value;
121
+ default:
122
+ return undefined;
123
+ }
124
+ };
125
+
126
+ export function createFormEditHooks(
127
+ useExpression: ExpressionHook
128
+ ): FormEditHooks {
129
+ return {
130
+ useDataProperties(
131
+ formState: FormEditState,
132
+ definition: DataControlDefinition,
133
+ field: SchemaField
134
+ ): DataControlProperties {
135
+ const visible = useIsControlVisible(definition, formState, useExpression);
136
+ const defaultValue = useDefaultValue(
137
+ definition,
138
+ field,
139
+ formState,
140
+ useExpression
141
+ );
142
+ const scalarControl = formState.data.fields[field.field];
143
+
144
+ useEffect(() => {
145
+ if (!visible) scalarControl.value = null;
146
+ else if (scalarControl.current.value == null) {
147
+ scalarControl.value = defaultValue;
148
+ }
149
+ }, [visible, defaultValue]);
150
+ return getDefaultScalarControlProperties(
151
+ definition,
152
+ field,
153
+ visible,
154
+ defaultValue,
155
+ scalarControl,
156
+ formState.readonly
157
+ );
158
+ },
159
+ useDisplayProperties: (fs, definition) => {
160
+ const visible = useIsControlVisible(definition, fs, useExpression);
161
+ return { visible };
162
+ },
163
+ useGroupProperties: (fs, definition, hooks) => {
164
+ const visible = useIsControlVisible(definition, fs, useExpression);
165
+ return { visible, hooks };
166
+ },
167
+ useActionProperties(
168
+ formState: FormEditState,
169
+ definition: ActionControlDefinition
170
+ ): ActionControlProperties {
171
+ const visible = useIsControlVisible(definition, formState, useExpression);
172
+ return { visible, onClick: () => {} };
173
+ },
174
+ };
175
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { SchemaField } from "./types";
2
+
3
+ export * from "./types";
4
+ export * from "./schemaBuilder";
5
+ export * from "./controlRender";
6
+ export * from "./hooks";
7
+
8
+ export function fieldHasTag(field: SchemaField, tag: string) {
9
+ return Boolean(field.tags?.includes(tag));
10
+ }
11
+
12
+ export function fieldDisplayName(field: SchemaField) {
13
+ return field.displayName ?? field.field;
14
+ }