@dxos/react-ui-form 0.7.2 → 0.7.3-staging.0905f03

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 (33) hide show
  1. package/dist/lib/browser/index.mjs +228 -58
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +228 -57
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +228 -58
  8. package/dist/lib/node-esm/index.mjs.map +3 -3
  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 +4 -4
  13. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.d.ts +11 -2
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +20 -5
  17. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Input.d.ts +5 -4
  19. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  20. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  21. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
  22. package/dist/types/src/hooks/useForm.d.ts +19 -11
  23. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  24. package/package.json +23 -22
  25. package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -3
  26. package/src/components/FieldEditor/FieldEditor.tsx +8 -8
  27. package/src/components/Form/Defaults.tsx +28 -13
  28. package/src/components/Form/Form.stories.tsx +81 -19
  29. package/src/components/Form/Form.tsx +216 -29
  30. package/src/components/Form/Input.tsx +5 -4
  31. package/src/components/ViewEditor/ViewEditor.tsx +8 -7
  32. package/src/components/deprecated/Form.stories.tsx +4 -4
  33. package/src/hooks/useForm.ts +65 -32
@@ -9,6 +9,10 @@ import { type SimpleType, type S } 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
+ import { type MaybePromise } from '@dxos/util';
13
+
14
+ // TODO(ZaymonFC): Where should this live?
15
+ export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
12
16
 
13
17
  /**
14
18
  * Return type from `useForm` hook.
@@ -19,19 +23,19 @@ export type FormHandler<T extends BaseObject> = {
19
23
  //
20
24
 
21
25
  values: T;
22
- errors: Record<PropertyKey<T>, string>;
23
- touched: Record<PropertyKey<T>, boolean>;
24
- changed: Record<PropertyKey<T>, boolean>;
25
- canSubmit: boolean;
26
- handleSubmit: () => void;
26
+ errors: Record<FormPath<T>, string>;
27
+ touched: Record<FormPath<T>, boolean>;
28
+ changed: Record<FormPath<T>, boolean>;
29
+ canSave: boolean;
30
+ handleSave: () => void;
27
31
 
28
32
  //
29
33
  // Form input component helpers.
30
34
  //
31
35
 
32
- getStatus: (property: PropertyKey<T>) => { status?: 'error'; error?: string };
33
- getValue: <V>(property: PropertyKey<T>) => V;
34
- onValueChange: <V>(property: PropertyKey<T>, type: SimpleType, value: V) => void;
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;
35
39
  onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
36
40
  };
37
41
 
@@ -39,9 +43,16 @@ export type FormHandler<T extends BaseObject> = {
39
43
  * Hook options.
40
44
  */
41
45
  export interface FormOptions<T extends BaseObject> {
46
+ /**
47
+ * Effect schema.
48
+ */
49
+ // TODO(burdon): Change to S.Struct<T>?
42
50
  schema: S.Schema<T>;
43
51
 
44
- // TODO(burdon): Are these reactive?
52
+ /**
53
+ * Initial values (which may not pass validation).
54
+ */
55
+ // TODO(burdon): Should be partial?
45
56
  initialValues: T;
46
57
 
47
58
  /**
@@ -56,7 +67,7 @@ export interface FormOptions<T extends BaseObject> {
56
67
  * Use this for complex validation logic that can't be expressed in the schema.
57
68
  * @returns Array of validation errors, or undefined if validation passes
58
69
  */
59
- // TODO(burdon): Change to key x value.
70
+ // TODO(burdon): Change to key x value?
60
71
  onValidate?: (values: T) => ValidationError[] | undefined;
61
72
 
62
73
  /**
@@ -67,7 +78,7 @@ export interface FormOptions<T extends BaseObject> {
67
78
  /**
68
79
  * Called when the form is submitted and passes validation.
69
80
  */
70
- onSubmit?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
81
+ onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
71
82
  }
72
83
 
73
84
  /**
@@ -80,16 +91,17 @@ export const useForm = <T extends BaseObject>({
80
91
  onValuesChanged,
81
92
  onValidate,
82
93
  onValid,
83
- onSubmit,
94
+ onSave,
84
95
  }: FormOptions<T>): FormHandler<T> => {
85
96
  const [values, setValues] = useState<T>(initialValues);
86
97
  useEffect(() => {
87
98
  setValues(initialValues);
88
99
  }, [initialValues]);
89
100
 
90
- const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
91
- const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
92
- const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
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>);
104
+ const [saving, setSaving] = useState(false);
93
105
 
94
106
  //
95
107
  // Validation.
@@ -123,26 +135,32 @@ export const useForm = <T extends BaseObject>({
123
135
  * NOTE: We can submit if there is no touched field that has an error.
124
136
  * Basically, if there's a validation message visible in the form, submit should be disabled.
125
137
  */
