@dxos/react-ui-form 0.7.4 → 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.
Files changed (65) hide show
  1. package/dist/lib/browser/index.mjs +574 -496
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +629 -565
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +574 -496
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
  13. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
  14. package/dist/types/src/components/Form/Defaults.d.ts +5 -6
  15. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +13 -17
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +7 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormActions.d.ts +7 -0
  21. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormContent.d.ts +26 -0
  23. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormContext.d.ts +20 -0
  25. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/Input.d.ts +5 -6
  27. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +4 -0
  29. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/custom/TupleInput.d.ts +6 -0
  31. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/custom/index.d.ts +3 -0
  33. package/dist/types/src/components/Form/custom/index.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/factory.d.ts +2 -2
  35. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/index.d.ts +2 -2
  37. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  38. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -5
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useForm.d.ts +13 -15
  42. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  43. package/dist/types/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +26 -26
  45. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  46. package/src/components/FieldEditor/FieldEditor.tsx +75 -50
  47. package/src/components/Form/ArrayField.tsx +111 -0
  48. package/src/components/Form/Defaults.tsx +20 -33
  49. package/src/components/Form/Form.stories.tsx +48 -6
  50. package/src/components/Form/Form.tsx +39 -333
  51. package/src/components/Form/FormActions.tsx +41 -0
  52. package/src/components/Form/FormContent.tsx +155 -0
  53. package/src/components/Form/FormContext.tsx +48 -0
  54. package/src/components/Form/Input.tsx +5 -6
  55. package/src/components/Form/custom/GeoPointInput.tsx +44 -0
  56. package/src/components/Form/custom/TupleInput.tsx +49 -0
  57. package/src/components/Form/custom/index.ts +6 -0
  58. package/src/components/Form/factory.tsx +4 -7
  59. package/src/components/Form/index.ts +3 -2
  60. package/src/components/ViewEditor/ViewEditor.stories.tsx +30 -8
  61. package/src/components/ViewEditor/ViewEditor.tsx +13 -8
  62. package/src/hooks/useForm.ts +67 -78
  63. package/dist/types/src/components/Form/Custom.d.ts +0 -5
  64. package/dist/types/src/components/Form/Custom.d.ts.map +0 -1
  65. package/src/components/Form/Custom.tsx +0 -65
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type GeoPoint } from '@dxos/echo-schema';
8
+ import { Input, useTranslation } from '@dxos/react-ui';
9
+ import { safeParseFloat } from '@dxos/util';
10
+
11
+ import { translationKey } from '../../../translations';
12
+ import { InputHeader, type InputProps } from '../Input';
13
+
14
+ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
15
+ const { t } = useTranslation(translationKey);
16
+ const { status, error } = getStatus();
17
+ const [lng = 0, lat = 0] = getValue<GeoPoint>() ?? [];
18
+
19
+ return (
20
+ <Input.Root validationValence={status}>
21
+ <InputHeader error={error}>
22
+ <Input.Label>{label}</Input.Label>
23
+ </InputHeader>
24
+ <div className='grid grid-cols-2 gap-2'>
25
+ <Input.TextInput
26
+ type='number'
27
+ disabled={disabled}
28
+ placeholder={t('placeholder longitude')}
29
+ value={lng}
30
+ onChange={(event) => onValueChange(type, [lat, safeParseFloat(event.target.value, 0)])}
31
+ onBlur={onBlur}
32
+ />
33
+ <Input.TextInput
34
+ type='number'
35
+ disabled={disabled}
36
+ placeholder={t('placeholder latitude')}
37
+ value={lat}
38
+ onChange={(event) => onValueChange(type, [safeParseFloat(event.target.value, 0), lng])}
39
+ onBlur={onBlur}
40
+ />
41
+ </div>
42
+ </Input.Root>
43
+ );
44
+ };
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Input } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/react-ui-theme';
9
+ import { safeParseFloat } from '@dxos/util';
10
+
11
+ import { InputHeader, type InputProps } from '../Input';
12
+
13
+ const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
14
+
15
+ export const TupleInput = ({
16
+ binding,
17
+ type,
18
+ label,
19
+ disabled,
20
+ getStatus,
21
+ getValue,
22
+ onValueChange,
23
+ onBlur,
24
+ }: InputProps & { binding: string[] }) => {
25
+ const { status, error } = getStatus();
26
+
27
+ // TODO(burdon): Generalize number/float/string, etc.
28
+ const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
29
+
30
+ return (
31
+ <Input.Root validationValence={status}>
32
+ <InputHeader error={error}>
33
+ <Input.Label>{label}</Input.Label>
34
+ </InputHeader>
35
+ <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
36
+ {binding.map((prop) => (
37
+ <Input.TextInput
38
+ key={prop}
39
+ type='number'
40
+ disabled={disabled}
41
+ value={values[prop]}
42
+ onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
43
+ onBlur={onBlur}
44
+ />
45
+ ))}
46
+ </div>
47
+ </Input.Root>
48
+ );
49
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './GeoPointInput';
6
+ export * from './TupleInput';
@@ -2,23 +2,20 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type BaseObject, FormatEnum } from '@dxos/echo-schema';
5
+ import { 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.
14
14
  */
15
- export const getInputComponent = <T extends BaseObject>(
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
- return GeiPointInput;
18
+ return GeoPointInput;
22
19
  }
23
20
 
24
21
  switch (type) {
@@ -2,7 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './Form';
6
- export * from './Custom';
5
+ export * from './custom';
6
+
7
7
  export * from './Defaults';
8
+ export * from './Form';
8
9
  export * from './Input';
@@ -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, useEffect, useState } from 'react';
8
+ import React, { useCallback, useMemo, useState } from 'react';
9
9
 
10
- import { Format, type MutableSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
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 { type ViewType, ViewProjection, createView } from '@dxos/schema';
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<MutableSchema>();
23
+ const [schema, setSchema] = useState<EchoSchema>();
23
24
  const [view, setView] = useState<ViewType>();
24
25
  const [projection, setProjection] = useState<ViewProjection>();
25
- useEffect(() => {
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.addSchema(TestSchema);
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 schema={schema} view={view} registry={space?.db.schemaRegistry} onDelete={handleDelete} />
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 SchemaResolver } from '@dxos/echo-db';
8
- import { AST, Format, type MutableSchema, S } from '@dxos/echo-schema';
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: MutableSchema;
33
+ schema: EchoSchema;
34
34
  view: ViewType;
35
- registry?: SchemaResolver;
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 = name;
71
- view.query.type = typename;
72
- schema.updateTypename(typename);
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) => {
@@ -2,18 +2,15 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { 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, 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';
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<FormPath<T>, string>;
27
- touched: Record<FormPath<T>, boolean>;
28
- changed: Record<FormPath<T>, boolean>;
22
+ values: Partial<T>;
23
+ errors: Record<JsonPath, string>;
24
+ touched: Record<JsonPath, boolean>;
25
+ changed: Record<JsonPath, boolean>;
29
26
  canSave: boolean;
30
27
  handleSave: () => void;
31
28
 
@@ -33,10 +30,10 @@ export type FormHandler<T extends BaseObject> = {
33
30
  // Form input component helpers.
34
31
  //
35
32
 
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;
39
- onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
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;
40
37
  };
41
38
 
42
39
  /**
@@ -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
- // TODO(burdon): Should be partial?
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
- const [values, setValues] = useState<T>(initialValues);
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<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>);
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>>({});
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
- errors = onValidate(values) ?? [];
112
+ const validatedValues = values as T;
113
+ errors = onValidate(validatedValues) ?? [];
116
114
  }
117
115
 
118
116
  setErrors(flatMap(errors));
119
- return errors.length === 0;
117
+ const valid = errors.length === 0;
118
+ return valid;
120
119
  },
121
120
  [schema, onValidate],
122
121
  );
@@ -138,10 +137,15 @@ 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>],
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
+ ),
143
147
  ),
144
- [values, touched, errors, saving],
148
+ [touched, errors, saving],
145
149
  );
146
150
 
147
151
  const handleSave = useCallback(async () => {
@@ -153,27 +157,34 @@ export const useForm = <T extends BaseObject>({
153
157
  setSaving(false);
154
158
  }
155
159
  }
156
- }, [values, validate, onSave]);
160
+ }, [values, validate, onSave, changed]);
157
161
 
158
162
  //
159
163
  // Fields.
160
164
  //
161
165
 
162
166
  const getStatus = useCallback<FormHandler<T>['getStatus']>(
163
- (property: FormPath<T>) => ({
164
- status: errors[property] ? 'error' : undefined,
165
- error: errors[property] ? errors[property] : undefined,
166
- }),
167
- [touched, errors],
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],
168
180
  );
169
181
 
170
- // TODO(burdon): Use path to extract hierarchical value.
171
- const getValue = <V>(property: FormPath<T>): V => {
172
- return getByPath(values, property);
182
+ const getFormValue = <V>(path: (string | number)[]): V | undefined => {
183
+ return getValue(values, createJsonPath(path));
173
184
  };
174
185
 
175
- // TODO(burdon): Use path to set hierarchical value.
176
- const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
186
+ const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
187
+ const jsonPath = createJsonPath(path);
177
188
  let parsedValue = value;
178
189
  try {
179
190
  if (type === 'number') {
@@ -184,9 +195,9 @@ export const useForm = <T extends BaseObject>({
184
195
  parsedValue = undefined;
185
196
  }
186
197
 
187
- const newValues = setByPath(values, property, parsedValue);
198
+ const newValues = { ...setValue(values, jsonPath, parsedValue) };
188
199
  setValues(newValues);
189
- setChanged((prev) => ({ ...prev, [property]: true }));
200
+ setChanged((prev) => ({ ...prev, [jsonPath]: true }));
190
201
  onValuesChanged?.(newValues);
191
202
 
192
203
  const isValid = validate(newValues);
@@ -195,19 +206,10 @@ export const useForm = <T extends BaseObject>({
195
206
  }
196
207
  };
197
208
 
198
- // TODO(burdon): This is a leaky abstraction: the hook ideally shouldn't get involved in UX.
199
- const onBlur = useCallback(
200
- (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
201
- const { name } = event.target;
202
- setTouched((touched) => ({ ...touched, [name]: true }));
203
-
204
- // TODO(Zan): This should be configurable behavior.
205
- if (event.relatedTarget?.getAttribute('type') === 'submit') {
206
- // NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
207
- // and results in the submit button being disabled when the form is invalid.
208
- setTouched(createKeySet(values, true));
209
- }
210
-
209
+ const onTouched = useCallback(
210
+ (path: (string | number)[]) => {
211
+ const jsonPath = createJsonPath(path);
212
+ setTouched((touched) => ({ ...touched, [jsonPath]: true }));
211
213
  validate(values);
212
214
  },
213
215
  [validate, values],
@@ -224,40 +226,27 @@ export const useForm = <T extends BaseObject>({
224
226
 
225
227
  // Field utils.
226
228
  getStatus,
227
- getValue,
229
+ getValue: getFormValue,
228
230
  onValueChange,
229
- onBlur,
231
+ onTouched,
230
232
  } satisfies FormHandler<T>;
231
233
  };
232
234
 
233
- const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<FormPath<T>, V> => {
235
+ const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
234
236
  invariant(obj);
235
- return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<FormPath<T>, V>);
237
+ return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
236
238
  };
237
239
 
238
- const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
240
+ const flatMap = (errors: ValidationError[]) => {
239
241
  return errors.reduce(
240
242
  (result, { path, message }) => {
241
- if (!(path in result)) {
242
- result[path as FormPath<T>] = message;
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;
243
247
  }
244
248
  return result;
245
249
  },
246
- {} as Record<FormPath<T>, string>,
250
+ {} as Record<JsonPath, string>,
247
251
  );
248
252
  };
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,5 +0,0 @@
1
- import React from 'react';
2
- import { type BaseObject } from '@dxos/echo-schema';
3
- import { type InputProps } from './Input';
4
- export declare const GeiPointInput: <T extends BaseObject>({ property, type, label, disabled, getStatus, getValue, onValueChange, onBlur, }: InputProps<T>) => React.JSX.Element;
5
- //# sourceMappingURL=Custom.d.ts.map
@@ -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"}
@@ -1,65 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import React from 'react';
6
-
7
- import { type BaseObject, type GeoPoint } from '@dxos/echo-schema';
8
- import { Input, useTranslation } from '@dxos/react-ui';
9
-
10
- import { InputHeader, type InputProps } from './Input';
11
- import { translationKey } from '../../translations';
12
-
13
- //
14
- // Custom format components.
15
- //
16
-
17
- export const GeiPointInput = <T extends BaseObject>({
18
- property,
19
- type,
20
- label,
21
- disabled,
22
- getStatus,
23
- getValue,
24
- onValueChange,
25
- onBlur,
26
- }: InputProps<T>) => {
27
- const { t } = useTranslation(translationKey);
28
- const { status, error } = getStatus?.(property);
29
- const [lng = 0, lat = 0] = getValue<GeoPoint>(property) ?? [];
30
-
31
- return (
32
- <Input.Root validationValence={status}>
33
- <InputHeader error={error}>
34
- <Input.Label>{label}</Input.Label>
35
- </InputHeader>
36
- <div className='grid grid-cols-2 gap-2'>
37
- <Input.TextInput
38
- type='number'
39
- disabled={disabled}
40
- placeholder={t('placeholder longitude')}
41
- value={lng}
42
- onChange={(event) => onValueChange(property, type, [lat, safeParseFloat(event.target.value, 0)])}
43
- onBlur={onBlur}
44
- />
45
- <Input.TextInput
46
- type='number'
47
- disabled={disabled}
48
- placeholder={t('placeholder latitude')}
49
- value={lat}
50
- onChange={(event) => onValueChange(property, type, [safeParseFloat(event.target.value, 0), lng])}
51
- onBlur={onBlur}
52
- />
53
- </div>
54
- </Input.Root>
55
- );
56
- };
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
- };