@dxos/react-ui-form 0.7.3-main.2dd075e → 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 +204 -37
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +204 -36
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +204 -37
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +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 +8 -0
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +11 -0
- 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/hooks/useForm.d.ts +9 -7
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/Form/Defaults.tsx +28 -13
- package/src/components/Form/Form.stories.tsx +42 -10
- package/src/components/Form/Form.tsx +204 -28
- package/src/components/Form/Input.tsx +5 -4
- package/src/components/ViewEditor/ViewEditor.tsx +2 -2
- package/src/hooks/useForm.ts +48 -22
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject, type FormatEnum
|
|
7
|
+
import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
|
|
8
8
|
import { type SimpleType } from '@dxos/effect';
|
|
9
9
|
import { Icon, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { errorText } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
import { type FormHandler } from '../../hooks';
|
|
12
|
+
import { type FormPath, type FormHandler } from '../../hooks';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Props passed to input components.
|
|
16
16
|
*/
|
|
17
17
|
export type InputProps<T extends BaseObject> = {
|
|
18
|
-
property:
|
|
18
|
+
property: FormPath<T>;
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
|
-
label
|
|
21
|
+
label?: string;
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
placeholder?: string;
|
|
24
|
+
inputOnly?: boolean;
|
|
24
25
|
} & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -60,7 +60,7 @@ export const ViewEditor = ({
|
|
|
60
60
|
name: view.name,
|
|
61
61
|
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
62
62
|
// TODO(burdon): Settings should have domain name owned by user.
|
|
63
|
-
typename: view.query.
|
|
63
|
+
typename: view.query.type,
|
|
64
64
|
};
|
|
65
65
|
}, [view]);
|
|
66
66
|
|
|
@@ -68,7 +68,7 @@ export const ViewEditor = ({
|
|
|
68
68
|
({ name, typename }: ViewMetaType) => {
|
|
69
69
|
requestAnimationFrame(() => {
|
|
70
70
|
view.name = name;
|
|
71
|
-
view.query.
|
|
71
|
+
view.query.type = typename;
|
|
72
72
|
schema.updateTypename(typename);
|
|
73
73
|
});
|
|
74
74
|
},
|
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,9 +23,9 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
19
23
|
//
|
|
20
24
|
|
|
21
25
|
values: T;
|
|
22
|
-
errors: Record<
|
|
23
|
-
touched: Record<
|
|
24
|
-
changed: Record<
|
|
26
|
+
errors: Record<FormPath<T>, string>;
|
|
27
|
+
touched: Record<FormPath<T>, boolean>;
|
|
28
|
+
changed: Record<FormPath<T>, boolean>;
|
|
25
29
|
canSave: boolean;
|
|
26
30
|
handleSave: () => void;
|
|
27
31
|
|
|
@@ -29,9 +33,9 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
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
|
|
|
@@ -74,7 +78,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
74
78
|
/**
|
|
75
79
|
* Called when the form is submitted and passes validation.
|
|
76
80
|
*/
|
|
77
|
-
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void
|
|
81
|
+
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -94,9 +98,10 @@ export const useForm = <T extends BaseObject>({
|
|
|
94
98
|
setValues(initialValues);
|
|
95
99
|
}, [initialValues]);
|
|
96
100
|
|
|
97
|
-
const [touched, setTouched] = useState<Record<
|
|
98
|
-
const [changed, setChanged] = useState<Record<
|
|
99
|
-
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);
|
|
100
105
|
|
|
101
106
|
//
|
|
102
107
|
// Validation.
|
|
@@ -132,15 +137,21 @@ export const useForm = <T extends BaseObject>({
|
|
|
132
137
|
*/
|
|
133
138
|
const canSave = useMemo(
|
|
134
139
|
() =>
|
|
140
|
+
!saving &&
|
|
135
141
|
Object.keys(values).every(
|
|
136
142
|
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
137
143
|
),
|
|
138
|
-
[values, touched, errors],
|
|
144
|
+
[values, touched, errors, saving],
|
|
139
145
|
);
|
|
140
146
|
|
|
141
|
-
const handleSave = useCallback(() => {
|
|
147
|
+
const handleSave = useCallback(async () => {
|
|
142
148
|
if (validate(values)) {
|
|
143
|
-
|
|
149
|
+
setSaving(true);
|
|
150
|
+
try {
|
|
151
|
+
await onSave?.(values, { changed });
|
|
152
|
+
} finally {
|
|
153
|
+
setSaving(false);
|
|
154
|
+
}
|
|
144
155
|
}
|
|
145
156
|
}, [values, validate, onSave]);
|
|
146
157
|
|
|
@@ -149,7 +160,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
149
160
|
//
|
|
150
161
|
|
|
151
162
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
152
|
-
(property:
|
|
163
|
+
(property: FormPath<T>) => ({
|
|
153
164
|
status: errors[property] ? 'error' : undefined,
|
|
154
165
|
error: errors[property] ? errors[property] : undefined,
|
|
155
166
|
}),
|
|
@@ -157,12 +168,12 @@ export const useForm = <T extends BaseObject>({
|
|
|
157
168
|
);
|
|
158
169
|
|
|
159
170
|
// TODO(burdon): Use path to extract hierarchical value.
|
|
160
|
-
const getValue = <V>(property:
|
|
161
|
-
return values
|
|
171
|
+
const getValue = <V>(property: FormPath<T>): V => {
|
|
172
|
+
return getByPath(values, property);
|
|
162
173
|
};
|
|
163
174
|
|
|
164
175
|
// TODO(burdon): Use path to set hierarchical value.
|
|
165
|
-
const onValueChange = (property:
|
|
176
|
+
const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
|
|
166
177
|
let parsedValue = value;
|
|
167
178
|
try {
|
|
168
179
|
if (type === 'number') {
|
|
@@ -173,7 +184,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
173
184
|
parsedValue = undefined;
|
|
174
185
|
}
|
|
175
186
|
|
|
176
|
-
const newValues =
|
|
187
|
+
const newValues = setByPath(values, property, parsedValue);
|
|
177
188
|
setValues(newValues);
|
|
178
189
|
setChanged((prev) => ({ ...prev, [property]: true }));
|
|
179
190
|
onValuesChanged?.(newValues);
|
|
@@ -219,19 +230,34 @@ export const useForm = <T extends BaseObject>({
|
|
|
219
230
|
} satisfies FormHandler<T>;
|
|
220
231
|
};
|
|
221
232
|
|
|
222
|
-
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> => {
|
|
223
234
|
invariant(obj);
|
|
224
|
-
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>);
|
|
225
236
|
};
|
|
226
237
|
|
|
227
238
|
const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
|
|
228
239
|
return errors.reduce(
|
|
229
240
|
(result, { path, message }) => {
|
|
230
241
|
if (!(path in result)) {
|
|
231
|
-
result[path as
|
|
242
|
+
result[path as FormPath<T>] = message;
|
|
232
243
|
}
|
|
233
244
|
return result;
|
|
234
245
|
},
|
|
235
|
-
{} as Record<
|
|
246
|
+
{} as Record<FormPath<T>, string>,
|
|
236
247
|
);
|
|
237
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
|
+
};
|