@dxos/react-ui-form 0.7.4 → 0.7.5-main.499c70c
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 +209 -184
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +302 -276
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +209 -184
- package/dist/lib/node-esm/index.mjs.map +4 -4
- 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/Form.d.ts +1 -1
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +5 -0
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +3 -3
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/{Custom.d.ts → custom/GeoPointInput.d.ts} +3 -3
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +7 -0
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/index.d.ts +3 -0
- package/dist/types/src/components/Form/custom/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +2 -2
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +4 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +9 -10
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +23 -23
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.tsx +21 -13
- package/src/components/Form/Form.stories.tsx +33 -1
- package/src/components/Form/Form.tsx +3 -4
- package/src/components/Form/Input.tsx +3 -3
- package/src/components/Form/{Custom.tsx → custom/GeoPointInput.tsx} +4 -16
- package/src/components/Form/custom/TupleInput.tsx +53 -0
- package/src/components/Form/custom/index.ts +6 -0
- package/src/components/Form/factory.tsx +2 -2
- package/src/components/Form/index.ts +3 -2
- package/src/components/ViewEditor/ViewEditor.stories.tsx +6 -5
- package/src/components/ViewEditor/ViewEditor.tsx +4 -4
- package/src/hooks/useForm.ts +32 -52
- package/dist/types/src/components/Form/Custom.d.ts.map +0 -1
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
-
import React, { useCallback,
|
|
8
|
+
import React, { useCallback, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { Format, type
|
|
10
|
+
import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
11
11
|
import { useSpace } from '@dxos/react-client/echo';
|
|
12
12
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
+
import { useAsyncEffect } from '@dxos/react-ui';
|
|
13
14
|
import { type ViewType, ViewProjection, createView } from '@dxos/schema';
|
|
14
15
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
15
16
|
|
|
@@ -19,10 +20,10 @@ import { TestLayout, TestPanel } from '../testing';
|
|
|
19
20
|
|
|
20
21
|
const DefaultStory = () => {
|
|
21
22
|
const space = useSpace();
|
|
22
|
-
const [schema, setSchema] = useState<
|
|
23
|
+
const [schema, setSchema] = useState<EchoSchema>();
|
|
23
24
|
const [view, setView] = useState<ViewType>();
|
|
24
25
|
const [projection, setProjection] = useState<ViewProjection>();
|
|
25
|
-
|
|
26
|
+
useAsyncEffect(async () => {
|
|
26
27
|
if (space) {
|
|
27
28
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
28
29
|
name: S.String,
|
|
@@ -30,7 +31,7 @@ const DefaultStory = () => {
|
|
|
30
31
|
salary: Format.Currency(),
|
|
31
32
|
}) {}
|
|
32
33
|
|
|
33
|
-
const schema = space.db.schemaRegistry.
|
|
34
|
+
const [schema] = await space.db.schemaRegistry.register([TestSchema]);
|
|
34
35
|
const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: toJsonSchema(TestSchema) });
|
|
35
36
|
const projection = new ViewProjection(schema, view);
|
|
36
37
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
8
|
-
import { AST, Format, type
|
|
7
|
+
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
8
|
+
import { AST, Format, type EchoSchema, S } from '@dxos/echo-schema';
|
|
9
9
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { List } from '@dxos/react-ui-list';
|
|
11
11
|
import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
|
|
@@ -30,9 +30,9 @@ const ViewMetaSchema = S.Struct({
|
|
|
30
30
|
type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
|
|
31
31
|
|
|
32
32
|
export type ViewEditorProps = ThemedClassName<{
|
|
33
|
-
schema:
|
|
33
|
+
schema: EchoSchema;
|
|
34
34
|
view: ViewType;
|
|
35
|
-
registry?:
|
|
35
|
+
registry?: SchemaRegistry;
|
|
36
36
|
readonly?: boolean;
|
|
37
37
|
showHeading?: boolean;
|
|
38
38
|
onDelete: (fieldId: string) => void;
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -4,16 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { type FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
8
|
-
import { type SimpleType, type S } from '@dxos/effect';
|
|
7
|
+
import { type BaseObject, type PropertyKey, getValue, setValue } from '@dxos/echo-schema';
|
|
8
|
+
import { type SimpleType, type S, type JsonPath } 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
12
|
import { type MaybePromise } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
// TODO(ZaymonFC): Where should this live?
|
|
15
|
-
export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
|
|
16
|
-
|
|
17
14
|
/**
|
|
18
15
|
* Return type from `useForm` hook.
|
|
19
16
|
*/
|
|
@@ -22,10 +19,10 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
22
19
|
// Form state management.
|
|
23
20
|
//
|
|
24
21
|
|
|
25
|
-
values: T
|
|
26
|
-
errors: Record<
|
|
27
|
-
touched: Record<
|
|
28
|
-
changed: Record<
|
|
22
|
+
values: Partial<T>;
|
|
23
|
+
errors: Record<PropertyKey<T>, string>;
|
|
24
|
+
touched: Record<PropertyKey<T>, boolean>;
|
|
25
|
+
changed: Record<PropertyKey<T>, boolean>;
|
|
29
26
|
canSave: boolean;
|
|
30
27
|
handleSave: () => void;
|
|
31
28
|
|
|
@@ -33,9 +30,9 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
33
30
|
// Form input component helpers.
|
|
34
31
|
//
|
|
35
32
|
|
|
36
|
-
getStatus: (property:
|
|
37
|
-
getValue: <V>(property:
|
|
38
|
-
onValueChange: <V>(property:
|
|
33
|
+
getStatus: (property: PropertyKey<T>) => { status?: 'error'; error?: string };
|
|
34
|
+
getValue: <V>(property: PropertyKey<T>) => V | undefined;
|
|
35
|
+
onValueChange: <V>(property: PropertyKey<T>, type: SimpleType, value: V) => void;
|
|
39
36
|
onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
40
37
|
};
|
|
41
38
|
|
|
@@ -52,15 +49,14 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
52
49
|
/**
|
|
53
50
|
* Initial values (which may not pass validation).
|
|
54
51
|
*/
|
|
55
|
-
|
|
56
|
-
initialValues: T;
|
|
52
|
+
initialValues: Partial<T>;
|
|
57
53
|
|
|
58
54
|
/**
|
|
59
55
|
* Callback for value changes. Note: This is called even when values are invalid.
|
|
60
56
|
* Sometimes the parent component may want to know about changes even if the form is
|
|
61
57
|
* in an invalid state.
|
|
62
58
|
*/
|
|
63
|
-
onValuesChanged?: (values: T) => void;
|
|
59
|
+
onValuesChanged?: (values: Partial<T>) => void;
|
|
64
60
|
|
|
65
61
|
/**
|
|
66
62
|
* Custom validation function that runs only after schema validation passes.
|
|
@@ -93,14 +89,14 @@ export const useForm = <T extends BaseObject>({
|
|
|
93
89
|
onValid,
|
|
94
90
|
onSave,
|
|
95
91
|
}: FormOptions<T>): FormHandler<T> => {
|
|
96
|
-
const [values, setValues] = useState<T
|
|
92
|
+
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
97
93
|
useEffect(() => {
|
|
98
94
|
setValues(initialValues);
|
|
99
95
|
}, [initialValues]);
|
|
100
96
|
|
|
101
|
-
const [touched, setTouched] = useState<Record<
|
|
102
|
-
const [changed, setChanged] = useState<Record<
|
|
103
|
-
const [errors, setErrors] = useState<Record<
|
|
97
|
+
const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
98
|
+
const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
99
|
+
const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
|
|
104
100
|
const [saving, setSaving] = useState(false);
|
|
105
101
|
|
|
106
102
|
//
|
|
@@ -109,14 +105,16 @@ export const useForm = <T extends BaseObject>({
|
|
|
109
105
|
|
|
110
106
|
// TODO(burdon): Validate each property separately.
|
|
111
107
|
const validate = useCallback(
|
|
112
|
-
(values: T) => {
|
|
108
|
+
(values: Partial<T>): values is T => {
|
|
113
109
|
let errors: ValidationError[] = validateSchema(schema, values) ?? [];
|
|
114
110
|
if (errors.length === 0 && onValidate) {
|
|
115
|
-
|
|
111
|
+
const validatedValues = values as T;
|
|
112
|
+
errors = onValidate(validatedValues) ?? [];
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
setErrors(flatMap(errors));
|
|
119
|
-
|
|
116
|
+
const valid = errors.length === 0;
|
|
117
|
+
return valid;
|
|
120
118
|
},
|
|
121
119
|
[schema, onValidate],
|
|
122
120
|
);
|
|
@@ -138,9 +136,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
138
136
|
const canSave = useMemo(
|
|
139
137
|
() =>
|
|
140
138
|
!saving &&
|
|
141
|
-
Object.keys(values).every(
|
|
142
|
-
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
143
|
-
),
|
|
139
|
+
Object.keys(values).every((property) => touched[property as JsonPath] === false || !errors[property as JsonPath]),
|
|
144
140
|
[values, touched, errors, saving],
|
|
145
141
|
);
|
|
146
142
|
|
|
@@ -153,14 +149,14 @@ export const useForm = <T extends BaseObject>({
|
|
|
153
149
|
setSaving(false);
|
|
154
150
|
}
|
|
155
151
|
}
|
|
156
|
-
}, [values, validate, onSave]);
|
|
152
|
+
}, [values, validate, onSave, changed]);
|
|
157
153
|
|
|
158
154
|
//
|
|
159
155
|
// Fields.
|
|
160
156
|
//
|
|
161
157
|
|
|
162
158
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
163
|
-
(property:
|
|
159
|
+
(property: PropertyKey<T>) => ({
|
|
164
160
|
status: errors[property] ? 'error' : undefined,
|
|
165
161
|
error: errors[property] ? errors[property] : undefined,
|
|
166
162
|
}),
|
|
@@ -168,12 +164,11 @@ export const useForm = <T extends BaseObject>({
|
|
|
168
164
|
);
|
|
169
165
|
|
|
170
166
|
// TODO(burdon): Use path to extract hierarchical value.
|
|
171
|
-
const
|
|
172
|
-
return
|
|
167
|
+
const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
|
|
168
|
+
return getValue(values, property as any as JsonPath);
|
|
173
169
|
};
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
|
|
171
|
+
const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
|
|
177
172
|
let parsedValue = value;
|
|
178
173
|
try {
|
|
179
174
|
if (type === 'number') {
|
|
@@ -184,7 +179,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
184
179
|
parsedValue = undefined;
|
|
185
180
|
}
|
|
186
181
|
|
|
187
|
-
const newValues =
|
|
182
|
+
const newValues = { ...setValue(values, property as any as JsonPath, parsedValue) };
|
|
188
183
|
setValues(newValues);
|
|
189
184
|
setChanged((prev) => ({ ...prev, [property]: true }));
|
|
190
185
|
onValuesChanged?.(newValues);
|
|
@@ -224,40 +219,25 @@ export const useForm = <T extends BaseObject>({
|
|
|
224
219
|
|
|
225
220
|
// Field utils.
|
|
226
221
|
getStatus,
|
|
227
|
-
getValue,
|
|
222
|
+
getValue: getFormValue,
|
|
228
223
|
onValueChange,
|
|
229
224
|
onBlur,
|
|
230
225
|
} satisfies FormHandler<T>;
|
|
231
226
|
};
|
|
232
227
|
|
|
233
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
228
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
|
|
234
229
|
invariant(obj);
|
|
235
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
230
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
|
|
236
231
|
};
|
|
237
232
|
|
|
238
233
|
const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
|
|
239
234
|
return errors.reduce(
|
|
240
235
|
(result, { path, message }) => {
|
|
241
236
|
if (!(path in result)) {
|
|
242
|
-
result[path as
|
|
237
|
+
result[path as PropertyKey<T>] = message;
|
|
243
238
|
}
|
|
244
239
|
return result;
|
|
245
240
|
},
|
|
246
|
-
{} as Record<
|
|
241
|
+
{} as Record<PropertyKey<T>, string>,
|
|
247
242
|
);
|
|
248
243
|
};
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Custom.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Custom.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,KAAK,UAAU,EAAiB,MAAM,mBAAmB,CAAC;AAGnE,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAOvD,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,UAAU,oFAS/C,UAAU,CAAC,CAAC,CAAC,sBA8Bf,CAAC"}
|