126
- const canSubmit = useMemo(
138
+ const canSave = useMemo(
127
139
  () =>
140
+ !saving &&
128
141
  Object.keys(values).every(
129
142
  (property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
130
143
  ),
131
- [values, touched, errors],
144
+ [values, touched, errors, saving],
132
145
  );
133
146
 
134
- const handleSubmit = useCallback(() => {
147
+ const handleSave = useCallback(async () => {
135
148
  if (validate(values)) {
136
- onSubmit?.(values, { changed });
149
+ setSaving(true);
150
+ try {
151
+ await onSave?.(values, { changed });
152
+ } finally {
153
+ setSaving(false);
154
+ }
137
155
  }
138
- }, [values, validate, onSubmit]);
156
+ }, [values, validate, onSave]);
139
157
 
140
158
  //
141
159
  // Fields.
142
160
  //
143
161
 
144
162
  const getStatus = useCallback<FormHandler<T>['getStatus']>(
145
- (property: PropertyKey<T>) => ({
163
+ (property: FormPath<T>) => ({
146
164
  status: errors[property] ? 'error' : undefined,
147
165
  error: errors[property] ? errors[property] : undefined,
148
166
  }),
@@ -150,12 +168,12 @@ export const useForm = <T extends BaseObject>({
150
168
  );
151
169
 
152
170
  // TODO(burdon): Use path to extract hierarchical value.
153
- const getValue = <V>(property: PropertyKey<T>): V => {
154
- return values[property] as V;
171
+ const getValue = <V>(property: FormPath<T>): V => {
172
+ return getByPath(values, property);
155
173
  };
156
174
 
157
175
  // TODO(burdon): Use path to set hierarchical value.
158
- const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
176
+ const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
159
177
  let parsedValue = value;
160
178
  try {
161
179
  if (type === 'number') {
@@ -166,7 +184,7 @@ export const useForm = <T extends BaseObject>({
166
184
  parsedValue = undefined;
167
185
  }
168
186
 
169
- const newValues = { ...values, [property]: parsedValue };
187
+ const newValues = setByPath(values, property, parsedValue);
170
188
  setValues(newValues);
171
189
  setChanged((prev) => ({ ...prev, [property]: true }));
172
190
  onValuesChanged?.(newValues);
@@ -185,7 +203,7 @@ export const useForm = <T extends BaseObject>({
185
203
 
186
204
  // TODO(Zan): This should be configurable behavior.
187
205
  if (event.relatedTarget?.getAttribute('type') === 'submit') {
188
- // NOTE: We do this here instead of onSubmit because the blur event is triggered before the submit event
206
+ // NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
189
207
  // and results in the submit button being disabled when the form is invalid.
190
208
  setTouched(createKeySet(values, true));
191
209
  }
@@ -201,8 +219,8 @@ export const useForm = <T extends BaseObject>({
201
219
  errors,
202
220
  touched,
203
221
  changed,
204
- canSubmit,
205
- handleSubmit,
222
+ canSave,
223
+ handleSave,
206
224
 
207
225
  // Field utils.
208
226
  getStatus,
@@ -212,19 +230,34 @@ export const useForm = <T extends BaseObject>({
212
230
  } satisfies FormHandler<T>;
213
231
  };
214
232
 
215
- const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
233
+ const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<FormPath<T>, V> => {
216
234
  invariant(obj);
217
- return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
235
+ return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<FormPath<T>, V>);
218
236
  };
219
237
 
220
238
  const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
221
239
  return errors.reduce(
222
240
  (result, { path, message }) => {
223
241
  if (!(path in result)) {
224
- result[path as PropertyKey<T>] = message;
242
+ result[path as FormPath<T>] = message;
225
243
  }
226
244
  return result;
227
245
  },
228
- {} as Record<PropertyKey<T>, string>,
246
+ {} as Record<FormPath<T>, string>,
229
247
  );
230
248
  };
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
+ };