@dxos/react-ui-form 0.7.5-feature-compute.4d9d99a → 0.7.5-labs.5f04cf6
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 +416 -409
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +461 -469
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +416 -409
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/Defaults.d.ts +5 -6
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +10 -20
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +2 -1
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts +7 -0
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormContent.d.ts +26 -0
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormContext.d.ts +20 -0
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
- package/dist/types/src/components/Form/Input.d.ts +5 -6
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -2
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -2
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +2 -2
- package/dist/types/src/components/Form/factory.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/package.json +23 -23
- package/src/components/FieldEditor/FieldEditor.tsx +4 -4
- package/src/components/Form/ArrayField.tsx +111 -0
- package/src/components/Form/Defaults.tsx +18 -33
- package/src/components/Form/Form.stories.tsx +15 -5
- package/src/components/Form/Form.tsx +37 -359
- package/src/components/Form/FormActions.tsx +41 -0
- package/src/components/Form/FormContent.tsx +155 -0
- package/src/components/Form/FormContext.tsx +48 -0
- package/src/components/Form/Input.tsx +5 -6
- package/src/components/Form/custom/GeoPointInput.tsx +6 -15
- package/src/components/Form/custom/TupleInput.tsx +5 -9
- package/src/components/Form/factory.tsx +2 -5
- package/src/hooks/useForm.ts +54 -46
|
@@ -4,30 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { 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
|
|
12
|
+
import { type FormInputStateProps } from './FormContext';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Props passed to input components.
|
|
16
16
|
*/
|
|
17
|
-
export type InputProps
|
|
18
|
-
property: PropertyKey<T>;
|
|
17
|
+
export type InputProps = {
|
|
19
18
|
type: SimpleType;
|
|
20
19
|
format?: FormatEnum;
|
|
21
20
|
label?: string;
|
|
22
21
|
disabled?: boolean;
|
|
23
22
|
placeholder?: string;
|
|
24
23
|
inputOnly?: boolean;
|
|
25
|
-
} &
|
|
24
|
+
} & FormInputStateProps;
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Form input component.
|
|
29
28
|
*/
|
|
30
|
-
export type InputComponent
|
|
29
|
+
export type InputComponent = FC<InputProps>;
|
|
31
30
|
|
|
32
31
|
export type InputHeaderProps = PropsWithChildren<{
|
|
33
32
|
error?: string;
|
|
@@ -4,26 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { type GeoPoint } from '@dxos/echo-schema';
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
11
|
import { translationKey } from '../../../translations';
|
|
12
12
|
import { InputHeader, type InputProps } from '../Input';
|
|
13
13
|
|
|
14
|
-
export const GeoPointInput =
|
|
15
|
-
property,
|
|
16
|
-
type,
|
|
17
|
-
label,
|
|
18
|
-
disabled,
|
|
19
|
-
getStatus,
|
|
20
|
-
getValue,
|
|
21
|
-
onValueChange,
|
|
22
|
-
onBlur,
|
|
23
|
-
}: InputProps<T>) => {
|
|
14
|
+
export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
|
|
24
15
|
const { t } = useTranslation(translationKey);
|
|
25
|
-
const { status, error } = getStatus
|
|
26
|
-
const [lng = 0, lat = 0] = getValue<GeoPoint>(
|
|
16
|
+
const { status, error } = getStatus();
|
|
17
|
+
const [lng = 0, lat = 0] = getValue<GeoPoint>() ?? [];
|
|
27
18
|
|
|
28
19
|
return (
|
|
29
20
|
<Input.Root validationValence={status}>
|
|
@@ -36,7 +27,7 @@ export const GeoPointInput = <T extends BaseObject>({
|
|
|
36
27
|
disabled={disabled}
|
|
37
28
|
placeholder={t('placeholder longitude')}
|
|
38
29
|
value={lng}
|
|
39
|
-
onChange={(event) => onValueChange(
|
|
30
|
+
onChange={(event) => onValueChange(type, [lat, safeParseFloat(event.target.value, 0)])}
|
|
40
31
|
onBlur={onBlur}
|
|
41
32
|
/>
|
|
42
33
|
<Input.TextInput
|
|
@@ -44,7 +35,7 @@ export const GeoPointInput = <T extends BaseObject>({
|
|
|
44
35
|
disabled={disabled}
|
|
45
36
|
placeholder={t('placeholder latitude')}
|
|
46
37
|
value={lat}
|
|
47
|
-
onChange={(event) => onValueChange(
|
|
38
|
+
onChange={(event) => onValueChange(type, [safeParseFloat(event.target.value, 0), lng])}
|
|
48
39
|
onBlur={onBlur}
|
|
49
40
|
/>
|
|
50
41
|
</div>
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject } from '@dxos/echo-schema';
|
|
8
7
|
import { Input } from '@dxos/react-ui';
|
|
9
8
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
9
|
import { safeParseFloat } from '@dxos/util';
|
|
@@ -13,9 +12,8 @@ import { InputHeader, type InputProps } from '../Input';
|
|
|
13
12
|
|
|
14
13
|
const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
|
|
15
14
|
|
|
16
|
-
export const TupleInput =
|
|
15
|
+
export const TupleInput = ({
|
|
17
16
|
binding,
|
|
18
|
-
property,
|
|
19
17
|
type,
|
|
20
18
|
label,
|
|
21
19
|
disabled,
|
|
@@ -23,11 +21,11 @@ export const TupleInput = <T extends BaseObject>({
|
|
|
23
21
|
getValue,
|
|
24
22
|
onValueChange,
|
|
25
23
|
onBlur,
|
|
26
|
-
}: InputProps
|
|
27
|
-
const { status, error } = getStatus
|
|
24
|
+
}: InputProps & { binding: string[] }) => {
|
|
25
|
+
const { status, error } = getStatus();
|
|
28
26
|
|
|
29
27
|
// TODO(burdon): Generalize number/float/string, etc.
|
|
30
|
-
const values: Record<string, number> = getValue<Record<string, number>>(
|
|
28
|
+
const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
|
|
31
29
|
|
|
32
30
|
return (
|
|
33
31
|
<Input.Root validationValence={status}>
|
|
@@ -41,9 +39,7 @@ export const TupleInput = <T extends BaseObject>({
|
|
|
41
39
|
type='number'
|
|
42
40
|
disabled={disabled}
|
|
43
41
|
value={values[prop]}
|
|
44
|
-
onChange={(event) =>
|
|
45
|
-
onValueChange(property, type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })
|
|
46
|
-
}
|
|
42
|
+
onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
|
|
47
43
|
onBlur={onBlur}
|
|
48
44
|
/>
|
|
49
45
|
))}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { FormatEnum } from '@dxos/echo-schema';
|
|
6
6
|
import { type SimpleType } from '@dxos/effect';
|
|
7
7
|
|
|
8
8
|
import { BooleanInput, NumberInput, TextInput } from './Defaults';
|
|
@@ -12,10 +12,7 @@ import { GeoPointInput } from './custom';
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
14
14
|
*/
|
|
15
|
-
export const getInputComponent =
|
|
16
|
-
type: SimpleType,
|
|
17
|
-
format?: FormatEnum,
|
|
18
|
-
): InputComponent<T> | undefined => {
|
|
15
|
+
export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
|
|
19
16
|
switch (format) {
|
|
20
17
|
case FormatEnum.LatLng:
|
|
21
18
|
return GeoPointInput;
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject,
|
|
8
|
-
import { type
|
|
7
|
+
import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
|
|
8
|
+
import { AST, type S, type SimpleType, type JsonPath, createJsonPath, fromEffectValidationPath } 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';
|
|
@@ -20,9 +20,9 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
20
20
|
//
|
|
21
21
|
|
|
22
22
|
values: Partial<T>;
|
|
23
|
-
errors: Record<
|
|
24
|
-
touched: Record<
|
|
25
|
-
changed: Record<
|
|
23
|
+
errors: Record<JsonPath, string>;
|
|
24
|
+
touched: Record<JsonPath, boolean>;
|
|
25
|
+
changed: Record<JsonPath, boolean>;
|
|
26
26
|
canSave: boolean;
|
|
27
27
|
handleSave: () => void;
|
|
28
28
|
|
|
@@ -30,10 +30,10 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
30
30
|
// Form input component helpers.
|
|
31
31
|
//
|
|
32
32
|
|
|
33
|
-
getStatus: (
|
|
34
|
-
getValue: <V>(
|
|
35
|
-
onValueChange: <V>(
|
|
36
|
-
|
|
33
|
+
getStatus: (path: string | (string | number)[]) => { status?: 'error'; error?: string };
|
|
34
|
+
getValue: <V>(path: (string | number)[]) => V | undefined;
|
|
35
|
+
onValueChange: <V>(path: (string | number)[], type: SimpleType, value: V) => void;
|
|
36
|
+
onTouched: (path: (string | number)[]) => void;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -95,9 +95,9 @@ export const useForm = <T extends BaseObject>({
|
|
|
95
95
|
setValues(initialValues);
|
|
96
96
|
}, [initialValues]);
|
|
97
97
|
|
|
98
|
-
const [touched, setTouched] = useState<Record<
|
|
99
|
-
const [changed, setChanged] = useState<Record<
|
|
100
|
-
const [errors, setErrors] = useState<Record<
|
|
98
|
+
const [touched, setTouched] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
99
|
+
const [changed, setChanged] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
100
|
+
const [errors, setErrors] = useState<Record<JsonPath, string>>({});
|
|
101
101
|
const [saving, setSaving] = useState(false);
|
|
102
102
|
|
|
103
103
|
//
|
|
@@ -137,8 +137,15 @@ export const useForm = <T extends BaseObject>({
|
|
|
137
137
|
const canSave = useMemo(
|
|
138
138
|
() =>
|
|
139
139
|
!saving &&
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// Check if any error paths that are touched have errors
|
|
141
|
+
!Object.entries(touched).some(
|
|
142
|
+
([path, isTouched]) =>
|
|
143
|
+
isTouched &&
|
|
144
|
+
Object.keys(errors).some(
|
|
145
|
+
(errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
[touched, errors, saving],
|
|
142
149
|
);
|
|
143
150
|
|
|
144
151
|
const handleSave = useCallback(async () => {
|
|
@@ -157,19 +164,27 @@ export const useForm = <T extends BaseObject>({
|
|
|
157
164
|
//
|
|
158
165
|
|
|
159
166
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
160
|
-
(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
(path) => {
|
|
168
|
+
const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
|
|
169
|
+
const matchingError = Object.entries(errors).find(
|
|
170
|
+
([errorPath]) =>
|
|
171
|
+
errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
status: matchingError ? 'error' : undefined,
|
|
176
|
+
error: matchingError ? matchingError[1] : undefined,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
[errors],
|
|
165
180
|
);
|
|
166
181
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return getValue(values, property as any as JsonPath);
|
|
182
|
+
const getFormValue = <V>(path: (string | number)[]): V | undefined => {
|
|
183
|
+
return getValue(values, createJsonPath(path));
|
|
170
184
|
};
|
|
171
185
|
|
|
172
|
-
const onValueChange = (
|
|
186
|
+
const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
|
|
187
|
+
const jsonPath = createJsonPath(path);
|
|
173
188
|
let parsedValue = value;
|
|
174
189
|
try {
|
|
175
190
|
if (type === 'number') {
|
|
@@ -180,9 +195,9 @@ export const useForm = <T extends BaseObject>({
|
|
|
180
195
|
parsedValue = undefined;
|
|
181
196
|
}
|
|
182
197
|
|
|
183
|
-
const newValues = { ...setValue(values,
|
|
198
|
+
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
184
199
|
setValues(newValues);
|
|
185
|
-
setChanged((prev) => ({ ...prev, [
|
|
200
|
+
setChanged((prev) => ({ ...prev, [jsonPath]: true }));
|
|
186
201
|
onValuesChanged?.(newValues);
|
|
187
202
|
|
|
188
203
|
const isValid = validate(newValues);
|
|
@@ -191,19 +206,10 @@ export const useForm = <T extends BaseObject>({
|
|
|
191
206
|
}
|
|
192
207
|
};
|
|
193
208
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
setTouched((touched) => ({ ...touched, [name]: true }));
|
|
199
|
-
|
|
200
|
-
// TODO(Zan): This should be configurable behavior.
|
|
201
|
-
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
202
|
-
// NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
|
|
203
|
-
// and results in the submit button being disabled when the form is invalid.
|
|
204
|
-
setTouched(createKeySet(values, true));
|
|
205
|
-
}
|
|
206
|
-
|
|
209
|
+
const onTouched = useCallback(
|
|
210
|
+
(path: (string | number)[]) => {
|
|
211
|
+
const jsonPath = createJsonPath(path);
|
|
212
|
+
setTouched((touched) => ({ ...touched, [jsonPath]: true }));
|
|
207
213
|
validate(values);
|
|
208
214
|
},
|
|
209
215
|
[validate, values],
|
|
@@ -222,23 +228,25 @@ export const useForm = <T extends BaseObject>({
|
|
|
222
228
|
getStatus,
|
|
223
229
|
getValue: getFormValue,
|
|
224
230
|
onValueChange,
|
|
225
|
-
|
|
231
|
+
onTouched,
|
|
226
232
|
} satisfies FormHandler<T>;
|
|
227
233
|
};
|
|
228
234
|
|
|
229
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
235
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
|
|
230
236
|
invariant(obj);
|
|
231
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
237
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
|
|
232
238
|
};
|
|
233
239
|
|
|
234
|
-
const flatMap =
|
|
240
|
+
const flatMap = (errors: ValidationError[]) => {
|
|
235
241
|
return errors.reduce(
|
|
236
242
|
(result, { path, message }) => {
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
// Convert the validation error path format to our JsonPath format
|
|
244
|
+
const jsonPath = fromEffectValidationPath(path);
|
|
245
|
+
if (!(jsonPath in result)) {
|
|
246
|
+
result[jsonPath] = message;
|
|
239
247
|
}
|
|
240
248
|
return result;
|
|
241
249
|
},
|
|
242
|
-
{} as Record<
|
|
250
|
+
{} as Record<JsonPath, string>,
|
|
243
251
|
);
|
|
244
252
|
};
|