@conform-to/react 0.6.0 → 0.6.2

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/module/hooks.js CHANGED
@@ -1,6 +1,122 @@
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, getFormData, parse, isSubmitting, getFormAction, getFormEncType, getFormMethod, getPaths, getName, isFieldElement, getErrors, getFormControls, getFormElement, parseListCommand, updateList, getValidationMessage, focusFormControl, focusFirstInvalidControl, INTENT, isFocusableFormControl, requestIntent, validate } from '@conform-to/dom';
3
+ import { useState, useMemo, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
4
+
5
+ /**
6
+ * Normalize error to an array of string.
7
+ */
8
+ function normalizeError(error) {
9
+ if (!error) {
10
+ // This treat both empty string and undefined as no error.
11
+ return [];
12
+ }
13
+ return [].concat(error);
14
+ }
15
+ function useNoValidate(defaultNoValidate, validateBeforeHydrate) {
16
+ var [noValidate, setNoValidate] = useState(defaultNoValidate || !validateBeforeHydrate);
17
+ useEffect(() => {
18
+ setNoValidate(true);
19
+ }, []);
20
+ return noValidate;
21
+ }
22
+ function useFormRef(userProvidedRef) {
23
+ var formRef = useRef(null);
24
+ return userProvidedRef !== null && userProvidedRef !== void 0 ? userProvidedRef : formRef;
25
+ }
26
+ function useConfigRef(config) {
27
+ var ref = useRef(config);
28
+ useSafeLayoutEffect(() => {
29
+ ref.current = config;
30
+ });
31
+ return ref;
32
+ }
33
+ function useFormReporter(ref, lastSubmission) {
34
+ var [submission, setSubmission] = useState(lastSubmission);
35
+ var report = useCallback((form, submission) => {
36
+ var event = new CustomEvent('conform', {
37
+ detail: submission.intent
38
+ });
39
+ form.dispatchEvent(event);
40
+ setSubmission(submission);
41
+ }, []);
42
+ useEffect(() => {
43
+ var form = ref.current;
44
+ if (!form || !lastSubmission) {
45
+ return;
46
+ }
47
+ report(form, lastSubmission);
48
+ }, [ref, lastSubmission, report]);
49
+ useEffect(() => {
50
+ var form = ref.current;
51
+ if (!form || !submission) {
52
+ return;
53
+ }
54
+ reportSubmission(form, submission);
55
+ }, [ref, submission]);
56
+ return report;
57
+ }
58
+ function useFormError(ref, config) {
59
+ var [error, setError] = useState(() => {
60
+ if (!config.initialError) {
61
+ return {};
62
+ }
63
+ var result = {};
64
+ for (var [name, message] of Object.entries(config.initialError)) {
65
+ var paths = getPaths(name);
66
+ if (paths.length === 1) {
67
+ result[paths[0]] = normalizeError(message);
68
+ }
69
+ }
70
+ return result;
71
+ });
72
+ useEffect(() => {
73
+ var handleInvalid = event => {
74
+ var form = getFormElement(ref.current);
75
+ var element = event.target;
76
+ if (!isFieldElement(element) || element.form !== form || !element.dataset.conformTouched) {
77
+ return;
78
+ }
79
+ var key = element.name;
80
+ if (config.name) {
81
+ var scopePaths = getPaths(config.name);
82
+ var fieldPaths = getPaths(element.name);
83
+ for (var i = 0; i <= scopePaths.length; i++) {
84
+ var path = fieldPaths[i];
85
+ if (i < scopePaths.length) {
86
+ // Skip if the field is not in the scope
87
+ if (path !== scopePaths[i]) {
88
+ return;
89
+ }
90
+ } else {
91
+ key = path;
92
+ }
93
+ }
94
+ }
95
+ setError(prev => {
96
+ if (element.validationMessage === getValidationMessage(prev[key])) {
97
+ return prev;
98
+ }
99
+ return _objectSpread2(_objectSpread2({}, prev), {}, {
100
+ [key]: getErrors(element.validationMessage)
101
+ });
102
+ });
103
+ event.preventDefault();
104
+ };
105
+ var handleReset = event => {
106
+ var form = getFormElement(ref.current);
107
+ if (form && event.target === form) {
108
+ setError({});
109
+ }
110
+ };
111
+ document.addEventListener('reset', handleReset);
112
+ document.addEventListener('invalid', handleInvalid, true);
113
+ return () => {
114
+ document.removeEventListener('reset', handleReset);
115
+ document.removeEventListener('invalid', handleInvalid, true);
116
+ };
117
+ }, [ref, config.name]);
118
+ return [error, setError];
119
+ }
4
120
 
5
121
  /**
6
122
  * Returns properties required to hook into form events.
@@ -9,95 +125,51 @@ import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react';
9
125
  * @see https://conform.guide/api/react#useform
10
126
  */
11
127
  function useForm() {
12
- var _config$lastSubmissio;
128
+ var _ref, _config$lastSubmissio2;
13
129
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
14
- var configRef = useRef(config);
15
- var ref = useRef(null);
16
- var [lastSubmission, setLastSubmission] = useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null);
130
+ var configRef = useConfigRef(config);
131
+ var ref = useFormRef(config.ref);
132
+ var noValidate = useNoValidate(config.noValidate, config.fallbackNative);
133
+ var report = useFormReporter(ref, config.lastSubmission);
17
134
  var [errors, setErrors] = useState(() => {
18
- if (!config.lastSubmission) {
19
- return [];
20
- }
21
- return [].concat(config.lastSubmission.error['']);
135
+ var _config$lastSubmissio;
136
+ return normalizeError((_config$lastSubmissio = config.lastSubmission) === null || _config$lastSubmissio === void 0 ? void 0 : _config$lastSubmissio.error['']);
22
137
  });
23
- var [uncontrolledState, setUncontrolledState] = useState(() => {
138
+ var initialError = useMemo(() => {
24
139
  var submission = config.lastSubmission;
25
140
  if (!submission) {
26
- return {
27
- defaultValue: config.defaultValue
28
- };
141
+ return {};
29
142
  }
30
143
  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
- }, {})
144
+ return scope === null ? submission.error : {
145
+ [scope]: submission.error[scope]
40
146
  };
41
- });
42
- var fieldsetConfig = _objectSpread2(_objectSpread2({}, uncontrolledState), {}, {
147
+ }, [config.lastSubmission]);
148
+ var fieldset = useFieldset(ref, {
149
+ defaultValue: (_ref = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref !== void 0 ? _ref : config.defaultValue,
150
+ initialError,
43
151
  constraint: config.constraint,
44
152
  form: config.id
45
153
  });
46
- var fieldset = useFieldset(ref, fieldsetConfig);
47
- var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative);
48
154
  useEffect(() => {
49
- configRef.current = config;
50
- });
51
- useEffect(() => {
52
- setNoValidate(true);
53
- }, []);
54
- useEffect(() => {
55
- var form = ref.current;
56
- var submission = config.lastSubmission;
57
- if (!form || !submission) {
58
- return;
59
- }
60
- var listCommand = parseListCommand(submission.intent);
61
- if (listCommand) {
62
- form.dispatchEvent(new CustomEvent('conform/list', {
63
- detail: submission.intent
64
- }));
65
- }
66
- setLastSubmission(submission);
67
- }, [config.lastSubmission]);
68
- useEffect(() => {
69
- var form = ref.current;
70
- if (!form || !lastSubmission) {
71
- return;
72
- }
73
- reportSubmission(ref.current, lastSubmission);
74
- }, [lastSubmission]);
75
- useEffect(() => {
76
- // Revalidate the form when input value is changed
77
- var handleInput = event => {
155
+ // custom validate handler
156
+ var createValidateHandler = name => event => {
78
157
  var field = event.target;
79
158
  var form = ref.current;
80
- var formConfig = configRef.current;
81
- if (!form || !isFieldElement(field) || field.form !== form) {
159
+ var {
160
+ initialReport = 'onSubmit',
161
+ shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport,
162
+ shouldRevalidate = 'onInput'
163
+ } = configRef.current;
164
+ if (!form || !isFocusableFormControl(field) || field.form !== form) {
82
165
  return;
83
166
  }
84
- if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') {
85
- requestIntent(form, validate(field.name));
86
- }
87
- };
88
- var handleBlur = event => {
89
- var field = event.target;
90
- var form = ref.current;
91
- var formConfig = configRef.current;
92
- if (!form || !isFieldElement(field) || field.form !== form) {
93
- return;
94
- }
95
- if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) {
167
+ if (field.dataset.conformTouched ? shouldRevalidate === name : shouldValidate === name) {
96
168
  requestIntent(form, validate(field.name));
97
169
  }
98
170
  };
99
171
  var handleInvalid = event => {
100
- var form = getFormElement(ref.current);
172
+ var form = ref.current;
101
173
  var field = event.target;
102
174
  if (!form || !isFieldElement(field) || field.form !== form || field.name !== FORM_ERROR_ELEMENT_NAME) {
103
175
  return;
@@ -109,30 +181,19 @@ function useForm() {
109
181
  };
110
182
  var handleReset = event => {
111
183
  var form = ref.current;
112
- var formConfig = configRef.current;
113
184
  if (!form || event.target !== form) {
114
185
  return;
115
186
  }
116
187
 
117
188
  // Reset all field state
118
- for (var field of form.elements) {
119
- if (isFieldElement(field)) {
120
- delete field.dataset.conformTouched;
121
- field.setCustomValidity('');
122
- }
189
+ for (var element of getFormControls(form)) {
190
+ delete element.dataset.conformTouched;
191
+ element.setCustomValidity('');
123
192
  }
124
193
  setErrors([]);
125
- setUncontrolledState({
126
- defaultValue: formConfig.defaultValue
127
- });
128
194
  };
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
- */
195
+ var handleInput = createValidateHandler('onInput');
196
+ var handleBlur = createValidateHandler('onBlur');
136
197
  document.addEventListener('input', handleInput, true);
137
198
  document.addEventListener('blur', handleBlur, true);
138
199
  document.addEventListener('invalid', handleInvalid, true);
@@ -143,7 +204,7 @@ function useForm() {
143
204
  document.removeEventListener('invalid', handleInvalid, true);
144
205
  document.removeEventListener('reset', handleReset);
145
206
  };
146
- }, []);
207
+ }, [ref, configRef]);
147
208
  var form = {
148
209
  ref,
149
210
  error: errors[0],
@@ -159,34 +220,32 @@ function useForm() {
159
220
  return;
160
221
  }
161
222
  try {
162
- var _config$onValidate;
223
+ var _config$onValidate, _config$onValidate2;
163
224
  var formData = getFormData(form, submitter);
164
- var getSubmission = (_config$onValidate = config.onValidate) !== null && _config$onValidate !== void 0 ? _config$onValidate : context => parse(context.formData);
165
- var submission = getSubmission({
225
+ var submission = (_config$onValidate = (_config$onValidate2 = config.onValidate) === null || _config$onValidate2 === void 0 ? void 0 : _config$onValidate2.call(config, {
166
226
  form,
167
227
  formData
168
- });
169
- if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref2 => {
228
+ })) !== null && _config$onValidate !== void 0 ? _config$onValidate : parse(formData);
229
+ var messages = Object.entries(submission.error).reduce((messages, _ref2) => {
170
230
  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 => {
173
- var [, message] = _ref3;
174
- return ![].concat(message).includes(VALIDATION_UNDEFINED);
175
- })) {
176
- var listCommand = parseListCommand(submission.intent);
177
- if (listCommand) {
178
- form.dispatchEvent(new CustomEvent('conform/list', {
179
- detail: submission.intent
180
- }));
181
- }
182
- setLastSubmission(submission);
231
+ return messages.concat(normalizeError(message));
232
+ }, []);
233
+ var shouldValidate = !config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate);
234
+ var shouldFallbackToServer = messages.includes(VALIDATION_UNDEFINED);
235
+ var hasClientValidation = typeof config.onValidate !== 'undefined';
236
+ var isValid = messages.length === 0;
237
+ if (hasClientValidation && (isSubmitting(submission.intent) ? shouldValidate && !isValid : !shouldFallbackToServer)) {
238
+ report(form, submission);
183
239
  event.preventDefault();
184
240
  } else {
185
241
  var _config$onSubmit;
186
- (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, _objectSpread2({
242
+ (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, {
187
243
  formData,
188
- submission
189
- }, getFormAttributes(form, submitter)));
244
+ submission,
245
+ action: getFormAction(nativeEvent),
246
+ encType: getFormEncType(nativeEvent),
247
+ method: getFormMethod(nativeEvent)
248
+ });
190
249
  }
191
250
  } catch (e) {
192
251
  console.warn(e);
@@ -198,10 +257,10 @@ function useForm() {
198
257
  form.id = config.id;
199
258
  form.errorId = "".concat(config.id, "-error");
200
259
  form.props.id = form.id;
201
- form.props['aria-describedby'] = form.errorId;
202
260
  }
203
261
  if (form.errorId && form.errors.length > 0) {
204
262
  form.props['aria-invalid'] = 'true';
263
+ form.props['aria-describedby'] = form.errorId;
205
264
  }
206
265
  return [form, fieldset];
207
266
  }
@@ -211,88 +270,10 @@ function useForm() {
211
270
  */
212
271
 
213
272
  function useFieldset(ref, config) {
214
- 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
- var [error, setError] = useState(() => {
236
- 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$ : []);
240
- }
241
- return result;
242
- });
243
- useEffect(() => {
244
- configRef.current = config;
273
+ var [error] = useFormError(ref, {
274
+ initialError: config.initialError,
275
+ name: config.name
245
276
  });
246
- useEffect(() => {
247
- var invalidHandler = event => {
248
- var _configRef$current$na;
249
- var form = getFormElement(ref.current);
250
- var field = event.target;
251
- var fieldsetName = (_configRef$current$na = configRef.current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : '';
252
- if (!form || !isFieldElement(field) || field.form !== form || !field.name.startsWith(fieldsetName)) {
253
- return;
254
- }
255
- var [key, ...paths] = getPaths(fieldsetName.length > 0 ? field.name.slice(fieldsetName.length + 1) : field.name);
256
-
257
- // Update the error only if the field belongs to the fieldset
258
- if (typeof key === 'string' && paths.length === 0) {
259
- if (field.dataset.conformTouched) {
260
- setError(prev => {
261
- var prevMessage = getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[key]);
262
- if (prevMessage === field.validationMessage) {
263
- return prev;
264
- }
265
- return _objectSpread2(_objectSpread2({}, prev), {}, {
266
- [key]: getErrors(field.validationMessage)
267
- });
268
- });
269
- }
270
- event.preventDefault();
271
- }
272
- };
273
- var resetHandler = event => {
274
- var _fieldsetConfig$defau;
275
- var form = getFormElement(ref.current);
276
- if (!form || event.target !== form) {
277
- return;
278
- }
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
- setError({});
286
- };
287
-
288
- // The invalid event does not bubble and so listening on the capturing pharse is needed
289
- document.addEventListener('invalid', invalidHandler, true);
290
- document.addEventListener('reset', resetHandler);
291
- return () => {
292
- document.removeEventListener('invalid', invalidHandler, true);
293
- document.removeEventListener('reset', resetHandler);
294
- };
295
- }, [ref]);
296
277
 
297
278
  /**
298
279
  * This allows us constructing the field at runtime as we have no information
@@ -301,17 +282,25 @@ function useFieldset(ref, config) {
301
282
  */
302
283
  return new Proxy({}, {
303
284
  get(_target, key) {
304
- var _fieldsetConfig$const;
285
+ var _fieldsetConfig$const, _fieldsetConfig$initi, _fieldsetConfig$defau;
305
286
  if (typeof key !== 'string') {
306
287
  return;
307
288
  }
308
- var fieldsetConfig = config !== null && config !== void 0 ? config : {};
289
+ var fieldsetConfig = config;
309
290
  var constraint = (_fieldsetConfig$const = fieldsetConfig.constraint) === null || _fieldsetConfig$const === void 0 ? void 0 : _fieldsetConfig$const[key];
310
291
  var errors = error === null || error === void 0 ? void 0 : error[key];
292
+ var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref3) => {
293
+ var [name, message] = _ref3;
294
+ var [field, ...paths] = getPaths(name);
295
+ if (field === key) {
296
+ result[getName(paths)] = message;
297
+ }
298
+ return result;
299
+ }, {});
311
300
  var field = _objectSpread2(_objectSpread2({}, constraint), {}, {
312
301
  name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key,
313
- defaultValue: uncontrolledState.defaultValue[key],
314
- initialError: uncontrolledState.initialError[key],
302
+ defaultValue: (_fieldsetConfig$defau = fieldsetConfig.defaultValue) === null || _fieldsetConfig$defau === void 0 ? void 0 : _fieldsetConfig$defau[key],
303
+ initialError,
315
304
  error: errors === null || errors === void 0 ? void 0 : errors[0],
316
305
  errors
317
306
  });
@@ -319,6 +308,7 @@ function useFieldset(ref, config) {
319
308
  field.form = fieldsetConfig.form;
320
309
  field.id = "".concat(fieldsetConfig.form, "-").concat(field.name);
321
310
  field.errorId = "".concat(field.id, "-error");
311
+ field.descriptionId = "".concat(field.id, "-description");
322
312
  }
323
313
  return field;
324
314
  }
@@ -326,68 +316,22 @@ function useFieldset(ref, config) {
326
316
  }
327
317
 
328
318
  /**
329
- * Returns a list of key and config, with a group of helpers
330
- * configuring buttons for list manipulation
319
+ * Returns a list of key and field config.
331
320
  *
332
321
  * @see https://conform.guide/api/react#usefieldlist
333
322
  */
334
323
  function useFieldList(ref, config) {
335
- var configRef = useRef(config);
336
- var [uncontrolledState, setUncontrolledState] = useState(() => {
337
- var _config$defaultValue2;
338
- 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;
341
- 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
- });
347
- }
348
- }
349
- return {
350
- defaultValue: (_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [],
351
- initialError
352
- };
324
+ var configRef = useConfigRef(config);
325
+ var [error, setError] = useFormError(ref, {
326
+ initialError: config.initialError,
327
+ name: config.name
353
328
  });
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
329
  var [entries, setEntries] = useState(() => {
359
- var _config$defaultValue3;
360
- return Object.entries((_config$defaultValue3 = config.defaultValue) !== null && _config$defaultValue3 !== void 0 ? _config$defaultValue3 : [undefined]);
361
- });
362
- useEffect(() => {
363
- configRef.current = config;
330
+ var _config$defaultValue;
331
+ return Object.entries((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : [undefined]);
364
332
  });
365
333
  useEffect(() => {
366
- var invalidHandler = event => {
367
- var _configRef$current$na2;
368
- var form = getFormElement(ref.current);
369
- var field = event.target;
370
- var prefix = (_configRef$current$na2 = configRef.current.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : '';
371
- if (!form || !isFieldElement(field) || field.form !== form || !field.name.startsWith(prefix)) {
372
- return;
373
- }
374
- var [index, ...paths] = getPaths(prefix.length > 0 ? field.name.slice(prefix.length) : field.name);
375
-
376
- // Update the error only if the field belongs to the fieldset
377
- if (typeof index === 'number' && paths.length === 0) {
378
- if (field.dataset.conformTouched) {
379
- setError(prev => {
380
- var prevMessage = getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[index]);
381
- if (prevMessage === field.validationMessage) {
382
- return prev;
383
- }
384
- return [...prev.slice(0, index), getErrors(field.validationMessage), ...prev.slice(index + 1)];
385
- });
386
- }
387
- event.preventDefault();
388
- }
389
- };
390
- var listHandler = event => {
334
+ var conformHandler = event => {
391
335
  var form = getFormElement(ref.current);
392
336
  if (!form || event.target !== form) {
393
337
  return;
@@ -416,55 +360,65 @@ function useFieldList(ref, config) {
416
360
  }
417
361
  });
418
362
  setError(error => {
363
+ var errorList = [];
364
+ for (var [key, messages] of Object.entries(error)) {
365
+ if (typeof key === 'number') {
366
+ errorList[key] = messages;
367
+ }
368
+ }
419
369
  switch (command.type) {
420
370
  case 'append':
421
371
  case 'prepend':
422
372
  case 'replace':
423
- return updateList([...error], _objectSpread2(_objectSpread2({}, command), {}, {
373
+ errorList = updateList(errorList, _objectSpread2(_objectSpread2({}, command), {}, {
424
374
  payload: _objectSpread2(_objectSpread2({}, command.payload), {}, {
425
375
  defaultValue: undefined
426
376
  })
427
377
  }));
378
+ break;
428
379
  default:
429
380
  {
430
- return updateList([...error], command);
381
+ errorList = updateList(errorList, command);
382
+ break;
431
383
  }
432
384
  }
385
+ return Object.assign({}, errorList);
433
386
  });
434
387
  };
435
388
  var resetHandler = event => {
436
- var _fieldConfig$defaultV, _fieldConfig$defaultV2;
389
+ var _configRef$current$de;
437
390
  var form = getFormElement(ref.current);
438
391
  if (!form || event.target !== form) {
439
392
  return;
440
393
  }
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]));
447
- setError([]);
394
+ setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [undefined]));
448
395
  };
449
396
 
450
- // @ts-expect-error Custom event: conform/list
451
- document.addEventListener('conform/list', listHandler, true);
452
- document.addEventListener('invalid', invalidHandler, true);
397
+ // @ts-expect-error Custom event: conform
398
+ document.addEventListener('conform', conformHandler, true);
453
399
  document.addEventListener('reset', resetHandler);
454
400
  return () => {
455
- // @ts-expect-error Custom event: conform/list
456
- document.removeEventListener('conform/list', listHandler, true);
457
- document.removeEventListener('invalid', invalidHandler, true);
401
+ // @ts-expect-error Custom event: conform
402
+ document.removeEventListener('conform', conformHandler, true);
458
403
  document.removeEventListener('reset', resetHandler);
459
404
  };
460
- }, [ref]);
405
+ }, [ref, configRef, setError]);
461
406
  return entries.map((_ref4, index) => {
407
+ var _config$initialError, _config$defaultValue2;
462
408
  var [key, defaultValue] = _ref4;
463
409
  var errors = error[index];
410
+ var initialError = Object.entries((_config$initialError = config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {}).reduce((result, _ref5) => {
411
+ var [name, message] = _ref5;
412
+ var [field, ...paths] = getPaths(name);
413
+ if (field === index) {
414
+ result[getName(paths)] = message;
415
+ }
416
+ return result;
417
+ }, {});
464
418
  var fieldConfig = {
465
419
  name: "".concat(config.name, "[").concat(index, "]"),
466
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index],
467
- initialError: uncontrolledState.initialError[index],
420
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue2 = config.defaultValue) === null || _config$defaultValue2 === void 0 ? void 0 : _config$defaultValue2[index],
421
+ initialError,
468
422
  error: errors === null || errors === void 0 ? void 0 : errors[0],
469
423
  errors
470
424
  };
@@ -472,6 +426,7 @@ function useFieldList(ref, config) {
472
426
  fieldConfig.form = config.form;
473
427
  fieldConfig.id = "".concat(config.form, "-").concat(config.name);
474
428
  fieldConfig.errorId = "".concat(fieldConfig.id, "-error");
429
+ fieldConfig.descriptionId = "".concat(fieldConfig.id, "-description");
475
430
  }
476
431
  return _objectSpread2({
477
432
  key
@@ -515,13 +470,10 @@ function setNativeValue(element, value) {
515
470
  var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
516
471
  function useInputEvent(options) {
517
472
  var ref = useRef(null);
518
- var optionsRef = useRef(options);
473
+ var optionsRef = useConfigRef(options);
519
474
  var changeDispatched = useRef(false);
520
475
  var focusDispatched = useRef(false);
521
476
  var blurDispatched = useRef(false);
522
- useSafeLayoutEffect(() => {
523
- optionsRef.current = options;
524
- });
525
477
  useSafeLayoutEffect(() => {
526
478
  var getInputElement = () => {
527
479
  var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2;
@@ -650,8 +602,151 @@ function useInputEvent(options) {
650
602
  blurDispatched.current = false;
651
603
  }
652
604
  };
653
- }, []);
605
+ }, [optionsRef]);
654
606
  return [ref, control];
655
607
  }
608
+ var VALIDATION_UNDEFINED = '__undefined__';
609
+ var VALIDATION_SKIPPED = '__skipped__';
610
+ var FORM_ERROR_ELEMENT_NAME = '__form__';
611
+
612
+ /**
613
+ * Validate the form with the Constraint Validation API
614
+ * @see https://conform.guide/api/react#validateconstraint
615
+ */
616
+ function validateConstraint(options) {
617
+ var _options$formData, _options$formatMessag;
618
+ var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form);
619
+ var getDefaultErrors = (validity, result) => {
620
+ var errors = [];
621
+ if (validity.valueMissing) errors.push('required');
622
+ if (validity.typeMismatch || validity.badInput) errors.push('type');
623
+ if (validity.tooShort) errors.push('minLength');
624
+ if (validity.rangeUnderflow) errors.push('min');
625
+ if (validity.stepMismatch) errors.push('step');
626
+ if (validity.tooLong) errors.push('maxLength');
627
+ if (validity.rangeOverflow) errors.push('max');
628
+ if (validity.patternMismatch) errors.push('pattern');
629
+ for (var [constraintName, valid] of Object.entries(result)) {
630
+ if (!valid) {
631
+ errors.push(constraintName);
632
+ }
633
+ }
634
+ return errors;
635
+ };
636
+ var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref6 => {
637
+ var {
638
+ defaultErrors
639
+ } = _ref6;
640
+ return defaultErrors;
641
+ };
642
+ return parse(formData, {
643
+ resolve(payload, intent) {
644
+ var error = {};
645
+ var constraintPattern = /^constraint[A-Z][^A-Z]*$/;
646
+ var _loop = function _loop(element) {
647
+ if (isFieldElement(element)) {
648
+ var _options$acceptMultip, _options$acceptMultip2;
649
+ var name = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
650
+ var constraint = Object.entries(element.dataset).reduce((result, _ref7) => {
651
+ var [name, attributeValue = ''] = _ref7;
652
+ if (constraintPattern.test(name)) {
653
+ var _options$constraint;
654
+ var constraintName = name.slice(10).toLowerCase();
655
+ var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName];
656
+ if (typeof _validate === 'function') {
657
+ result[constraintName] = _validate(element.value, {
658
+ formData,
659
+ attributeValue
660
+ });
661
+ } else {
662
+ console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API."));
663
+ }
664
+ }
665
+ return result;
666
+ }, {});
667
+ var errors = formatMessages({
668
+ name,
669
+ validity: element.validity,
670
+ constraint,
671
+ defaultErrors: getDefaultErrors(element.validity, constraint)
672
+ });
673
+ var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, {
674
+ name,
675
+ payload,
676
+ intent
677
+ })) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false;
678
+ if (errors.length > 0) {
679
+ error[name] = shouldAcceptMultipleErrors ? errors : errors[0];
680
+ }
681
+ }
682
+ };
683
+ for (var element of options.form.elements) {
684
+ _loop(element);
685
+ }
686
+ return {
687
+ error
688
+ };
689
+ }
690
+ });
691
+ }
692
+ function reportSubmission(form, submission) {
693
+ for (var [name, message] of Object.entries(submission.error)) {
694
+ // There is no need to create a placeholder button if all we want is to reset the error
695
+ if (message === '') {
696
+ continue;
697
+ }
698
+
699
+ // We can't use empty string as button name
700
+ // As `form.element.namedItem('')` will always returns null
701
+ var elementName = name ? name : FORM_ERROR_ELEMENT_NAME;
702
+ var item = form.elements.namedItem(elementName);
703
+ if (item instanceof RadioNodeList) {
704
+ for (var field of item) {
705
+ if (field.type !== 'radio') {
706
+ console.warn('Repeated field name is not supported.');
707
+ continue;
708
+ }
709
+ }
710
+ }
711
+ if (item === null) {
712
+ // Create placeholder button to keep the error without contributing to the form data
713
+ var button = document.createElement('button');
714
+ button.name = elementName;
715
+ button.hidden = true;
716
+ button.dataset.conformTouched = 'true';
717
+ form.appendChild(button);
718
+ }
719
+ }
720
+ var scope = getScope(submission.intent);
721
+ for (var element of getFormControls(form)) {
722
+ var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : '';
723
+ var messages = normalizeError(submission.error[_elementName]);
724
+ if (scope === null || scope === _elementName) {
725
+ element.dataset.conformTouched = 'true';
726
+ }
727
+ if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) {
728
+ var invalidEvent = new Event('invalid', {
729
+ cancelable: true
730
+ });
731
+ element.setCustomValidity(getValidationMessage(messages));
732
+ element.dispatchEvent(invalidEvent);
733
+ }
734
+ }
735
+ if (isSubmitting(submission.intent) || isFocusedOnIntentButton(form, submission.intent)) {
736
+ if (scope) {
737
+ focusFormControl(form, scope);
738
+ } else {
739
+ focusFirstInvalidControl(form);
740
+ }
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Check if the current focus is on a intent button.
746
+ */
747
+ function isFocusedOnIntentButton(form, intent) {
748
+ var element = document.activeElement;
749
+ return isFieldElement(element) && element.type === 'submit' && element.form === form && element.name === INTENT && element.value === intent;
750
+ }
656
751
 
657
- export { useFieldList, useFieldset, useForm, useInputEvent };
752
+ export { FORM_ERROR_ELEMENT_NAME, VALIDATION_SKIPPED, VALIDATION_UNDEFINED, isFocusedOnIntentButton, reportSubmission, useFieldList, useFieldset, useForm, useInputEvent, validateConstraint };