@dxos/react-ui-form 0.7.2 → 0.7.3-staging.0905f03
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/dist/lib/browser/index.mjs +228 -58
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +228 -57
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +228 -58
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +4 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +11 -2
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +20 -5
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +5 -4
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +19 -11
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/package.json +23 -22
- package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -3
- package/src/components/FieldEditor/FieldEditor.tsx +8 -8
- package/src/components/Form/Defaults.tsx +28 -13
- package/src/components/Form/Form.stories.tsx +81 -19
- package/src/components/Form/Form.tsx +216 -29
- package/src/components/Form/Input.tsx +5 -4
- package/src/components/ViewEditor/ViewEditor.tsx +8 -7
- package/src/components/deprecated/Form.stories.tsx +4 -4
- package/src/hooks/useForm.ts +65 -32
package/src/hooks/useForm.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { type SimpleType, type S } from '@dxos/effect';
|
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
import { validateSchema, type ValidationError } from '@dxos/schema';
|
|
12
|
+
import { type MaybePromise } from '@dxos/util';
|
|
13
|
+
|
|
14
|
+
// TODO(ZaymonFC): Where should this live?
|
|
15
|
+
export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Return type from `useForm` hook.
|
|
@@ -19,19 +23,19 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
19
23
|
//
|
|
20
24
|
|
|
21
25
|
values: T;
|
|
22
|
-
errors: Record<
|
|
23
|
-
touched: Record<
|
|
24
|
-
changed: Record<
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
errors: Record<FormPath<T>, string>;
|
|
27
|
+
touched: Record<FormPath<T>, boolean>;
|
|
28
|
+
changed: Record<FormPath<T>, boolean>;
|
|
29
|
+
canSave: boolean;
|
|
30
|
+
handleSave: () => void;
|
|
27
31
|
|
|
28
32
|
//
|
|
29
33
|
// Form input component helpers.
|
|
30
34
|
//
|
|
31
35
|
|
|
32
|
-
getStatus: (property:
|
|
33
|
-
getValue: <V>(property:
|
|
34
|
-
onValueChange: <V>(property:
|
|
36
|
+
getStatus: (property: FormPath<T>) => { status?: 'error'; error?: string };
|
|
37
|
+
getValue: <V>(property: FormPath<T>) => V;
|
|
38
|
+
onValueChange: <V>(property: FormPath<T>, type: SimpleType, value: V) => void;
|
|
35
39
|
onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
36
40
|
};
|
|
37
41
|
|
|
@@ -39,9 +43,16 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
39
43
|
* Hook options.
|
|
40
44
|
*/
|
|
41
45
|
export interface FormOptions<T extends BaseObject> {
|
|
46
|
+
/**
|
|
47
|
+
* Effect schema.
|
|
48
|
+
*/
|
|
49
|
+
// TODO(burdon): Change to S.Struct<T>?
|
|
42
50
|
schema: S.Schema<T>;
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Initial values (which may not pass validation).
|
|
54
|
+
*/
|
|
55
|
+
// TODO(burdon): Should be partial?
|
|
45
56
|
initialValues: T;
|
|
46
57
|
|
|
47
58
|
/**
|
|
@@ -56,7 +67,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
56
67
|
* Use this for complex validation logic that can't be expressed in the schema.
|
|
57
68
|
* @returns Array of validation errors, or undefined if validation passes
|
|
58
69
|
*/
|
|
59
|
-
// TODO(burdon): Change to key x value
|
|
70
|
+
// TODO(burdon): Change to key x value?
|
|
60
71
|
onValidate?: (values: T) => ValidationError[] | undefined;
|
|
61
72
|
|
|
62
73
|
/**
|
|
@@ -67,7 +78,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
67
78
|
/**
|
|
68
79
|
* Called when the form is submitted and passes validation.
|
|
69
80
|
*/
|
|
70
|
-
|
|
81
|
+
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
/**
|
|
@@ -80,16 +91,17 @@ export const useForm = <T extends BaseObject>({
|
|
|
80
91
|
onValuesChanged,
|
|
81
92
|
onValidate,
|
|
82
93
|
onValid,
|
|
83
|
-
|
|
94
|
+
onSave,
|
|
84
95
|
}: FormOptions<T>): FormHandler<T> => {
|
|
85
96
|
const [values, setValues] = useState<T>(initialValues);
|
|
86
97
|
useEffect(() => {
|
|
87
98
|
setValues(initialValues);
|
|
88
99
|
}, [initialValues]);
|
|
89
100
|
|
|
90
|
-
const [touched, setTouched] = useState<Record<
|
|
91
|
-
const [changed, setChanged] = useState<Record<
|
|
92
|
-
const [errors, setErrors] = useState<Record<
|
|
101
|
+
const [touched, setTouched] = useState<Record<FormPath<T>, boolean>>(createKeySet(initialValues, false));
|
|
102
|
+
const [changed, setChanged] = useState<Record<FormPath<T>, boolean>>(createKeySet(initialValues, false));
|
|
103
|
+
const [errors, setErrors] = useState<Record<FormPath<T>, string>>({} as Record<FormPath<T>, string>);
|
|
104
|
+
const [saving, setSaving] = useState(false);
|
|
93
105
|
|
|
94
106
|
//
|
|
95
107
|
// Validation.
|
|
@@ -123,26 +135,32 @@ export const useForm = <T extends BaseObject>({
|
|
|
123
135
|
* NOTE: We can submit if there is no touched field that has an error.
|
|
124
136
|
* Basically, if there's a validation message visible in the form, submit should be disabled.
|
|
125
137
|
*/
|
|
126
|
-
const
|
|
138
|
+
const canSave = useMemo(
|
|
127
139
|
() =>
|
|
140
|
+
!saving &&
|
|
128
141
|
Object.keys(values).every(
|
|
129
142
|
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
130
143
|
),
|
|
131
|
-
[values, touched, errors],
|
|
144
|
+
[values, touched, errors, saving],
|
|
132
145
|
);
|
|
133
146
|
|
|
134
|
-
const
|
|
147
|
+
const handleSave = useCallback(async () => {
|
|
135
148
|
if (validate(values)) {
|
|
136
|
-
|
|
149
|
+
setSaving(true);
|
|
150
|
+
try {
|
|
151
|
+
await onSave?.(values, { changed });
|
|
152
|
+
} finally {
|
|
153
|
+
setSaving(false);
|
|
154
|
+
}
|
|
137
155
|
}
|
|
138
|
-
}, [values, validate,
|
|
156
|
+
}, [values, validate, onSave]);
|
|
139
157
|
|
|
140
158
|
//
|
|
141
159
|
// Fields.
|
|
142
160
|
//
|
|
143
161
|
|
|
144
162
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
145
|
-
(property:
|
|
163
|
+
(property: FormPath<T>) => ({
|
|
146
164
|
status: errors[property] ? 'error' : undefined,
|
|
147
165
|
error: errors[property] ? errors[property] : undefined,
|
|
148
166
|
}),
|
|
@@ -150,12 +168,12 @@ export const useForm = <T extends BaseObject>({
|
|
|
150
168
|
);
|
|
151
169
|
|
|
152
170
|
// TODO(burdon): Use path to extract hierarchical value.
|
|
153
|
-
const getValue = <V>(property:
|
|
154
|
-
return values
|
|
171
|
+
const getValue = <V>(property: FormPath<T>): V => {
|
|
172
|
+
return getByPath(values, property);
|
|
155
173
|
};
|
|
156
174
|
|
|
157
175
|
// TODO(burdon): Use path to set hierarchical value.
|
|
158
|
-
const onValueChange = (property:
|
|
176
|
+
const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
|
|
159
177
|
let parsedValue = value;
|
|
160
178
|
try {
|
|
161
179
|
if (type === 'number') {
|
|
@@ -166,7 +184,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
166
184
|
parsedValue = undefined;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
const newValues =
|
|
187
|
+
const newValues = setByPath(values, property, parsedValue);
|
|
170
188
|
setValues(newValues);
|
|
171
189
|
setChanged((prev) => ({ ...prev, [property]: true }));
|
|
172
190
|
onValuesChanged?.(newValues);
|
|
@@ -185,7 +203,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
185
203
|
|
|
186
204
|
// TODO(Zan): This should be configurable behavior.
|
|
187
205
|
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
188
|
-
// NOTE: We do this here instead of
|
|
206
|
+
// NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
|
|
189
207
|
// and results in the submit button being disabled when the form is invalid.
|
|
190
208
|
setTouched(createKeySet(values, true));
|
|
191
209
|
}
|
|
@@ -201,8 +219,8 @@ export const useForm = <T extends BaseObject>({
|
|
|
201
219
|
errors,
|
|
202
220
|
touched,
|
|
203
221
|
changed,
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
canSave,
|
|
223
|
+
handleSave,
|
|
206
224
|
|
|
207
225
|
// Field utils.
|
|
208
226
|
getStatus,
|
|
@@ -212,19 +230,34 @@ export const useForm = <T extends BaseObject>({
|
|
|
212
230
|
} satisfies FormHandler<T>;
|
|
213
231
|
};
|
|
214
232
|
|
|
215
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
233
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<FormPath<T>, V> => {
|
|
216
234
|
invariant(obj);
|
|
217
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
235
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<FormPath<T>, V>);
|
|
218
236
|
};
|
|
219
237
|
|
|
220
238
|
const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
|
|
221
239
|
return errors.reduce(
|
|
222
240
|
(result, { path, message }) => {
|
|
223
241
|
if (!(path in result)) {
|
|
224
|
-
result[path as
|
|
242
|
+
result[path as FormPath<T>] = message;
|
|
225
243
|
}
|
|
226
244
|
return result;
|
|
227
245
|
},
|
|
228
|
-
{} as Record<
|
|
246
|
+
{} as Record<FormPath<T>, string>,
|
|
229
247
|
);
|
|
230
248
|
};
|
|
249
|
+
|
|
250
|
+
// TODO(ZaymonFC): Move to util?
|
|
251
|
+
const getByPath = <T extends object, V>(obj: T, path: FormPath<T>): V => {
|
|
252
|
+
const [key, index] = path.split('.');
|
|
253
|
+
return index === undefined ? (obj[key as keyof T] as V) : ((obj[key as keyof T] as any[])[parseInt(index)] as V);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const setByPath = <T extends object>(obj: T, path: FormPath<T>, value: any): T => {
|
|
257
|
+
const [key, index] = path.split('.');
|
|
258
|
+
const newValue =
|
|
259
|
+
index === undefined
|
|
260
|
+
? value
|
|
261
|
+
: [...(obj[key as keyof T] as any[])].map((v, i) => (i === parseInt(index) ? value : v));
|
|
262
|
+
return { ...obj, [key]: newValue };
|
|
263
|
+
};
|