@react-typed-forms/schemas 1.0.0-dev.17 → 1.0.0-dev.19

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.
package/src/hooks.tsx ADDED
@@ -0,0 +1,441 @@
1
+ import {
2
+ ActionControlDefinition,
3
+ ControlDefinition,
4
+ ControlDefinitionType,
5
+ DataControlDefinition,
6
+ DataRenderType,
7
+ DateComparison,
8
+ DateValidator,
9
+ DynamicPropertyType,
10
+ EntityExpression,
11
+ ExpressionType,
12
+ FieldOption,
13
+ FieldValueExpression,
14
+ GroupedControlsDefinition,
15
+ JsonataExpression,
16
+ SchemaField,
17
+ SchemaValidator,
18
+ ValidatorType,
19
+ } from "./types";
20
+ import {
21
+ ActionRendererProps,
22
+ ArrayRendererProps,
23
+ controlForField,
24
+ controlTitle,
25
+ DataRendererProps,
26
+ fieldForControl,
27
+ FormEditHooks,
28
+ FormEditState,
29
+ GroupRendererProps,
30
+ renderControl,
31
+ SchemaHooks,
32
+ Visibility,
33
+ } from "./controlRender";
34
+ import React, { Fragment, ReactElement, useEffect, useMemo } from "react";
35
+ import {
36
+ addElement,
37
+ Control,
38
+ ControlChange,
39
+ newControl,
40
+ removeElement,
41
+ useComputed,
42
+ useControl,
43
+ useControlEffect,
44
+ useValidator,
45
+ } from "@react-typed-forms/core";
46
+ import jsonata from "jsonata";
47
+ import {
48
+ addMissingControls,
49
+ elementValueForField,
50
+ findCompoundField,
51
+ findField,
52
+ isGroupControl,
53
+ isScalarField,
54
+ } from "./util";
55
+
56
+ export function useDefaultValue(
57
+ definition: DataControlDefinition,
58
+ field: SchemaField,
59
+ formState: FormEditState,
60
+ hooks: SchemaHooks,
61
+ ) {
62
+ const valueExpression = definition.dynamic?.find(
63
+ (x) => x.type === DynamicPropertyType.DefaultValue,
64
+ );
65
+ if (valueExpression) {
66
+ return hooks.useExpression(valueExpression.expr, formState).value;
67
+ }
68
+ return field.defaultValue;
69
+ }
70
+
71
+ export function useIsControlVisible(
72
+ definition: ControlDefinition,
73
+ formState: FormEditState,
74
+ hooks: SchemaHooks,
75
+ ): Visibility {
76
+ const visibleExpression = definition.dynamic?.find(
77
+ (x) => x.type === DynamicPropertyType.Visible,
78
+ );
79
+ if (visibleExpression && visibleExpression.expr) {
80
+ const exprValue = hooks.useExpression(
81
+ visibleExpression.expr,
82
+ formState,
83
+ ).value;
84
+ return {
85
+ value: Boolean(exprValue),
86
+ canChange: true,
87
+ };
88
+ }
89
+ const schemaFields = formState.fields;
90
+
91
+ const { typeControl, compoundField } = useMemo(() => {
92
+ const typeField = schemaFields.find(
93
+ (x) => isScalarField(x) && x.isTypeField,
94
+ ) as SchemaField | undefined;
95
+
96
+ const typeControl = ((typeField &&
97
+ formState.data.fields?.[typeField.field]) ??
98
+ newControl(undefined)) as Control<string | undefined>;
99
+ const compoundField =
100
+ isGroupControl(definition) && definition.compoundField
101
+ ? formState.data.fields[definition.compoundField]
102
+ : undefined;
103
+ return { typeControl, compoundField };
104
+ }, [schemaFields, formState.data]);
105
+
106
+ const fieldName = fieldForControl(definition);
107
+ const onlyForTypes = (
108
+ fieldName ? findField(schemaFields, fieldName) : undefined
109
+ )?.onlyForTypes;
110
+ const canChange = Boolean(compoundField || (onlyForTypes?.length ?? 0) > 0);
111
+ const value =
112
+ (!compoundField || compoundField.value != null) &&
113
+ (!onlyForTypes ||
114
+ onlyForTypes.length === 0 ||
115
+ Boolean(typeControl.value && onlyForTypes.includes(typeControl.value)));
116
+ return { value, canChange };
117
+ }
118
+
119
+ export function getDefaultScalarControlProperties(
120
+ definition: DataControlDefinition,
121
+ field: SchemaField,
122
+ visible: Visibility,
123
+ defaultValue: any,
124
+ control: Control<any>,
125
+ formState: FormEditState,
126
+ ): DataRendererProps {
127
+ return {
128
+ definition,
129
+ field,
130
+ defaultValue,
131
+ options: getOptionsForScalarField(field),
132
+ renderOptions: definition.renderOptions ?? {
133
+ type: DataRenderType.Standard,
134
+ },
135
+ required: definition.required ?? false,
136
+ visible,
137
+ readonly: formState.readonly ?? definition.readonly ?? false,
138
+ control,
139
+ formState,
140
+ };
141
+ }
142
+
143
+ export function getOptionsForScalarField(
144
+ field: SchemaField,
145
+ ): FieldOption[] | undefined | null {
146
+ const opts = field.options;
147
+ if (opts?.length ?? 0 > 0) {
148
+ return opts;
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ export function createDefaultSchemaHooks(): SchemaHooks {
154
+ function useExpression(
155
+ expr: EntityExpression,
156
+ formState: FormEditState,
157
+ ): Control<any | undefined> {
158
+ switch (expr.type) {
159
+ case ExpressionType.Jsonata:
160
+ const jExpr = expr as JsonataExpression;
161
+ const compiledExpr = useMemo(
162
+ () => jsonata(jExpr.expression),
163
+ [jExpr.expression],
164
+ );
165
+ const control = useControl();
166
+ useControlEffect(
167
+ () => formState.data.value,
168
+ async (v) => {
169
+ control.value = await compiledExpr.evaluate(v);
170
+ },
171
+ );
172
+ return control;
173
+ case ExpressionType.FieldValue:
174
+ const fvExpr = expr as FieldValueExpression;
175
+ return useComputed(() => {
176
+ const fv = controlForField(fvExpr.field, formState).value;
177
+ return Array.isArray(fv)
178
+ ? fv.includes(fvExpr.value)
179
+ : fv === fvExpr.value;
180
+ });
181
+ default:
182
+ return useControl(undefined);
183
+ }
184
+ }
185
+
186
+ function useValidators(
187
+ formState: FormEditState,
188
+ isVisible: boolean,
189
+ control: Control<any>,
190
+ required: boolean,
191
+ validators?: SchemaValidator[] | null,
192
+ ) {
193
+ if (required)
194
+ useValidator(
195
+ control,
196
+ (v) =>
197
+ isVisible && (v == null || v == "") ? "Please enter a value" : null,
198
+ "required",
199
+ );
200
+ validators?.forEach((v, i) => {
201
+ switch (v.type) {
202
+ case ValidatorType.Date:
203
+ processDateValidator(v as DateValidator);
204
+ break;
205
+ case ValidatorType.Jsonata:
206
+ const errorMsg = useExpression(
207
+ v satisfies EntityExpression,
208
+ formState,
209
+ );
210
+ useControlEffect(
211
+ () => [isVisible, errorMsg.value],
212
+ ([isVisible, msg]) =>
213
+ control.setError(v.type + i, isVisible ? msg : null),
214
+ true,
215
+ );
216
+ break;
217
+ }
218
+
219
+ function processDateValidator(dv: DateValidator) {
220
+ let comparisonDate: number;
221
+ if (dv.fixedDate) {
222
+ comparisonDate = Date.parse(dv.fixedDate);
223
+ } else {
224
+ const nowDate = new Date();
225
+ comparisonDate = Date.UTC(
226
+ nowDate.getFullYear(),
227
+ nowDate.getMonth(),
228
+ nowDate.getDate(),
229
+ );
230
+ if (dv.daysFromCurrent) {
231
+ comparisonDate += dv.daysFromCurrent * 86400000;
232
+ }
233
+ }
234
+ useValidator(
235
+ control,
236
+ (v) => {
237
+ if (v) {
238
+ const selDate = Date.parse(v);
239
+ const notAfter = dv.comparison === DateComparison.NotAfter;
240
+ if (
241
+ notAfter ? selDate > comparisonDate : selDate < comparisonDate
242
+ ) {
243
+ return `Date must not be ${
244
+ notAfter ? "after" : "before"
245
+ } ${new Date(comparisonDate).toDateString()}`;
246
+ }
247
+ }
248
+ return null;
249
+ },
250
+ "date" + i,
251
+ );
252
+ }
253
+ });
254
+ }
255
+ return { useExpression, useValidators };
256
+ }
257
+
258
+ export const defaultFormEditHooks = createFormEditHooks(
259
+ createDefaultSchemaHooks(),
260
+ );
261
+
262
+ export function createFormEditHooks(schemaHooks: SchemaHooks): FormEditHooks {
263
+ return {
264
+ schemaHooks,
265
+ useDataProperties(
266
+ formState,
267
+ definition,
268
+ field,
269
+ renderer,
270
+ ): DataRendererProps {
271
+ const visible = useIsControlVisible(definition, formState, schemaHooks);
272
+ const isVisible = visible.value && !formState.invisible;
273
+ const defaultValue = useDefaultValue(
274
+ definition,
275
+ field,
276
+ formState,
277
+ schemaHooks,
278
+ );
279
+ const scalarControl = formState.data.fields[field.field];
280
+
281
+ useEffect(() => {
282
+ if (!isVisible) scalarControl.value = null;
283
+ else if (scalarControl.current.value == null) {
284
+ scalarControl.value = defaultValue;
285
+ }
286
+ }, [isVisible, defaultValue]);
287
+
288
+ const dataProps = getDefaultScalarControlProperties(
289
+ definition,
290
+ field,
291
+ visible,
292
+ defaultValue,
293
+ scalarControl,
294
+ formState,
295
+ );
296
+
297
+ schemaHooks.useValidators(
298
+ formState,
299
+ isVisible,
300
+ scalarControl,
301
+ dataProps.required,
302
+ definition.validators,
303
+ );
304
+
305
+ useEffect(() => {
306
+ const subscription = scalarControl.subscribe(
307
+ (c) => (c.touched = true),
308
+ ControlChange.Validate,
309
+ );
310
+ return () => scalarControl.unsubscribe(subscription);
311
+ }, []);
312
+
313
+ if (!field.collection) return dataProps;
314
+ return {
315
+ ...dataProps,
316
+ array: defaultArrayRendererProps(
317
+ scalarControl,
318
+ field,
319
+ definition,
320
+ dataProps.readonly,
321
+ (c) => renderer.renderData({ ...dataProps, control: c }),
322
+ ),
323
+ };
324
+ },
325
+ useDisplayProperties: (fs, definition) => {
326
+ const visible = useIsControlVisible(definition, fs, schemaHooks);
327
+ return { visible, definition };
328
+ },
329
+ useGroupProperties: (fs, definition, hooks, renderers) => {
330
+ const visible = useIsControlVisible(definition, fs, schemaHooks);
331
+ const field = definition.compoundField
332
+ ? findCompoundField(fs.fields, definition.compoundField)
333
+ : undefined;
334
+ const newFs: Omit<FormEditState, "data"> & { data: Control<any> } = {
335
+ ...fs,
336
+ fields: field ? field.children : fs.fields,
337
+ data: field ? fs.data.fields[field.field] : fs.data,
338
+ invisible: !visible.value || fs.invisible,
339
+ };
340
+ const groupProps = {
341
+ visible,
342
+ hooks,
343
+ hideTitle: definition.groupOptions.hideTitle ?? false,
344
+ childCount: definition.children.length,
345
+ renderChild: (i) =>
346
+ renderControl(definition.children[i], newFs, hooks, i),
347
+ definition,
348
+ } satisfies GroupRendererProps;
349
+ if (field?.collection) {
350
+ return {
351
+ ...groupProps,
352
+ array: defaultArrayRendererProps(
353
+ newFs.data,
354
+ field,
355
+ definition,
356
+ fs.readonly,
357
+ (e) =>
358
+ renderers.renderGroup({
359
+ ...groupProps,
360
+ hideTitle: true,
361
+ renderChild: (i) =>
362
+ renderControl(
363
+ definition.children[i],
364
+ { ...newFs, data: e },
365
+ hooks,
366
+ i,
367
+ ),
368
+ }),
369
+ ),
370
+ };
371
+ }
372
+ return groupProps;
373
+ },
374
+ useActionProperties(
375
+ formState: FormEditState,
376
+ definition: ActionControlDefinition,
377
+ ): ActionRendererProps {
378
+ const visible = useIsControlVisible(definition, formState, schemaHooks);
379
+ return {
380
+ visible,
381
+ onClick: () => {},
382
+ definition,
383
+ };
384
+ },
385
+ };
386
+ }
387
+
388
+ function defaultArrayRendererProps(
389
+ control: Control<any[]>,
390
+ field: SchemaField,
391
+ definition: DataControlDefinition | GroupedControlsDefinition,
392
+ readonly: boolean | undefined | null,
393
+ renderElem: (c: Control<any>) => ReactElement,
394
+ ): ArrayRendererProps {
395
+ return {
396
+ control,
397
+ childCount: control.elements?.length ?? 0,
398
+ field,
399
+ definition,
400
+ addAction: !readonly
401
+ ? {
402
+ definition: {
403
+ title: "Add " + controlTitle(definition.title, field),
404
+ type: ControlDefinitionType.Action,
405
+ actionId: "addElement",
406
+ },
407
+ visible: { value: true, canChange: false },
408
+ onClick: () => addElement(control, elementValueForField(field)),
409
+ }
410
+ : undefined,
411
+ removeAction: !readonly
412
+ ? (i) => ({
413
+ definition: {
414
+ title: "Remove",
415
+ type: ControlDefinitionType.Action,
416
+ actionId: "removeElement",
417
+ },
418
+ visible: { value: true, canChange: false },
419
+ onClick: () => removeElement(control, control.elements[i]),
420
+ })
421
+ : undefined,
422
+ childKey: (i) => control.elements[i].uniqueId,
423
+ renderChild: (i) => {
424
+ const c = control.elements[i];
425
+ return <Fragment key={c.uniqueId}>{renderElem(c)}</Fragment>;
426
+ },
427
+ };
428
+ }
429
+
430
+ export function useControlsWithDefaults(
431
+ definition: GroupedControlsDefinition,
432
+ sf: SchemaField[],
433
+ ) {
434
+ return useMemo(
435
+ () =>
436
+ definition.children.length
437
+ ? definition
438
+ : { ...definition, children: addMissingControls(sf, []) },
439
+ [sf, definition],
440
+ );
441
+ }
package/src/index.ts CHANGED
@@ -3,3 +3,5 @@ export * from "./schemaBuilder";
3
3
  export * from "./controlRender";
4
4
  export * from "./hooks";
5
5
  export * from "./util";
6
+ export * from "./renderers";
7
+ export * from "./tailwind";