@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/README.md +10 -2
- package/base.d.ts +0 -0
- package/helpers.d.ts +11 -12
- package/helpers.js +7 -9
- package/hooks.d.ts +80 -14
- package/hooks.js +372 -271
- package/index.d.ts +2 -2
- package/index.js +1 -4
- package/module/helpers.js +6 -2
- package/module/hooks.js +368 -273
- package/module/index.js +2 -2
- package/package.json +7 -3
package/module/hooks.js
CHANGED
|
@@ -1,6 +1,122 @@
|
|
|
1
1
|
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
|
|
2
|
-
import { getScope,
|
|
3
|
-
import {
|
|
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$
|
|
128
|
+
var _ref, _config$lastSubmissio2;
|
|
13
129
|
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
14
|
-
var configRef =
|
|
15
|
-
var ref =
|
|
16
|
-
var
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
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
|
|
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 =
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,
|
|
242
|
+
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, {
|
|
187
243
|
formData,
|
|
188
|
-
submission
|
|
189
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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:
|
|
314
|
-
initialError
|
|
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
|
|
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 =
|
|
336
|
-
var [
|
|
337
|
-
|
|
338
|
-
|
|
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$
|
|
360
|
-
return Object.entries((_config$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
451
|
-
document.addEventListener('conform
|
|
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
|
|
456
|
-
document.removeEventListener('conform
|
|
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 :
|
|
467
|
-
initialError
|
|
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 =
|
|
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 };
|