@dxos/react-ui-form 0.7.4 → 0.7.5-feature-compute.4d9d99a
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 +292 -221
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +380 -308
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +292 -221
- 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/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +10 -4
- 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 +6 -5
- 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 +10 -11
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +26 -26
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.tsx +75 -50
- package/src/components/Form/Defaults.tsx +2 -0
- package/src/components/Form/Form.stories.tsx +33 -1
- package/src/components/Form/Form.tsx +33 -5
- 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 +30 -8
- package/src/components/ViewEditor/ViewEditor.tsx +13 -8
- package/src/hooks/useForm.ts +35 -54
- package/dist/types/src/components/Form/Custom.d.ts.map +0 -1
|
@@ -15,7 +15,7 @@ import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
|
15
15
|
import { isNotFalsy } from '@dxos/util';
|
|
16
16
|
|
|
17
17
|
import { SelectInput } from './Defaults';
|
|
18
|
-
import { InputHeader, type InputComponent } from './Input';
|
|
18
|
+
import { InputHeader, type InputProps, type InputComponent } from './Input';
|
|
19
19
|
import { getInputComponent } from './factory';
|
|
20
20
|
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
21
|
import { translationKey } from '../../translations';
|
|
@@ -28,8 +28,7 @@ export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) =>
|
|
|
28
28
|
|
|
29
29
|
export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
30
30
|
{
|
|
31
|
-
|
|
32
|
-
values: T;
|
|
31
|
+
values: Partial<T>;
|
|
33
32
|
|
|
34
33
|
/** Path to the current object from the root. Used with nested forms. */
|
|
35
34
|
path?: string[];
|
|
@@ -44,8 +43,14 @@ export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
|
44
43
|
testId?: string;
|
|
45
44
|
onCancel?: () => void;
|
|
46
45
|
|
|
46
|
+
lookupComponent?: (args: {
|
|
47
|
+
prop: string;
|
|
48
|
+
schema: S.Schema<any>;
|
|
49
|
+
inputProps: InputProps<T>;
|
|
50
|
+
}) => React.ReactElement | undefined;
|
|
47
51
|
/**
|
|
48
52
|
* Map of custom renderers for specific properties.
|
|
53
|
+
* @deprecated Use lookupComponent instead.
|
|
49
54
|
*/
|
|
50
55
|
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
51
56
|
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'>
|
|
@@ -69,6 +74,7 @@ export const Form = <T extends BaseObject>({
|
|
|
69
74
|
onValidate,
|
|
70
75
|
onSave,
|
|
71
76
|
onCancel,
|
|
77
|
+
lookupComponent,
|
|
72
78
|
Custom,
|
|
73
79
|
}: FormProps<T>) => {
|
|
74
80
|
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
@@ -110,6 +116,28 @@ export const Form = <T extends BaseObject>({
|
|
|
110
116
|
const label = title ?? pipe(name, capitalize);
|
|
111
117
|
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
112
118
|
|
|
119
|
+
const FoundComponent = lookupComponent?.({
|
|
120
|
+
prop: name,
|
|
121
|
+
schema: S.make(ast),
|
|
122
|
+
inputProps: {
|
|
123
|
+
property: name,
|
|
124
|
+
type,
|
|
125
|
+
format,
|
|
126
|
+
label,
|
|
127
|
+
disabled: readonly,
|
|
128
|
+
placeholder,
|
|
129
|
+
...inputProps,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (FoundComponent) {
|
|
134
|
+
return (
|
|
135
|
+
<div key={name} role='none' className={padding}>
|
|
136
|
+
{FoundComponent}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
// Get generic input.
|
|
114
142
|
// TODO(ZaymonFC): We might need to switch this to using globs since we're now indexing into arrays.
|
|
115
143
|
let InputComponent = Custom?.[key.join('.')];
|
|
@@ -261,7 +289,7 @@ const ArrayField = <T extends BaseObject>({
|
|
|
261
289
|
<div role='none' className='flex-1'>
|
|
262
290
|
<Form<any>
|
|
263
291
|
schema={schema}
|
|
264
|
-
path={[...key, index
|
|
292
|
+
path={[...key, `[${index}]`]}
|
|
265
293
|
values={value}
|
|
266
294
|
onValuesChanged={(childValues) => {
|
|
267
295
|
const newValues = [...arrayValues];
|
|
@@ -322,7 +350,7 @@ const ArrayField = <T extends BaseObject>({
|
|
|
322
350
|
format={format}
|
|
323
351
|
label={label}
|
|
324
352
|
inputOnly
|
|
325
|
-
property={`${name}
|
|
353
|
+
property={`${name}[${index}]` as PropertyKey<T>}
|
|
326
354
|
disabled={readonly}
|
|
327
355
|
placeholder={placeholder}
|
|
328
356
|
{...inputProps}
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
|
|
7
|
+
import { type BaseObject, type FormatEnum, type PropertyKey } 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
|
|
12
|
+
import { 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: PropertyKey<T>;
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
21
|
label?: string;
|
|
@@ -6,15 +6,12 @@ import React from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { type BaseObject, type GeoPoint } from '@dxos/echo-schema';
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { safeParseFloat } from '@dxos/util';
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { translationKey } from '../../../translations';
|
|
12
|
+
import { InputHeader, type InputProps } from '../Input';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
// Custom format components.
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
export const GeiPointInput = <T extends BaseObject>({
|
|
14
|
+
export const GeoPointInput = <T extends BaseObject>({
|
|
18
15
|
property,
|
|
19
16
|
type,
|
|
20
17
|
label,
|
|
@@ -54,12 +51,3 @@ export const GeiPointInput = <T extends BaseObject>({
|
|
|
54
51
|
</Input.Root>
|
|
55
52
|
);
|
|
56
53
|
};
|
|
57
|
-
|
|
58
|
-
// TODO(burdon): Factor out.
|
|
59
|
-
const safeParseFloat = (str: string, defaultValue?: number): number | undefined => {
|
|
60
|
-
try {
|
|
61
|
-
return parseFloat(str);
|
|
62
|
-
} catch {
|
|
63
|
-
return defaultValue ?? 0;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { type BaseObject } from '@dxos/echo-schema';
|
|
8
|
+
import { Input } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
import { safeParseFloat } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { InputHeader, type InputProps } from '../Input';
|
|
13
|
+
|
|
14
|
+
const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
|
|
15
|
+
|
|
16
|
+
export const TupleInput = <T extends BaseObject>({
|
|
17
|
+
binding,
|
|
18
|
+
property,
|
|
19
|
+
type,
|
|
20
|
+
label,
|
|
21
|
+
disabled,
|
|
22
|
+
getStatus,
|
|
23
|
+
getValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
onBlur,
|
|
26
|
+
}: InputProps<T> & { binding: string[] }) => {
|
|
27
|
+
const { status, error } = getStatus?.(property);
|
|
28
|
+
|
|
29
|
+
// TODO(burdon): Generalize number/float/string, etc.
|
|
30
|
+
const values: Record<string, number> = getValue<Record<string, number>>(property) ?? {};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Input.Root validationValence={status}>
|
|
34
|
+
<InputHeader error={error}>
|
|
35
|
+
<Input.Label>{label}</Input.Label>
|
|
36
|
+
</InputHeader>
|
|
37
|
+
<div className={mx('grid gap-2', gridCols[binding.length - 1])}>
|
|
38
|
+
{binding.map((prop) => (
|
|
39
|
+
<Input.TextInput
|
|
40
|
+
key={prop}
|
|
41
|
+
type='number'
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
value={values[prop]}
|
|
44
|
+
onChange={(event) =>
|
|
45
|
+
onValueChange(property, type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })
|
|
46
|
+
}
|
|
47
|
+
onBlur={onBlur}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</Input.Root>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { type BaseObject, FormatEnum } from '@dxos/echo-schema';
|
|
6
6
|
import { type SimpleType } from '@dxos/effect';
|
|
7
7
|
|
|
8
|
-
import { GeiPointInput } from './Custom';
|
|
9
8
|
import { BooleanInput, NumberInput, TextInput } from './Defaults';
|
|
10
9
|
import { type InputComponent } from './Input';
|
|
10
|
+
import { GeoPointInput } from './custom';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
@@ -18,7 +18,7 @@ export const getInputComponent = <T extends BaseObject>(
|
|
|
18
18
|
): InputComponent<T> | undefined => {
|
|
19
19
|
switch (format) {
|
|
20
20
|
case FormatEnum.LatLng:
|
|
21
|
-
return
|
|
21
|
+
return GeoPointInput;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
switch (type) {
|
|
@@ -5,12 +5,13 @@
|
|
|
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, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { Format, type
|
|
11
|
-
import { useSpace } from '@dxos/react-client/echo';
|
|
10
|
+
import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
11
|
+
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
12
12
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
-
import {
|
|
13
|
+
import { useAsyncEffect } from '@dxos/react-ui';
|
|
14
|
+
import { ViewProjection, ViewType, createView } from '@dxos/schema';
|
|
14
15
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
15
16
|
|
|
16
17
|
import { ViewEditor } from './ViewEditor';
|
|
@@ -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
|
|
|
@@ -40,6 +41,21 @@ const DefaultStory = () => {
|
|
|
40
41
|
}
|
|
41
42
|
}, [space]);
|
|
42
43
|
|
|
44
|
+
const views = useQuery(space, Filter.schema(ViewType));
|
|
45
|
+
const currentTypename = useMemo(() => view?.query?.type, [view]);
|
|
46
|
+
const updateViewTypename = useCallback(
|
|
47
|
+
(newTypename: string) => {
|
|
48
|
+
if (!schema) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const matchingViews = views.filter((view) => view.query.type === currentTypename);
|
|
52
|
+
for (const view of matchingViews) {
|
|
53
|
+
view.query.type = newTypename;
|
|
54
|
+
}
|
|
55
|
+
schema.updateTypename(newTypename);
|
|
56
|
+
},
|
|
57
|
+
[views, schema],
|
|
58
|
+
);
|
|
43
59
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
44
60
|
|
|
45
61
|
if (!schema || !view || !projection) {
|
|
@@ -49,7 +65,13 @@ const DefaultStory = () => {
|
|
|
49
65
|
return (
|
|
50
66
|
<TestLayout json={{ schema, view, projection }}>
|
|
51
67
|
<TestPanel>
|
|
52
|
-
<ViewEditor
|
|
68
|
+
<ViewEditor
|
|
69
|
+
schema={schema}
|
|
70
|
+
view={view}
|
|
71
|
+
registry={space?.db.schemaRegistry}
|
|
72
|
+
onTypenameChanged={updateViewTypename}
|
|
73
|
+
onDelete={handleDelete}
|
|
74
|
+
/>
|
|
53
75
|
</TestPanel>
|
|
54
76
|
</TestLayout>
|
|
55
77
|
);
|
|
@@ -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,11 +30,12 @@ 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
|
+
onTypenameChanged: (typename: string) => void;
|
|
38
39
|
onDelete: (fieldId: string) => void;
|
|
39
40
|
}>;
|
|
40
41
|
|
|
@@ -48,6 +49,7 @@ export const ViewEditor = ({
|
|
|
48
49
|
registry,
|
|
49
50
|
readonly,
|
|
50
51
|
showHeading = false,
|
|
52
|
+
onTypenameChanged,
|
|
51
53
|
onDelete,
|
|
52
54
|
}: ViewEditorProps) => {
|
|
53
55
|
const { t } = useTranslation(translationKey);
|
|
@@ -67,12 +69,15 @@ export const ViewEditor = ({
|
|
|
67
69
|
const handleViewUpdate = useCallback(
|
|
68
70
|
({ name, typename }: ViewMetaType) => {
|
|
69
71
|
requestAnimationFrame(() => {
|
|
70
|
-
view.name
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if (view.name !== name) {
|
|
73
|
+
view.name = name;
|
|
74
|
+
}
|
|
75
|
+
if (view.query.type !== typename) {
|
|
76
|
+
onTypenameChanged(typename);
|
|
77
|
+
}
|
|
73
78
|
});
|
|
74
79
|
},
|
|
75
|
-
[view, schema],
|
|
80
|
+
[view, schema, onTypenameChanged],
|
|
76
81
|
);
|
|
77
82
|
|
|
78
83
|
const handleSelect = useCallback((field: FieldType) => {
|
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, AST } 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
|
|
|
@@ -44,23 +41,21 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
44
41
|
*/
|
|
45
42
|
export interface FormOptions<T extends BaseObject> {
|
|
46
43
|
/**
|
|
47
|
-
* Effect schema.
|
|
44
|
+
* Effect schema (Type literal).
|
|
48
45
|
*/
|
|
49
|
-
// TODO(burdon): Change to S.Struct<T>?
|
|
50
46
|
schema: S.Schema<T>;
|
|
51
47
|
|
|
52
48
|
/**
|
|
53
49
|
* Initial values (which may not pass validation).
|
|
54
50
|
*/
|
|
55
|
-
|
|
56
|
-
initialValues: T;
|
|
51
|
+
initialValues: Partial<T>;
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Callback for value changes. Note: This is called even when values are invalid.
|
|
60
55
|
* Sometimes the parent component may want to know about changes even if the form is
|
|
61
56
|
* in an invalid state.
|
|
62
57
|
*/
|
|
63
|
-
onValuesChanged?: (values: T) => void;
|
|
58
|
+
onValuesChanged?: (values: Partial<T>) => void;
|
|
64
59
|
|
|
65
60
|
/**
|
|
66
61
|
* Custom validation function that runs only after schema validation passes.
|
|
@@ -93,14 +88,16 @@ export const useForm = <T extends BaseObject>({
|
|
|
93
88
|
onValid,
|
|
94
89
|
onSave,
|
|
95
90
|
}: FormOptions<T>): FormHandler<T> => {
|
|
96
|
-
|
|
91
|
+
invariant(AST.isTypeLiteral(schema.ast));
|
|
92
|
+
|
|
93
|
+
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
97
94
|
useEffect(() => {
|
|
98
95
|
setValues(initialValues);
|
|
99
96
|
}, [initialValues]);
|
|
100
97
|
|
|
101
|
-
const [touched, setTouched] = useState<Record<
|
|
102
|
-
const [changed, setChanged] = useState<Record<
|
|
103
|
-
const [errors, setErrors] = useState<Record<
|
|
98
|
+
const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
99
|
+
const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
100
|
+
const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
|
|
104
101
|
const [saving, setSaving] = useState(false);
|
|
105
102
|
|
|
106
103
|
//
|
|
@@ -109,14 +106,16 @@ export const useForm = <T extends BaseObject>({
|
|
|
109
106
|
|
|
110
107
|
// TODO(burdon): Validate each property separately.
|
|
111
108
|
const validate = useCallback(
|
|
112
|
-
(values: T) => {
|
|
109
|
+
(values: Partial<T>): values is T => {
|
|
113
110
|
let errors: ValidationError[] = validateSchema(schema, values) ?? [];
|
|
114
111
|
if (errors.length === 0 && onValidate) {
|
|
115
|
-
|
|
112
|
+
const validatedValues = values as T;
|
|
113
|
+
errors = onValidate(validatedValues) ?? [];
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
setErrors(flatMap(errors));
|
|
119
|
-
|
|
117
|
+
const valid = errors.length === 0;
|
|
118
|
+
return valid;
|
|
120
119
|
},
|
|
121
120
|
[schema, onValidate],
|
|
122
121
|
);
|
|
@@ -138,9 +137,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
138
137
|
const canSave = useMemo(
|
|
139
138
|
() =>
|
|
140
139
|
!saving &&
|
|
141
|
-
Object.keys(values).every(
|
|
142
|
-
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
143
|
-
),
|
|
140
|
+
Object.keys(values).every((property) => touched[property as JsonPath] === false || !errors[property as JsonPath]),
|
|
144
141
|
[values, touched, errors, saving],
|
|
145
142
|
);
|
|
146
143
|
|
|
@@ -153,14 +150,14 @@ export const useForm = <T extends BaseObject>({
|
|
|
153
150
|
setSaving(false);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
|
-
}, [values, validate, onSave]);
|
|
153
|
+
}, [values, validate, onSave, changed]);
|
|
157
154
|
|
|
158
155
|
//
|
|
159
156
|
// Fields.
|
|
160
157
|
//
|
|
161
158
|
|
|
162
159
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
163
|
-
(property:
|
|
160
|
+
(property: PropertyKey<T>) => ({
|
|
164
161
|
status: errors[property] ? 'error' : undefined,
|
|
165
162
|
error: errors[property] ? errors[property] : undefined,
|
|
166
163
|
}),
|
|
@@ -168,12 +165,11 @@ export const useForm = <T extends BaseObject>({
|
|
|
168
165
|
);
|
|
169
166
|
|
|
170
167
|
// TODO(burdon): Use path to extract hierarchical value.
|
|
171
|
-
const
|
|
172
|
-
return
|
|
168
|
+
const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
|
|
169
|
+
return getValue(values, property as any as JsonPath);
|
|
173
170
|
};
|
|
174
171
|
|
|
175
|
-
|
|
176
|
-
const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
|
|
172
|
+
const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
|
|
177
173
|
let parsedValue = value;
|
|
178
174
|
try {
|
|
179
175
|
if (type === 'number') {
|
|
@@ -184,7 +180,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
184
180
|
parsedValue = undefined;
|
|
185
181
|
}
|
|
186
182
|
|
|
187
|
-
const newValues =
|
|
183
|
+
const newValues = { ...setValue(values, property as any as JsonPath, parsedValue) };
|
|
188
184
|
setValues(newValues);
|
|
189
185
|
setChanged((prev) => ({ ...prev, [property]: true }));
|
|
190
186
|
onValuesChanged?.(newValues);
|
|
@@ -224,40 +220,25 @@ export const useForm = <T extends BaseObject>({
|
|
|
224
220
|
|
|
225
221
|
// Field utils.
|
|
226
222
|
getStatus,
|
|
227
|
-
getValue,
|
|
223
|
+
getValue: getFormValue,
|
|
228
224
|
onValueChange,
|
|
229
225
|
onBlur,
|
|
230
226
|
} satisfies FormHandler<T>;
|
|
231
227
|
};
|
|
232
228
|
|
|
233
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
229
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
|
|
234
230
|
invariant(obj);
|
|
235
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
231
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
|
|
236
232
|
};
|
|
237
233
|
|
|
238
234
|
const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
|
|
239
235
|
return errors.reduce(
|
|
240
236
|
(result, { path, message }) => {
|
|
241
237
|
if (!(path in result)) {
|
|
242
|
-
result[path as
|
|
238
|
+
result[path as PropertyKey<T>] = message;
|
|
243
239
|
}
|
|
244
240
|
return result;
|
|
245
241
|
},
|
|
246
|
-
{} as Record<
|
|
242
|
+
{} as Record<PropertyKey<T>, string>,
|
|
247
243
|
);
|
|
248
244
|
};
|
|
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"}
|