@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.
Files changed (47) hide show
  1. package/dist/lib/browser/index.mjs +416 -409
  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 +461 -469
  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 +416 -409
  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/Form/ArrayField.d.ts +16 -0
  11. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
  12. package/dist/types/src/components/Form/Defaults.d.ts +5 -6
  13. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.d.ts +10 -20
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +2 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormActions.d.ts +7 -0
  19. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormContent.d.ts +26 -0
  21. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormContext.d.ts +20 -0
  23. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/Input.d.ts +5 -6
  25. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -2
  27. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -2
  29. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/factory.d.ts +2 -2
  31. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  32. package/dist/types/src/hooks/useForm.d.ts +9 -10
  33. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  34. package/package.json +23 -23
  35. package/src/components/FieldEditor/FieldEditor.tsx +4 -4
  36. package/src/components/Form/ArrayField.tsx +111 -0
  37. package/src/components/Form/Defaults.tsx +18 -33
  38. package/src/components/Form/Form.stories.tsx +15 -5
  39. package/src/components/Form/Form.tsx +37 -359
  40. package/src/components/Form/FormActions.tsx +41 -0
  41. package/src/components/Form/FormContent.tsx +155 -0
  42. package/src/components/Form/FormContext.tsx +48 -0
  43. package/src/components/Form/Input.tsx +5 -6
  44. package/src/components/Form/custom/GeoPointInput.tsx +6 -15
  45. package/src/components/Form/custom/TupleInput.tsx +5 -9
  46. package/src/components/Form/factory.tsx +2 -5
  47. 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 BaseObject, type FormatEnum, type PropertyKey } from '@dxos/echo-schema';
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 FormHandler } from '../../hooks';
12
+ import { type FormInputStateProps } from './FormContext';
13
13
 
14
14
  /**
15
15
  * Props passed to input components.
16
16
  */
17
- export type InputProps<T extends BaseObject> = {
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
- } & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
24
+ } & FormInputStateProps;
26
25
 
27
26
  /**
28
27
  * Form input component.
29
28
  */
30
- export type InputComponent<T extends BaseObject> = FC<InputProps<T>>;
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 BaseObject, type GeoPoint } from '@dxos/echo-schema';
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 = <T extends BaseObject>({
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?.(property);
26
- const [lng = 0, lat = 0] = getValue<GeoPoint>(property) ?? [];
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(property, type, [lat, safeParseFloat(event.target.value, 0)])}
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(property, type, [safeParseFloat(event.target.value, 0), lng])}
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 = <T extends BaseObject>({
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<T> & { binding: string[] }) => {
27
- const { status, error } = getStatus?.(property);
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>>(property) ?? {};
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 { 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
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 = <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
18
  return GeoPointInput;
@@ -2,10 +2,10 @@
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, getValue, setValue } from '@dxos/echo-schema';
8
- import { type SimpleType, type S, type JsonPath, AST } 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';
@@ -20,9 +20,9 @@ export type FormHandler<T extends BaseObject> = {
20
20
  //
21
21
 
22
22
  values: Partial<T>;
23
- errors: Record<PropertyKey<T>, string>;
24
- touched: Record<PropertyKey<T>, boolean>;
25
- changed: Record<PropertyKey<T>, boolean>;
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: (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;
36
- 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;
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<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>);
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
- Object.keys(values).every((property) => touched[property as JsonPath] === false || !errors[property as JsonPath]),
141
- [values, touched, errors, saving],
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
- (property: PropertyKey<T>) => ({
161
- status: errors[property] ? 'error' : undefined,
162
- error: errors[property] ? errors[property] : undefined,
163
- }),
164
- [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],
165
180
  );
166
181
 
167
- // TODO(burdon): Use path to extract hierarchical value.
168
- const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
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 = (property: PropertyKey<T>, type: SimpleType, value: any) => {
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, property as any as JsonPath, parsedValue) };
198
+ const newValues = { ...setValue(values, jsonPath, parsedValue) };
184
199
  setValues(newValues);
185
- setChanged((prev) => ({ ...prev, [property]: true }));
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
- // TODO(burdon): This is a leaky abstraction: the hook ideally shouldn't get involved in UX.
195
- const onBlur = useCallback(
196
- (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
197
- const { name } = event.target;
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
- onBlur,
231
+ onTouched,
226
232
  } satisfies FormHandler<T>;
227
233
  };
228
234
 
229
- const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
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<PropertyKey<T>, V>);
237
+ return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
232
238
  };
233
239
 
234
- const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
240
+ const flatMap = (errors: ValidationError[]) => {
235
241
  return errors.reduce(
236
242
  (result, { path, message }) => {
237
- if (!(path in result)) {
238
- result[path as PropertyKey<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;
239
247
  }
240
248
  return result;
241
249
  },
242
- {} as Record<PropertyKey<T>, string>,
250
+ {} as Record<JsonPath, string>,
243
251
  );
244
252
  };