@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.
Files changed (49) hide show
  1. package/dist/lib/browser/index.mjs +292 -221
  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 +380 -308
  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 +292 -221
  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/Defaults.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.d.ts +10 -4
  14. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.stories.d.ts +5 -0
  16. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Input.d.ts +3 -3
  18. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/{Custom.d.ts → custom/GeoPointInput.d.ts} +3 -3
  20. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/custom/TupleInput.d.ts +7 -0
  22. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/custom/index.d.ts +3 -0
  24. package/dist/types/src/components/Form/custom/index.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/index.d.ts +2 -2
  27. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  28. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -5
  29. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  30. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useForm.d.ts +10 -11
  32. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  33. package/dist/types/tsconfig.tsbuildinfo +1 -0
  34. package/package.json +26 -26
  35. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  36. package/src/components/FieldEditor/FieldEditor.tsx +75 -50
  37. package/src/components/Form/Defaults.tsx +2 -0
  38. package/src/components/Form/Form.stories.tsx +33 -1
  39. package/src/components/Form/Form.tsx +33 -5
  40. package/src/components/Form/Input.tsx +3 -3
  41. package/src/components/Form/{Custom.tsx → custom/GeoPointInput.tsx} +4 -16
  42. package/src/components/Form/custom/TupleInput.tsx +53 -0
  43. package/src/components/Form/custom/index.ts +6 -0
  44. package/src/components/Form/factory.tsx +2 -2
  45. package/src/components/Form/index.ts +3 -2
  46. package/src/components/ViewEditor/ViewEditor.stories.tsx +30 -8
  47. package/src/components/ViewEditor/ViewEditor.tsx +13 -8
  48. package/src/hooks/useForm.ts +35 -54
  49. 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
- // TODO(burdon): Partial<T>?
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.toString()]}
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}.${index}`}
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 FormPath, type FormHandler } from '../../hooks';
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: FormPath<T>;
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 { InputHeader, type InputProps } from './Input';
11
- import { translationKey } from '../../translations';
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
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './GeoPointInput';
6
+ export * from './TupleInput';
@@ -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 GeiPointInput;
21
+ return GeoPointInput;
22
22
  }
23
23
 
24
24
  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) => {
@@ -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<FormPath<T>, string>;
27
- touched: Record<FormPath<T>, boolean>;
28
- changed: Record<FormPath<T>, boolean>;
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: FormPath<T>) => { status?: 'error'; error?: string };
37
- getValue: <V>(property: FormPath<T>) => V;
38
- onValueChange: <V>(property: FormPath<T>, type: SimpleType, value: V) => void;
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
- // 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<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
- 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,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: FormPath<T>) => ({
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 getValue = <V>(property: FormPath<T>): V => {
172
- return getByPath(values, property);
168
+ const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
169
+ return getValue(values, property as any as JsonPath);
173
170
  };
174
171
 
175
- // TODO(burdon): Use path to set hierarchical value.
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 = setByPath(values, property, parsedValue);
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<FormPath<T>, V> => {
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<FormPath<T>, V>);
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 FormPath<T>] = message;
238
+ result[path as PropertyKey<T>] = message;
243
239
  }
244
240
  return result;
245
241
  },
246
- {} as Record<FormPath<T>, string>,
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"}