@conform-to/react 0.6.0 → 0.6.1

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/README.md CHANGED
@@ -42,12 +42,20 @@ function LoginForm() {
42
42
  id: undefined,
43
43
 
44
44
  /**
45
- * Define when the error should be reported initially.
45
+ * Define when conform should start validation.
46
46
  * Support "onSubmit", "onChange", "onBlur".
47
47
  *
48
48
  * Default to `onSubmit`.
49
49
  */
50
- initialReport: 'onBlur',
50
+ shouldValidate: 'onSubmit',
51
+
52
+ /**
53
+ * Define when conform should revalidate again.
54
+ * Support "onSubmit", "onChange", "onBlur".
55
+ *
56
+ * Default to `onInput`.
57
+ */
58
+ shouldRevalidate: 'onInput',
51
59
 
52
60
  /**
53
61
  * An object representing the initial value of the form.
package/helpers.d.ts CHANGED
@@ -34,23 +34,21 @@ interface TextareaProps extends FormControlProps {
34
34
  maxLength?: number;
35
35
  defaultValue?: string;
36
36
  }
37
- type InputOptions = {
38
- type: 'checkbox' | 'radio';
37
+ type BaseOptions = {
38
+ description?: boolean;
39
39
  hidden?: boolean;
40
+ };
41
+ type InputOptions = BaseOptions & ({
42
+ type: 'checkbox' | 'radio';
40
43
  value?: string;
41
44
  } | {
42
45
  type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden'>;
43
- hidden?: boolean;
44
46
  value?: never;
45
- };
46
- export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: {
47
+ });
48
+ export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: InputOptions & {
47
49
  type: 'file';
48
50
  }): InputProps<Schema>;
49
51
  export declare function input<Schema extends Primitive>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>;
50
- export declare function select(config: FieldConfig<Primitive | Primitive[]>, options?: {
51
- hidden?: boolean;
52
- }): SelectProps;
53
- export declare function textarea(config: FieldConfig<Primitive>, options?: {
54
- hidden?: boolean;
55
- }): TextareaProps;
52
+ export declare function select(config: FieldConfig<Primitive | Primitive[]>, options?: BaseOptions): SelectProps;
53
+ export declare function textarea(config: FieldConfig<Primitive>, options?: BaseOptions): TextareaProps;
56
54
  export { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED };
package/helpers.js CHANGED
@@ -30,10 +30,13 @@ function getFormControlProps(config, options) {
30
30
  };
31
31
  if (config.id) {
32
32
  props.id = config.id;
33
- props['aria-describedby'] = config.errorId;
33
+ }
34
+ if (config.descriptionId && options !== null && options !== void 0 && options.description) {
35
+ props['aria-describedby'] = config.descriptionId;
34
36
  }
35
37
  if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) {
36
38
  props['aria-invalid'] = true;
39
+ props['aria-describedby'] = config.descriptionId && options !== null && options !== void 0 && options.description ? "".concat(config.errorId, " ").concat(config.descriptionId) : config.errorId;
37
40
  }
38
41
  if (config.initialError && Object.entries(config.initialError).length > 0) {
39
42
  props.autoFocus = true;
package/hooks.d.ts CHANGED
@@ -7,12 +7,27 @@ export interface FormConfig<Schema extends Record<string, any>, ClientSubmission
7
7
  */
8
8
  id?: string;
9
9
  /**
10
- * Define when the error should be reported initially.
10
+ * A form ref object. Conform will fallback to its own ref object if it is not provided.
11
+ */
12
+ ref?: RefObject<HTMLFormElement>;
13
+ /**
14
+ * @deprecated Use `shouldValidate` and `shouldRevalidate` instead.
15
+ */
16
+ initialReport?: 'onSubmit' | 'onChange' | 'onBlur';
17
+ /**
18
+ * Define when conform should start validation.
11
19
  * Support "onSubmit", "onChange", "onBlur".
12
20
  *
13
21
  * Default to `onSubmit`.
14
22
  */
15
- initialReport?: 'onSubmit' | 'onChange' | 'onBlur';
23
+ shouldValidate?: 'onSubmit' | 'onBlur' | 'onInput';
24
+ /**
25
+ * Define when conform should revalidate again.
26
+ * Support "onSubmit", "onChange", "onBlur".
27
+ *
28
+ * Default to `onInput`.
29
+ */
30
+ shouldRevalidate?: 'onSubmit' | 'onBlur' | 'onInput';
16
31
  /**
17
32
  * An object representing the initial value of the form.
18
33
  */
package/hooks.js CHANGED
@@ -13,10 +13,10 @@ var react = require('react');
13
13
  * @see https://conform.guide/api/react#useform
14
14
  */
15
15
  function useForm() {
16
- var _config$lastSubmissio;
16
+ var _config$lastSubmissio, _config$ref, _ref2, _config$lastSubmissio2;
17
17
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
18
18
  var configRef = react.useRef(config);
19
- var ref = react.useRef(null);
19
+ var formRef = react.useRef(null);
20
20
  var [lastSubmission, setLastSubmission] = react.useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null);
21
21
  var [errors, setErrors] = react.useState(() => {
22
22
  if (!config.lastSubmission) {
@@ -24,32 +24,29 @@ function useForm() {
24
24
  }
25
25
  return [].concat(config.lastSubmission.error['']);
26
26
  });
27
- var [uncontrolledState, setUncontrolledState] = react.useState(() => {
27
+ var initialError = react.useMemo(() => {
28
28
  var submission = config.lastSubmission;
29
29
  if (!submission) {
30
- return {
31
- defaultValue: config.defaultValue
32
- };
30
+ return {};
33
31
  }
34
32
  var scope = dom.getScope(submission.intent);
35
- return {
36
- defaultValue: submission.payload,
37
- initialError: Object.entries(submission.error).reduce((result, _ref) => {
38
- var [name, message] = _ref;
39
- if (name !== '' && (scope === null || scope === name)) {
40
- result[name] = message;
41
- }
42
- return result;
43
- }, {})
44
- };
45
- });
46
- var fieldsetConfig = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, uncontrolledState), {}, {
33
+ return Object.entries(submission.error).reduce((result, _ref) => {
34
+ var [name, message] = _ref;
35
+ if (name !== '' && (scope === null || scope === name)) {
36
+ result[name] = message;
37
+ }
38
+ return result;
39
+ }, {});
40
+ }, [config.lastSubmission]);
41
+ var ref = (_config$ref = config.ref) !== null && _config$ref !== void 0 ? _config$ref : formRef;
42
+ var fieldset = useFieldset(ref, {
43
+ defaultValue: (_ref2 = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref2 !== void 0 ? _ref2 : config.defaultValue,
44
+ initialError,
47
45
  constraint: config.constraint,
48
46
  form: config.id
49
47
  });
50
- var fieldset = useFieldset(ref, fieldsetConfig);
51
48
  var [noValidate, setNoValidate] = react.useState(config.noValidate || !config.fallbackNative);
52
- react.useEffect(() => {
49
+ useSafeLayoutEffect(() => {
53
50
  configRef.current = config;
54
51
  });
55
52
  react.useEffect(() => {
@@ -68,24 +65,29 @@ function useForm() {
68
65
  }));
69
66
  }
70
67
  setLastSubmission(submission);
71
- }, [config.lastSubmission]);
68
+ }, [ref, config.lastSubmission]);
72
69
  react.useEffect(() => {
73
70
  var form = ref.current;
74
71
  if (!form || !lastSubmission) {
75
72
  return;
76
73
  }
77
- dom.reportSubmission(ref.current, lastSubmission);
78
- }, [lastSubmission]);
74
+ dom.reportSubmission(form, lastSubmission);
75
+ }, [ref, lastSubmission]);
79
76
  react.useEffect(() => {
80
77
  // Revalidate the form when input value is changed
81
78
  var handleInput = event => {
82
79
  var field = event.target;
83
80
  var form = ref.current;
84
81
  var formConfig = configRef.current;
82
+ var {
83
+ initialReport = 'onSubmit',
84
+ shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport,
85
+ shouldRevalidate = 'onInput'
86
+ } = formConfig;
85
87
  if (!form || !dom.isFieldElement(field) || field.form !== form) {
86
88
  return;
87
89
  }
88
- if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') {
90
+ if (field.dataset.conformTouched ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') {
89
91
  dom.requestIntent(form, dom.validate(field.name));
90
92
  }
91
93
  };
@@ -93,15 +95,20 @@ function useForm() {
93
95
  var field = event.target;
94
96
  var form = ref.current;
95
97
  var formConfig = configRef.current;
98
+ var {
99
+ initialReport = 'onSubmit',
100
+ shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport,
101
+ shouldRevalidate = 'onInput'
102
+ } = formConfig;
96
103
  if (!form || !dom.isFieldElement(field) || field.form !== form) {
97
104
  return;
98
105
  }
99
- if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) {
106
+ if (field.dataset.conformTouched ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') {
100
107
  dom.requestIntent(form, dom.validate(field.name));
101
108
  }
102
109
  };
103
110
  var handleInvalid = event => {
104
- var form = dom.getFormElement(ref.current);
111
+ var form = ref.current;
105
112
  var field = event.target;
106
113
  if (!form || !dom.isFieldElement(field) || field.form !== form || field.name !== dom.FORM_ERROR_ELEMENT_NAME) {
107
114
  return;
@@ -113,7 +120,6 @@ function useForm() {
113
120
  };
114
121
  var handleReset = event => {
115
122
  var form = ref.current;
116
- var formConfig = configRef.current;
117
123
  if (!form || event.target !== form) {
118
124
  return;
119
125
  }
@@ -126,17 +132,7 @@ function useForm() {
126
132
  }
127
133
  }
128
134
  setErrors([]);
129
- setUncontrolledState({
130
- defaultValue: formConfig.defaultValue
131
- });
132
135
  };
133
-
134
- /**
135
- * The input event handler will be triggered in capturing phase in order to
136
- * allow follow-up action in the bubble phase based on the latest validity
137
- * E.g. `useFieldset` reset the error of valid field after checking the
138
- * validity in the bubble phase.
139
- */
140
136
  document.addEventListener('input', handleInput, true);
141
137
  document.addEventListener('blur', handleBlur, true);
142
138
  document.addEventListener('invalid', handleInvalid, true);
@@ -147,7 +143,7 @@ function useForm() {
147
143
  document.removeEventListener('invalid', handleInvalid, true);
148
144
  document.removeEventListener('reset', handleReset);
149
145
  };
150
- }, []);
146
+ }, [ref]);
151
147
  var form = {
152
148
  ref,
153
149
  error: errors[0],
@@ -170,11 +166,11 @@ function useForm() {
170
166
  form,
171
167
  formData
172
168
  });
