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