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

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