173
- if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref2 => {
174
- var [, message] = _ref2;
175
- return message !== '' && ![].concat(message).includes(dom.VALIDATION_UNDEFINED);
176
- }) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref3 => {
169
+ if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref3 => {
177
170
  var [, message] = _ref3;
171
+ return message !== '' && ![].concat(message).includes(dom.VALIDATION_UNDEFINED);
172
+ }) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref4 => {
173
+ var [, message] = _ref4;
178
174
  return ![].concat(message).includes(dom.VALIDATION_UNDEFINED);
179
175
  })) {
180
176
  var listCommand = dom.parseListCommand(submission.intent);
@@ -202,10 +198,10 @@ function useForm() {
202
198
  form.id = config.id;
203
199
  form.errorId = "".concat(config.id, "-error");
204
200
  form.props.id = form.id;
205
- form.props['aria-describedby'] = form.errorId;
206
201
  }
207
202
  if (form.errorId && form.errors.length > 0) {
208
203
  form.props['aria-invalid'] = 'true';
204
+ form.props['aria-describedby'] = form.errorId;
209
205
  }
210
206
  return [form, fieldset];
211
207
  }
@@ -216,35 +212,21 @@ function useForm() {
216
212
 
217
213
  function useFieldset(ref, config) {
218
214
  var configRef = react.useRef(config);
219
- var [uncontrolledState, setUncontrolledState] = react.useState(
220
- // @ts-expect-error
221
- () => {
222
- var _config$defaultValue;
223
- var initialError = {};
224
- for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
225
- var _config$initialError;
226
- var [key, ...paths] = dom.getPaths(name);
227
- if (typeof key === 'string') {
228
- var scopedName = dom.getName(paths);
229
- initialError[key] = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, initialError[key]), {}, {
230
- [scopedName]: message
231
- });
232
- }
233
- }
234
- return {
235
- defaultValue: (_config$defaultValue = config === null || config === void 0 ? void 0 : config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : {},
236
- initialError
237
- };
238
- });
239
215
  var [error, setError] = react.useState(() => {
216
+ var initialError = config === null || config === void 0 ? void 0 : config.initialError;
217
+ if (!initialError) {
218
+ return {};
219
+ }
240
220
  var result = {};
241
- for (var [key, _error] of Object.entries(uncontrolledState.initialError)) {
242
- var _error$;
243
- result[key] = [].concat((_error$ = _error === null || _error === void 0 ? void 0 : _error['']) !== null && _error$ !== void 0 ? _error$ : []);
221
+ for (var [name, message] of Object.entries(initialError)) {
222
+ var [key, ...paths] = dom.getPaths(name);
223
+ if (typeof key === 'string' && paths.length === 0) {
224
+ result[key] = [].concat(message !== null && message !== void 0 ? message : []);
225
+ }
244
226
  }
245
227
  return result;
246
228
  });
247
- react.useEffect(() => {
229
+ useSafeLayoutEffect(() => {
248
230
  configRef.current = config;
249
231
  });
250
232
  react.useEffect(() => {
@@ -275,17 +257,10 @@ function useFieldset(ref, config) {
275
257
  }
276
258
  };
277
259
  var resetHandler = event => {
278
- var _fieldsetConfig$defau;
279
260
  var form = dom.getFormElement(ref.current);
280
261
  if (!form || event.target !== form) {
281
262
  return;
282
263
  }
283
- var fieldsetConfig = configRef.current;
284
- setUncontrolledState({
285
- // @ts-expect-error
286
- defaultValue: (_fieldsetConfig$defau = fieldsetConfig === null || fieldsetConfig === void 0 ? void 0 : fieldsetConfig.defaultValue) !== null && _fieldsetConfig$defau !== void 0 ? _fieldsetConfig$defau : {},
287
- initialError: {}
288
- });
289
264
  setError({});
290
265
  };
291
266
 
@@ -305,17 +280,25 @@ function useFieldset(ref, config) {
305
280
  */
306
281
  return new Proxy({}, {
307
282
  get(_target, key) {
308
- var _fieldsetConfig$const;
283
+ var _fieldsetConfig$const, _fieldsetConfig$initi, _fieldsetConfig$defau;
309
284
  if (typeof key !== 'string') {
310
285
  return;
311
286
  }
312
- var fieldsetConfig = config !== null && config !== void 0 ? config : {};
287
+ var fieldsetConfig = config;
313
288
  var constraint = (_fieldsetConfig$const = fieldsetConfig.constraint) === null || _fieldsetConfig$const === void 0 ? void 0 : _fieldsetConfig$const[key];
314
289
  var errors = error === null || error === void 0 ? void 0 : error[key];
290
+ var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref5) => {
291
+ var [name, message] = _ref5;
292
+ var [field, ...paths] = dom.getPaths(name);
293
+ if (field === key) {
294
+ result[dom.getName(paths)] = message;
295
+ }
296
+ return result;
297
+ }, {});
315
298
  var field = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, constraint), {}, {
316
299
  name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key,
317
- defaultValue: uncontrolledState.defaultValue[key],
318
- initialError: uncontrolledState.initialError[key],
300
+ defaultValue: (_fieldsetConfig$defau = fieldsetConfig.defaultValue) === null || _fieldsetConfig$defau === void 0 ? void 0 : _fieldsetConfig$defau[key],
301
+ initialError,
319
302
  error: errors === null || errors === void 0 ? void 0 : errors[0],
320
303
  errors
321
304
  });
@@ -323,6 +306,7 @@ function useFieldset(ref, config) {
323
306
  field.form = fieldsetConfig.form;
324
307
  field.id = "".concat(fieldsetConfig.form, "-").concat(field.name);
325
308
  field.errorId = "".concat(field.id, "-error");
309
+ field.descriptionId = "".concat(field.id, "-description");
326
310
  }
327
311
  return field;
328
312
  }
@@ -337,33 +321,22 @@ function useFieldset(ref, config) {
337
321
  */
338
322
  function useFieldList(ref, config) {
339
323
  var configRef = react.useRef(config);
340
- var [uncontrolledState, setUncontrolledState] = react.useState(() => {
341
- var _config$defaultValue2;
324
+ var [error, setError] = react.useState(() => {
342
325
  var initialError = [];
343
- for (var [name, message] of Object.entries((_config$initialError2 = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {})) {
344
- var _config$initialError2;
326
+ for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
327
+ var _config$initialError;
345
328
  var [index, ...paths] = dom.getPaths(name);
346
- if (typeof index === 'number') {
347
- var scopedName = dom.getName(paths);
348
- initialError[index] = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, initialError[index]), {}, {
349
- [scopedName]: message
350
- });
329
+ if (typeof index === 'number' && paths.length === 0) {
330
+ initialError[index] = [].concat(message !== null && message !== void 0 ? message : []);
351
331
  }
352
332
  }
353
- return {
354
- defaultValue: (_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [],
355
- initialError
356
- };
333
+ return initialError;
357
334
  });
358
- var [error, setError] = react.useState(() => uncontrolledState.initialError.map(error => {
359
- var _error$2;
360
- return [].concat((_error$2 = error === null || error === void 0 ? void 0 : error['']) !== null && _error$2 !== void 0 ? _error$2 : []);
361
- }));
362
335
  var [entries, setEntries] = react.useState(() => {
363
- var _config$defaultValue3;
364
- return Object.entries((_config$defaultValue3 = config.defaultValue) !== null && _config$defaultValue3 !== void 0 ? _config$defaultValue3 : [undefined]);
336
+ var _config$defaultValue;
337
+ return Object.entries((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : [undefined]);
365
338
  });
366
- react.useEffect(() => {
339
+ useSafeLayoutEffect(() => {
367
340
  configRef.current = config;
368
341
  });
369
342
  react.useEffect(() => {
@@ -437,17 +410,12 @@ function useFieldList(ref, config) {
437
410
  });
438
411
  };
439
412
  var resetHandler = event => {
440
- var _fieldConfig$defaultV, _fieldConfig$defaultV2;
413
+ var _configRef$current$de;
441
414
  var form = dom.getFormElement(ref.current);
442
415
  if (!form || event.target !== form) {
443
416
  return;
444
417
  }
445
- var fieldConfig = configRef.current;
446
- setUncontrolledState({
447
- defaultValue: (_fieldConfig$defaultV = fieldConfig.defaultValue) !== null && _fieldConfig$defaultV !== void 0 ? _fieldConfig$defaultV : [],
448
- initialError: []
449
- });
450
- setEntries(Object.entries((_fieldConfig$defaultV2 = fieldConfig.defaultValue) !== null && _fieldConfig$defaultV2 !== void 0 ? _fieldConfig$defaultV2 : [undefined]));
418
+ setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [undefined]));
451
419
  setError([]);
452
420
  };
453
421
 
@@ -462,13 +430,22 @@ function useFieldList(ref, config) {
462
430
  document.removeEventListener('reset', resetHandler);
463
431
  };
464
432
  }, [ref]);
465
- return entries.map((_ref4, index) => {
466
- var [key, defaultValue] = _ref4;
433
+ return entries.map((_ref6, index) => {
434
+ var _config$initialError2, _config$defaultValue2;
435
+ var [key, defaultValue] = _ref6;
467
436
  var errors = error[index];
437
+ var initialError = Object.entries((_config$initialError2 = config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {}).reduce((result, _ref7) => {
438
+ var [name, message] = _ref7;
439
+ var [field, ...paths] = dom.getPaths(name);
440
+ if (field === index) {
441
+ result[dom.getName(paths)] = message;
442
+ }
443
+ return result;
444
+ }, {});
468
445
  var fieldConfig = {
469
446
  name: "".concat(config.name, "[").concat(index, "]"),
470
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index],
471
- initialError: uncontrolledState.initialError[index],
447
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue2 = config.defaultValue) === null || _config$defaultValue2 === void 0 ? void 0 : _config$defaultValue2[index],
448
+ initialError,
472
449
  error: errors === null || errors === void 0 ? void 0 : errors[0],
473
450
  errors
474
451
  };
@@ -476,6 +453,7 @@ function useFieldList(ref, config) {
476
453
  fieldConfig.form = config.form;
477
454
  fieldConfig.id = "".concat(config.form, "-").concat(config.name);
478
455
  fieldConfig.errorId = "".concat(fieldConfig.id, "-error");
456
+ fieldConfig.descriptionId = "".concat(fieldConfig.id, "-description");
479
457
  }
480
458
  return _rollupPluginBabelHelpers.objectSpread2({
481
459
  key
package/module/helpers.js CHANGED
@@ -26,10 +26,13 @@ function getFormControlProps(config, options) {
26
26
  };
27
27
  if (config.id) {
28
28
  props.id = config.id;
29
- props['aria-describedby'] = config.errorId;
29
+ }
30
+ if (config.descriptionId && options !== null && options !== void 0 && options.description) {
31
+ props['aria-describedby'] = config.descriptionId;
30
32
  }
31
33
  if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) {
32
34
  props['aria-invalid'] = true;
35
+ props['aria-describedby'] = config.descriptionId && options !== null && options !== void 0 && options.description ? "".concat(config.errorId, " ").concat(config.descriptionId) : config.errorId;
33
36
  }
34
37
  if (config.initialError && Object.entries(config.initialError).length > 0) {
35
38
  props.autoFocus = true;
package/module/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
- import { getScope, parseListCommand, reportSubmission, getFormData, parse, VALIDATION_UNDEFINED, getFormAttributes, getPaths, getName, isFieldElement, requestIntent, validate, getFormElement, FORM_ERROR_ELEMENT_NAME, getErrors, getValidationMessage, updateList } from '@conform-to/dom';
3
- import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react';
2
+ import { getScope, parseListCommand, reportSubmission, getFormData, parse, VALIDATION_UNDEFINED, getFormAttributes, getPaths, getName, isFieldElement, requestIntent, validate, FORM_ERROR_ELEMENT_NAME, getErrors, getFormElement, getValidationMessage, updateList } from '@conform-to/dom';
3
+ import { useRef, useState, useMemo, useEffect, useLayoutEffect } from 'react';
4
4
 
5
5
  /**
6
6
  * Returns properties required to hook into form events.
@@ -9,10 +9,10 @@ import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react';
9
9
  * @see https://conform.guide/api/react#useform
10
10
  */
11
11
  function useForm() {
12
- var _config$lastSubmissio;
12
+ var _config$lastSubmissio, _config$ref, _ref2, _config$lastSubmissio2;
13
13
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
14
14
  var configRef = useRef(config);
15
- var ref = useRef(null);
15
+ var formRef = useRef(null);
16
16
  var [lastSubmission, setLastSubmission] = useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null);
17
17
  var [errors, setErrors] = useState(() => {
18
18
  if (!config.lastSubmission) {
@@ -20,32 +20,29 @@ function useForm() {
20
20
  }
21
21
  return [].concat(config.lastSubmission.error['']);
22
22
  });
23
- var [uncontrolledState, setUncontrolledState] = useState(() => {
23
+ var initialError = useMemo(() => {
24
24
  var submission = config.lastSubmission;
25
25
  if (!submission) {
26
- return {
27
- defaultValue: config.defaultValue
28
- };
26
+ return {};
29
27
  }
30
28
  var scope = getScope(submission.intent);
31
- return {
32
- defaultValue: submission.payload,
33
- initialError: Object.entries(submission.error).reduce((result, _ref) => {
34
- var [name, message] = _ref;
35
- if (name !== '' && (scope === null || scope === name)) {
36
- result[name] = message;
37
- }
38
- return result;
39
- }, {})
40
- };
41
- });
42
- var fieldsetConfig = _objectSpread2(_objectSpread2({}, uncontrolledState), {}, {
29
+ return Object.entries(submission.error).reduce((result, _ref) => {
30
+ var [name, message] = _ref;
31
+ if (name !== '' && (scope === null || scope === name)) {
32
+ result[name] = message;
33
+ }
34
+ return result;
35
+ }, {});
36
+ }, [config.lastSubmission]);
37
+ var ref = (_config$ref = config.ref) !== null && _config$ref !== void 0 ? _config$ref : formRef;
38
+ var fieldset = useFieldset(ref, {
39
+ defaultValue: (_ref2 = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref2 !== void 0 ? _ref2 : config.defaultValue,
40
+ initialError,
43
41
  constraint: config.constraint,
44
42
  form: config.id
45
43
  });
46
- var fieldset = useFieldset(ref, fieldsetConfig);
47
44
  var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative);
48
- useEffect(() => {
45
+ useSafeLayoutEffect(() => {
49
46
  configRef.current = config;
50
47
  });
51
48
  useEffect(() => {
@@ -64,24 +61,29 @@ function useForm() {
64
61
  }));
65
62
  }
66
63
  setLastSubmission(submission);
67
- }, [config.lastSubmission]);
64
+ }, [ref, config.lastSubmission]);
68
65
  useEffect(() => {
69
66
  var form = ref.current;
70
67
  if (!form || !lastSubmission) {
71
68
  return;
72
69
  }
73
- reportSubmission(ref.current, lastSubmission);
74
- }, [lastSubmission]);
70
+ reportSubmission(form, lastSubmission);
71
+ }, [ref, lastSubmission]);
75
72
  useEffect(() => {
76
73
  // Revalidate the form when input value is changed
77
74
  var handleInput = event => {
78
75
  var field = event.target;
79
76
  var form = ref.current;
80
77
  var formConfig = configRef.current;
78
+ var {
79
+ initialReport = 'onSubmit',
80
+ shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport,
81
+ shouldRevalidate = 'onInput'
82
+ } = formConfig;
81
83
  if (!form || !isFieldElement(field) || field.form !== form) {
82
84
  return;
83
85
  }
84
- if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') {
86
+ if (field.dataset.conformTouched ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') {
85
87
  requestIntent(form, validate(field.name));
86
88
  }
87
89
  };
@@ -89,15 +91,20 @@ function useForm() {
89
91
  var field = event.target;
90
92
  var form = ref.current;
91
93
  var formConfig = configRef.current;
94
+ var {
95
+ initialReport = 'onSubmit',
96
+ shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport,
97
+ shouldRevalidate = 'onInput'
98
+ } = formConfig;
92
99
  if (!form || !isFieldElement(field) || field.form !== form) {
93
100
  return;
94
101
  }
95
- if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) {
102
+ if (field.dataset.conformTouched ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') {
96
103
  requestIntent(form, validate(field.name));
97
104
  }
98
105
  };
99
106
  var handleInvalid = event => {
100
- var form = getFormElement(ref.current);
107
+ var form = ref.current;
101
108
  var field = event.target;
102
109
  if (!form || !isFieldElement(field) || field.form !== form || field.name !== FORM_ERROR_ELEMENT_NAME) {
103
110
  return;
@@ -109,7 +116,6 @@ function useForm() {
109
116
  };
110
117
  var handleReset = event => {
111
118
  var form = ref.current;
112
- var formConfig = configRef.current;
113
119
  if (!form || event.target !== form) {
114
120
  return;
115
121
  }
@@ -122,17 +128,7 @@ function useForm() {
122
128
  }
123
129
  }
124
130
  setErrors([]);
125
- setUncontrolledState({
126
- defaultValue: formConfig.defaultValue
127
- });
128
131
  };
129
-
130
- /**
131
- * The input event handler will be triggered in capturing phase in order to
132
- * allow follow-up action in the bubble phase based on the latest validity
133
- * E.g. `useFieldset` reset the error of valid field after checking the
134
- * validity in the bubble phase.
135
- */
136
132
  document.addEventListener('input', handleInput, true);
137
133
  document.addEventListener('blur', handleBlur, true);
138
134
  document.addEventListener('invalid', handleInvalid, true);
@@ -143,7 +139,7 @@ function useForm() {
143
139
  document.removeEventListener('invalid', handleInvalid, true);
144
140
  document.removeEventListener('reset', handleReset);
145
141
  };
146
- }, []);
142
+ }, [ref]);
147
143
  var form = {
148
144
  ref,
149
145
  error: errors[0],
@@ -166,11 +162,11 @@ function useForm() {
166
162
  form,
167
163
  formData
168
164
  });
169
- if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref2 => {
170
- var [, message] = _ref2;
171
- return message !== '' && ![].concat(message).includes(VALIDATION_UNDEFINED);
172
- }) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref3 => {
165
+ if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref3 => {
173
166
  var [, message] = _ref3;
167
+ return message !== '' && ![].concat(message).includes(VALIDATION_UNDEFINED);
168
+ }) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref4 => {
169
+ var [, message] = _ref4;
174
170
  return ![].concat(message).includes(VALIDATION_UNDEFINED);
175
171
  })) {
176
172
  var listCommand = parseListCommand(submission.intent);
@@ -198,10 +194,10 @@ function useForm() {
198
194
  form.id = config.id;
199
195
  form.errorId = "".concat(config.id, "-error");
200
196
  form.props.id = form.id;
201
- form.props['aria-describedby'] = form.errorId;
202
197
  }
203
198
  if (form.errorId && form.errors.length > 0) {
204
199
  form.props['aria-invalid'] = 'true';
200
+ form.props['aria-describedby'] = form.errorId;
205
201
  }
206
202
  return [form, fieldset];
207
203
  }
@@ -212,35 +208,21 @@ function useForm() {
212
208
 
213
209
  function useFieldset(ref, config) {
214
210
  var configRef = useRef(config);
215
- var [uncontrolledState, setUncontrolledState] = useState(
216
- // @ts-expect-error
217
- () => {
218
- var _config$defaultValue;
219
- var initialError = {};
220
- for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
221
- var _config$initialError;
222
- var [key, ...paths] = getPaths(name);
223
- if (typeof key === 'string') {
224
- var scopedName = getName(paths);
225
- initialError[key] = _objectSpread2(_objectSpread2({}, initialError[key]), {}, {
226
- [scopedName]: message
227
- });
228
- }
229
- }
230
- return {
231
- defaultValue: (_config$defaultValue = config === null || config === void 0 ? void 0 : config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : {},
232
- initialError
233
- };
234
- });
235
211
  var [error, setError] = useState(() => {
212
+ var initialError = config === null || config === void 0 ? void 0 : config.initialError;
213
+ if (!initialError) {
214
+ return {};
215
+ }
236
216
  var result = {};
237
- for (var [key, _error] of Object.entries(uncontrolledState.initialError)) {
238
- var _error$;
239
- result[key] = [].concat((_error$ = _error === null || _error === void 0 ? void 0 : _error['']) !== null && _error$ !== void 0 ? _error$ : []);
217
+ for (var [name, message] of Object.entries(initialError)) {
218
+ var [key, ...paths] = getPaths(name);
219
+ if (typeof key === 'string' && paths.length === 0) {
220
+ result[key] = [].concat(message !== null && message !== void 0 ? message : []);
221
+ }
240
222
  }
241
223
  return result;
242
224
  });
243
- useEffect(() => {
225
+ useSafeLayoutEffect(() => {
244
226
  configRef.current = config;
245
227
  });
246
228
  useEffect(() => {
@@ -271,17 +253,10 @@ function useFieldset(ref, config) {
271
253
  }
272
254
  };
273
255
  var resetHandler = event => {
274
- var _fieldsetConfig$defau;
275
256
  var form = getFormElement(ref.current);
276
257
  if (!form || event.target !== form) {
277
258
  return;
278
259
  }
279
- var fieldsetConfig = configRef.current;
280
- setUncontrolledState({
281
- // @ts-expect-error
282
- defaultValue: (_fieldsetConfig$defau = fieldsetConfig === null || fieldsetConfig === void 0 ? void 0 : fieldsetConfig.defaultValue) !== null && _fieldsetConfig$defau !== void 0 ? _fieldsetConfig$defau : {},
283
- initialError: {}
284
- });
285
260
  setError({});
286
261
  };
287
262
 
@@ -301,17 +276,25 @@ function useFieldset(ref, config) {
301
276
  */
302
277
  return new Proxy({}, {
303
278
  get(_target, key) {
304
- var _fieldsetConfig$const;
279
+ var _fieldsetConfig$const, _fieldsetConfig$initi, _fieldsetConfig$defau;
305
280
  if (typeof key !== 'string') {
306
281
  return;
307
282
  }
308
- var fieldsetConfig = config !== null && config !== void 0 ? config : {};
283
+ var fieldsetConfig = config;
309
284
  var constraint = (_fieldsetConfig$const = fieldsetConfig.constraint) === null || _fieldsetConfig$const === void 0 ? void 0 : _fieldsetConfig$const[key];
310
285
  var errors = error === null || error === void 0 ? void 0 : error[key];
286
+ var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref5) => {
287
+ var [name, message] = _ref5;
288
+ var [field, ...paths] = getPaths(name);
289
+ if (field === key) {
290
+ result[getName(paths)] = message;
291
+ }
292
+ return result;
293
+ }, {});
311
294
  var field = _objectSpread2(_objectSpread2({}, constraint), {}, {
312
295
  name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key,
313
- defaultValue: uncontrolledState.defaultValue[key],
314
- initialError: uncontrolledState.initialError[key],
296
+ defaultValue: (_fieldsetConfig$defau = fieldsetConfig.defaultValue) === null || _fieldsetConfig$defau === void 0 ? void 0 : _fieldsetConfig$defau[key],
297
+ initialError,
315
298
  error: errors === null || errors === void 0 ? void 0 : errors[0],
316
299
  errors
317
300
  });
@@ -319,6 +302,7 @@ function useFieldset(ref, config) {
319
302
  field.form = fieldsetConfig.form;
320
303
  field.id = "".concat(fieldsetConfig.form, "-").concat(field.name);
321
304
  field.errorId = "".concat(field.id, "-error");
305
+ field.descriptionId = "".concat(field.id, "-description");
322
306
  }
323
307
  return field;
324
308
  }
@@ -333,33 +317,22 @@ function useFieldset(ref, config) {
333
317
  */
334
318
  function useFieldList(ref, config) {
335
319
  var configRef = useRef(config);
336
- var [uncontrolledState, setUncontrolledState] = useState(() => {
337
- var _config$defaultValue2;
320
+ var [error, setError] = useState(() => {
338
321
  var initialError = [];
339
- for (var [name, message] of Object.entries((_config$initialError2 = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {})) {
340
- var _config$initialError2;
322
+ for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
323
+ var _config$initialError;
341
324
  var [index, ...paths] = getPaths(name);
342
- if (typeof index === 'number') {
343
- var scopedName = getName(paths);
344
- initialError[index] = _objectSpread2(_objectSpread2({}, initialError[index]), {}, {
345
- [scopedName]: message
346
- });
325
+ if (typeof index === 'number' && paths.length === 0) {
326
+ initialError[index] = [].concat(message !== null && message !== void 0 ? message : []);
347
327
  }
348
328
  }
349
- return {
350
- defaultValue: (_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [],
351
- initialError
352
- };
329
+ return initialError;
353
330
  });
354
- var [error, setError] = useState(() => uncontrolledState.initialError.map(error => {
355
- var _error$2;
356
- return [].concat((_error$2 = error === null || error === void 0 ? void 0 : error['']) !== null && _error$2 !== void 0 ? _error$2 : []);
357
- }));
358
331
  var [entries, setEntries] = useState(() => {
359
- var _config$defaultValue3;
360
- return Object.entries((_config$defaultValue3 = config.defaultValue) !== null && _config$defaultValue3 !== void 0 ? _config$defaultValue3 : [undefined]);
332
+ var _config$defaultValue;
333
+ return Object.entries((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : [undefined]);
361
334
  });
362
- useEffect(() => {
335
+ useSafeLayoutEffect(() => {
363
336
  configRef.current = config;
364
337
  });
365
338
  useEffect(() => {
@@ -433,17 +406,12 @@ function useFieldList(ref, config) {
433
406
  });
434
407
  };
435
408
  var resetHandler = event => {
436
- var _fieldConfig$defaultV, _fieldConfig$defaultV2;
409
+ var _configRef$current$de;
437
410
  var form = getFormElement(ref.current);
438
411
  if (!form || event.target !== form) {
439
412
  return;
440
413
  }
441
- var fieldConfig = configRef.current;
442
- setUncontrolledState({
443
- defaultValue: (_fieldConfig$defaultV = fieldConfig.defaultValue) !== null && _fieldConfig$defaultV !== void 0 ? _fieldConfig$defaultV : [],
444
- initialError: []
445
- });
446
- setEntries(Object.entries((_fieldConfig$defaultV2 = fieldConfig.defaultValue) !== null && _fieldConfig$defaultV2 !== void 0 ? _fieldConfig$defaultV2 : [undefined]));
414
+ setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [undefined]));
447
415
  setError([]);
448
416
  };
449
417
 
@@ -458,13 +426,22 @@ function useFieldList(ref, config) {
458
426
  document.removeEventListener('reset', resetHandler);
459
427
  };
460
428
  }, [ref]);
461
- return entries.map((_ref4, index) => {
462
- var [key, defaultValue] = _ref4;
429
+ return entries.map((_ref6, index) => {
430
+ var _config$initialError2, _config$defaultValue2;
431
+ var [key, defaultValue] = _ref6;
463
432
  var errors = error[index];
433
+ var initialError = Object.entries((_config$initialError2 = config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {}).reduce((result, _ref7) => {
434
+ var [name, message] = _ref7;
435
+ var [field, ...paths] = getPaths(name);
436
+ if (field === index) {
437
+ result[getName(paths)] = message;
438
+ }
439
+ return result;
440
+ }, {});
464
441
  var fieldConfig = {
465
442
  name: "".concat(config.name, "[").concat(index, "]"),
466
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index],
467
- initialError: uncontrolledState.initialError[index],
443
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue2 = config.defaultValue) === null || _config$defaultValue2 === void 0 ? void 0 : _config$defaultValue2[index],
444
+ initialError,
468
445
  error: errors === null || errors === void 0 ? void 0 : errors[0],
469
446
  errors
470
447
  };
@@ -472,6 +449,7 @@ function useFieldList(ref, config) {
472
449
  fieldConfig.form = config.form;
473
450
  fieldConfig.id = "".concat(config.form, "-").concat(config.name);
474
451
  fieldConfig.errorId = "".concat(fieldConfig.id, "-error");
452
+ fieldConfig.descriptionId = "".concat(fieldConfig.id, "-description");
475
453
  }
476
454
  return _objectSpread2({
477
455
  key
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@conform-to/react",
3
3
  "description": "Conform view adapter for react",
4
+ "homepage": "https://conform.guide",
4
5
  "license": "MIT",
5
- "version": "0.6.0",
6
+ "version": "0.6.1",
6
7
  "main": "index.js",
7
8
  "module": "module/index.js",
8
9
  "repository": {
@@ -19,7 +20,7 @@
19
20
  "url": "https://github.com/edmundhung/conform/issues"
20
21
  },
21
22
  "dependencies": {
22
- "@conform-to/dom": "0.6.0"
23
+ "@conform-to/dom": "0.6.1"
23
24
  },
24
25
  "peerDependencies": {
25
26
  "react": ">=16.8"
@@ -28,8 +29,11 @@
28
29
  "constraint-validation",
29
30
  "form",
30
31
  "form-validation",
32
+ "html",
33
+ "progressive-enhancement",
31
34
  "validation",
32
- "react"
35
+ "react",
36
+ "remix"
33
37
  ],
34
38
  "sideEffects": false
35
39
  }