@conform-to/react 1.8.2 → 1.9.0
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 +1 -1
- package/dist/future/dom.d.ts +36 -0
- package/dist/future/dom.js +226 -0
- package/dist/future/dom.mjs +212 -0
- package/dist/future/hooks.d.ts +145 -54
- package/dist/future/hooks.js +566 -38
- package/dist/future/hooks.mjs +550 -33
- package/dist/future/index.d.ts +4 -3
- package/dist/future/index.js +15 -2
- package/dist/future/index.mjs +2 -2
- package/dist/future/intent.d.ts +35 -0
- package/dist/future/intent.js +305 -0
- package/dist/future/intent.mjs +294 -0
- package/dist/future/state.d.ts +56 -0
- package/dist/future/state.js +300 -0
- package/dist/future/state.mjs +283 -0
- package/dist/future/types.d.ts +360 -0
- package/dist/future/util.d.ts +65 -36
- package/dist/future/util.js +198 -116
- package/dist/future/util.mjs +182 -110
- package/package.json +3 -2
- package/dist/future/context.d.ts +0 -15
- package/dist/future/context.js +0 -12
- package/dist/future/context.mjs +0 -8
package/dist/future/hooks.mjs
CHANGED
|
@@ -1,7 +1,506 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs';
|
|
3
|
+
import { DEFAULT_INTENT_NAME, createGlobalFormsObserver, serialize, isFieldElement, deepEqual, change, focus, blur, getFormData, parseSubmission, report, createSubmitEvent } from '@conform-to/dom/future';
|
|
4
|
+
import { createContext, useContext, useMemo, useId, useRef, useEffect, useSyncExternalStore, useCallback, useState, useLayoutEffect } from 'react';
|
|
5
|
+
import { resolveStandardSchemaResult, resolveValidateResult, appendUniqueItem } from './util.mjs';
|
|
6
|
+
import { isTouched, getFormMetadata, getFieldset, getField, initializeState, updateState } from './state.mjs';
|
|
7
|
+
import { deserializeIntent, actionHandlers, applyIntent } from './intent.mjs';
|
|
8
|
+
import { focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, updateFormValue, getSubmitEvent } from './dom.mjs';
|
|
9
|
+
import { jsx } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
var FormConfig = /*#__PURE__*/createContext({
|
|
12
|
+
intentName: DEFAULT_INTENT_NAME,
|
|
13
|
+
observer: createGlobalFormsObserver(),
|
|
14
|
+
serialize
|
|
15
|
+
});
|
|
16
|
+
var Form = /*#__PURE__*/createContext([]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provides form context to child components.
|
|
20
|
+
* Stacks contexts to support nested forms, with latest context taking priority.
|
|
21
|
+
*/
|
|
22
|
+
function FormProvider(props) {
|
|
23
|
+
var stack = useContext(Form);
|
|
24
|
+
var value = useMemo(
|
|
25
|
+
// Put the latest form context first to ensure that to be the first one found
|
|
26
|
+
() => [props.context].concat(stack), [stack, props.context]);
|
|
27
|
+
return /*#__PURE__*/jsx(Form.Provider, {
|
|
28
|
+
value: value,
|
|
29
|
+
children: props.children
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function useFormContext(formId) {
|
|
33
|
+
var contexts = useContext(Form);
|
|
34
|
+
var context = formId ? contexts.find(context => formId === context.formId) : contexts[0];
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error('No form context found; Have you render a <FormProvider /> with the corresponding form context?');
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Core form hook that manages form state, validation, and submission.
|
|
43
|
+
* Handles both sync and async validation, intent dispatching, and DOM updates.
|
|
44
|
+
*/
|
|
45
|
+
function useConform(formRef, options) {
|
|
46
|
+
var {
|
|
47
|
+
lastResult
|
|
48
|
+
} = options;
|
|
49
|
+
var [state, setState] = useState(() => {
|
|
50
|
+
var state = initializeState();
|
|
51
|
+
if (lastResult) {
|
|
52
|
+
state = updateState(state, _objectSpread2(_objectSpread2({}, lastResult), {}, {
|
|
53
|
+
type: 'initialize',
|
|
54
|
+
intent: lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null,
|
|
55
|
+
ctx: {
|
|
56
|
+
handlers: actionHandlers,
|
|
57
|
+
reset: () => state
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
});
|
|
63
|
+
var keyRef = useRef(options.key);
|
|
64
|
+
var resetKeyRef = useRef(state.resetKey);
|
|
65
|
+
var optionsRef = useLatest(options);
|
|
66
|
+
var lastResultRef = useRef(lastResult);
|
|
67
|
+
var lastIntentedValueRef = useRef();
|
|
68
|
+
var lastAsyncResultRef = useRef(null);
|
|
69
|
+
var abortControllerRef = useRef(null);
|
|
70
|
+
var handleSubmission = useCallback((result, options) => {
|
|
71
|
+
var _optionsRef$current$o, _optionsRef$current;
|
|
72
|
+
var intent = result.submission.intent ? deserializeIntent(result.submission.intent) : null;
|
|
73
|
+
setState(state => updateState(state, _objectSpread2(_objectSpread2({}, result), {}, {
|
|
74
|
+
type: options.type,
|
|
75
|
+
intent,
|
|
76
|
+
ctx: {
|
|
77
|
+
handlers: actionHandlers,
|
|
78
|
+
reset() {
|
|
79
|
+
return initializeState();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})));
|
|
83
|
+
var formElement = getFormElement(formRef);
|
|
84
|
+
if (!formElement || !result.error) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
|
|
88
|
+
formElement,
|
|
89
|
+
error: result.error,
|
|
90
|
+
intent
|
|
91
|
+
});
|
|
92
|
+
}, [formRef, optionsRef]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
return () => {
|
|
95
|
+
var _abortControllerRef$c;
|
|
96
|
+
// Cancel pending validation request
|
|
97
|
+
(_abortControllerRef$c = abortControllerRef.current) === null || _abortControllerRef$c === void 0 || _abortControllerRef$c.abort('The component is unmounted');
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// To avoid re-applying the same result twice
|
|
102
|
+
if (lastResult && lastResult !== lastResultRef.current) {
|
|
103
|
+
handleSubmission(lastResult, {
|
|
104
|
+
type: 'server'
|
|
105
|
+
});
|
|
106
|
+
lastResultRef.current = lastResult;
|
|
107
|
+
}
|
|
108
|
+
}, [lastResult, handleSubmission]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// Reset the form state if the form key changes
|
|
111
|
+
if (options.key !== keyRef.current) {
|
|
112
|
+
keyRef.current = options.key;
|
|
113
|
+
setState(initializeState());
|
|
114
|
+
}
|
|
115
|
+
}, [options.key]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
var formElement = getFormElement(formRef);
|
|
118
|
+
|
|
119
|
+
// Reset the form values if the reset key changes
|
|
120
|
+
if (formElement && state.resetKey !== resetKeyRef.current) {
|
|
121
|
+
resetKeyRef.current = state.resetKey;
|
|
122
|
+
formElement.reset();
|
|
123
|
+
}
|
|
124
|
+
}, [formRef, state.resetKey]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!state.intendedValue) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
var formElement = getFormElement(formRef);
|
|
130
|
+
if (!formElement) {
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.error('Failed to update form value; No form element found');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
updateFormValue(formElement, state.intendedValue, optionsRef.current.serialize);
|
|
136
|
+
lastIntentedValueRef.current = undefined;
|
|
137
|
+
}, [formRef, state.intendedValue, optionsRef]);
|
|
138
|
+
var handleSubmit = useCallback(event => {
|
|
139
|
+
var _abortControllerRef$c2, _lastAsyncResultRef$c;
|
|
140
|
+
var abortController = new AbortController();
|
|
141
|
+
|
|
142
|
+
// Keep track of the abort controller so we can cancel the previous request if a new one is made
|
|
143
|
+
(_abortControllerRef$c2 = abortControllerRef.current) === null || _abortControllerRef$c2 === void 0 || _abortControllerRef$c2.abort('A new submission is made');
|
|
144
|
+
abortControllerRef.current = abortController;
|
|
145
|
+
var formData;
|
|
146
|
+
var result;
|
|
147
|
+
var resolvedValue;
|
|
148
|
+
|
|
149
|
+
// The form might be re-submitted manually if there was an async validation
|
|
150
|
+
if (event.nativeEvent === ((_lastAsyncResultRef$c = lastAsyncResultRef.current) === null || _lastAsyncResultRef$c === void 0 ? void 0 : _lastAsyncResultRef$c.event)) {
|
|
151
|
+
formData = lastAsyncResultRef.current.formData;
|
|
152
|
+
result = lastAsyncResultRef.current.result;
|
|
153
|
+
resolvedValue = lastAsyncResultRef.current.resolvedValue;
|
|
154
|
+
} else {
|
|
155
|
+
var _optionsRef$current$o2, _optionsRef$current2;
|
|
156
|
+
var formElement = event.currentTarget;
|
|
157
|
+
var submitEvent = getSubmitEvent(event);
|
|
158
|
+
formData = getFormData(formElement, submitEvent.submitter);
|
|
159
|
+
var submission = parseSubmission(formData, {
|
|
160
|
+
intentName: optionsRef.current.intentName
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Patch missing fields in the submission object
|
|
164
|
+
for (var element of formElement.elements) {
|
|
165
|
+
if (isFieldElement(element) && element.name) {
|
|
166
|
+
appendUniqueItem(submission.fields, element.name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Override submission value if the last intended value is not applied yet (i.e. batch updates)
|
|
171
|
+
if (typeof lastIntentedValueRef.current !== 'undefined') {
|
|
172
|
+
var _lastIntentedValueRef;
|
|
173
|
+
submission.payload = (_lastIntentedValueRef = lastIntentedValueRef.current) !== null && _lastIntentedValueRef !== void 0 ? _lastIntentedValueRef : {};
|
|
174
|
+
}
|
|
175
|
+
var intendedValue = applyIntent(submission);
|
|
176
|
+
|
|
177
|
+
// Update the last intended value in case there will be another intent dispatched
|
|
178
|
+
lastIntentedValueRef.current = intendedValue === submission.payload ? undefined : intendedValue;
|
|
179
|
+
var submissionResult = report(submission, {
|
|
180
|
+
keepFiles: true,
|
|
181
|
+
intendedValue
|
|
182
|
+
});
|
|
183
|
+
var validateResult =
|
|
184
|
+
// Skip validation on form reset
|
|
185
|
+
intendedValue !== null ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
|
|
186
|
+
payload: intendedValue,
|
|
187
|
+
error: {
|
|
188
|
+
formErrors: [],
|
|
189
|
+
fieldErrors: {}
|
|
190
|
+
},
|
|
191
|
+
intent: submission.intent ? deserializeIntent(submission.intent) : null,
|
|
192
|
+
formElement,
|
|
193
|
+
submitter: submitEvent.submitter,
|
|
194
|
+
formData
|
|
195
|
+
}) : {
|
|
196
|
+
error: null
|
|
197
|
+
};
|
|
198
|
+
var {
|
|
199
|
+
syncResult,
|
|
200
|
+
asyncResult
|
|
201
|
+
} = resolveValidateResult(validateResult);
|
|
202
|
+
if (typeof syncResult !== 'undefined') {
|
|
203
|
+
submissionResult.error = syncResult.error;
|
|
204
|
+
resolvedValue = syncResult.value;
|
|
205
|
+
}
|
|
206
|
+
if (typeof asyncResult !== 'undefined') {
|
|
207
|
+
// Update the form when the validation result is resolved
|
|
208
|
+
asyncResult.then(_ref => {
|
|
209
|
+
var {
|
|
210
|
+
error,
|
|
211
|
+
value
|
|
212
|
+
} = _ref;
|
|
213
|
+
// Update the form with the validation result
|
|
214
|
+
// There is no need to flush the update in this case
|
|
215
|
+
if (!abortController.signal.aborted) {
|
|
216
|
+
submissionResult.error = error;
|
|
217
|
+
handleSubmission(submissionResult, {
|
|
218
|
+
type: 'server'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// If the form is meant to be submitted and there is no error
|
|
222
|
+
if (error === null && !submission.intent) {
|
|
223
|
+
var _event = createSubmitEvent(submitEvent.submitter);
|
|
224
|
+
|
|
225
|
+
// Keep track of the submit event so we can skip validation on the next submit
|
|
226
|
+
lastAsyncResultRef.current = {
|
|
227
|
+
event: _event,
|
|
228
|
+
formData,
|
|
229
|
+
resolvedValue: value,
|
|
230
|
+
result: submissionResult
|
|
231
|
+
};
|
|
232
|
+
formElement.dispatchEvent(_event);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
handleSubmission(submissionResult, {
|
|
238
|
+
type: 'client'
|
|
239
|
+
});
|
|
240
|
+
if (
|
|
241
|
+
// If client validation happens
|
|
242
|
+
(typeof syncResult !== 'undefined' || typeof asyncResult !== 'undefined') && (
|
|
243
|
+
// Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation
|
|
244
|
+
submissionResult.submission.intent || submissionResult.error !== null)) {
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
}
|
|
247
|
+
result = submissionResult;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// We might not prevent form submission if server validation is required
|
|
251
|
+
// But the `onSubmit` handler should be triggered only if there is no intent
|
|
252
|
+
if (!event.isDefaultPrevented() && result.submission.intent === null) {
|
|
253
|
+
var _optionsRef$current$o3, _optionsRef$current3;
|
|
254
|
+
(_optionsRef$current$o3 = (_optionsRef$current3 = optionsRef.current).onSubmit) === null || _optionsRef$current$o3 === void 0 || _optionsRef$current$o3.call(_optionsRef$current3, event, {
|
|
255
|
+
formData,
|
|
256
|
+
get value() {
|
|
257
|
+
if (typeof resolvedValue === 'undefined') {
|
|
258
|
+
throw new Error('`value` is not available; Please make sure you have included the value in the `onValidate` result.');
|
|
259
|
+
}
|
|
260
|
+
return resolvedValue;
|
|
261
|
+
},
|
|
262
|
+
update(options) {
|
|
263
|
+
if (!abortController.signal.aborted) {
|
|
264
|
+
var _submissionResult = report(result.submission, _objectSpread2(_objectSpread2({}, options), {}, {
|
|
265
|
+
keepFiles: true
|
|
266
|
+
}));
|
|
267
|
+
handleSubmission(_submissionResult, {
|
|
268
|
+
type: 'server'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}, [handleSubmission, optionsRef]);
|
|
275
|
+
return [state, handleSubmit];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The main React hook for form management. Handles form state, validation, and submission
|
|
280
|
+
* while providing access to form metadata, field objects, and form actions.
|
|
281
|
+
*
|
|
282
|
+
* @see https://conform.guide/api/react/future/useForm
|
|
283
|
+
* @example
|
|
284
|
+
* ```tsx
|
|
285
|
+
* const { form, fields } = useForm({
|
|
286
|
+
* onValidate({ payload, error }) {
|
|
287
|
+
* if (!payload.email) {
|
|
288
|
+
* error.fieldErrors.email = ['Required'];
|
|
289
|
+
* }
|
|
290
|
+
* return error;
|
|
291
|
+
* }
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* return (
|
|
295
|
+
* <form {...form.props}>
|
|
296
|
+
* <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
|
|
297
|
+
* <div>{fields.email.errors}</div>
|
|
298
|
+
* </form>
|
|
299
|
+
* );
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
function useForm(options) {
|
|
303
|
+
var _optionsRef$current$o4;
|
|
304
|
+
var {
|
|
305
|
+
id,
|
|
306
|
+
defaultValue,
|
|
307
|
+
constraint
|
|
308
|
+
} = options;
|
|
309
|
+
var config = useContext(FormConfig);
|
|
310
|
+
var optionsRef = useLatest(options);
|
|
311
|
+
var fallbackId = useId();
|
|
312
|
+
var formId = id !== null && id !== void 0 ? id : "form-".concat(fallbackId);
|
|
313
|
+
var [state, handleSubmit] = useConform(formId, _objectSpread2(_objectSpread2({}, options), {}, {
|
|
314
|
+
serialize: config.serialize,
|
|
315
|
+
intentName: config.intentName,
|
|
316
|
+
onError: (_optionsRef$current$o4 = optionsRef.current.onError) !== null && _optionsRef$current$o4 !== void 0 ? _optionsRef$current$o4 : focusFirstInvalidField,
|
|
317
|
+
onValidate(ctx) {
|
|
318
|
+
var _options$onValidate, _options$onValidate2;
|
|
319
|
+
if (options.schema) {
|
|
320
|
+
var standardResult = options.schema['~standard'].validate(ctx.payload);
|
|
321
|
+
if (standardResult instanceof Promise) {
|
|
322
|
+
return standardResult.then(actualStandardResult => {
|
|
323
|
+
if (typeof options.onValidate === 'function') {
|
|
324
|
+
throw new Error('The "onValidate" handler is not supported when used with asynchronous schema validation.');
|
|
325
|
+
}
|
|
326
|
+
return resolveStandardSchemaResult(actualStandardResult);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
var resolvedResult = resolveStandardSchemaResult(standardResult);
|
|
330
|
+
if (!options.onValidate) {
|
|
331
|
+
return resolvedResult;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Update the schema error in the context
|
|
335
|
+
if (resolvedResult.error) {
|
|
336
|
+
ctx.error = resolvedResult.error;
|
|
337
|
+
}
|
|
338
|
+
var validateResult = resolveValidateResult(options.onValidate(ctx));
|
|
339
|
+
if (validateResult.syncResult) {
|
|
340
|
+
var _validateResult$syncR, _validateResult$syncR2;
|
|
341
|
+
(_validateResult$syncR2 = (_validateResult$syncR = validateResult.syncResult).value) !== null && _validateResult$syncR2 !== void 0 ? _validateResult$syncR2 : _validateResult$syncR.value = resolvedResult.value;
|
|
342
|
+
}
|
|
343
|
+
if (validateResult.asyncResult) {
|
|
344
|
+
validateResult.asyncResult = validateResult.asyncResult.then(result => {
|
|
345
|
+
var _result$value;
|
|
346
|
+
(_result$value = result.value) !== null && _result$value !== void 0 ? _result$value : result.value = resolvedResult.value;
|
|
347
|
+
return result;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return [validateResult.syncResult, validateResult.asyncResult];
|
|
351
|
+
}
|
|
352
|
+
return (_options$onValidate = (_options$onValidate2 = options.onValidate) === null || _options$onValidate2 === void 0 ? void 0 : _options$onValidate2.call(options, ctx)) !== null && _options$onValidate !== void 0 ? _options$onValidate : {
|
|
353
|
+
// To avoid conform falling back to server validation,
|
|
354
|
+
// if neither schema nor validation handler is provided,
|
|
355
|
+
// we just treat it as a valid client submission
|
|
356
|
+
error: null
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}));
|
|
360
|
+
var intent = useIntent(formId);
|
|
361
|
+
var context = useMemo(() => ({
|
|
362
|
+
formId,
|
|
363
|
+
state,
|
|
364
|
+
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : null,
|
|
365
|
+
constraint: constraint !== null && constraint !== void 0 ? constraint : null,
|
|
366
|
+
handleSubmit: handleSubmit,
|
|
367
|
+
handleInput(event) {
|
|
368
|
+
var _optionsRef$current$o5, _optionsRef$current4;
|
|
369
|
+
if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
(_optionsRef$current$o5 = (_optionsRef$current4 = optionsRef.current).onInput) === null || _optionsRef$current$o5 === void 0 || _optionsRef$current$o5.call(_optionsRef$current4, _objectSpread2(_objectSpread2({}, event), {}, {
|
|
373
|
+
target: event.target,
|
|
374
|
+
currentTarget: event.target.form
|
|
375
|
+
}));
|
|
376
|
+
if (event.defaultPrevented) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
var {
|
|
380
|
+
shouldValidate = 'onSubmit',
|
|
381
|
+
shouldRevalidate = shouldValidate
|
|
382
|
+
} = optionsRef.current;
|
|
383
|
+
if (isTouched(state, event.target.name) ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') {
|
|
384
|
+
intent.validate(event.target.name);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
handleBlur(event) {
|
|
388
|
+
var _optionsRef$current$o6, _optionsRef$current5;
|
|
389
|
+
if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
(_optionsRef$current$o6 = (_optionsRef$current5 = optionsRef.current).onBlur) === null || _optionsRef$current$o6 === void 0 || _optionsRef$current$o6.call(_optionsRef$current5, _objectSpread2(_objectSpread2({}, event), {}, {
|
|
393
|
+
target: event.target,
|
|
394
|
+
currentTarget: event.target.form
|
|
395
|
+
}));
|
|
396
|
+
if (event.defaultPrevented) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
var {
|
|
400
|
+
shouldValidate = 'onSubmit',
|
|
401
|
+
shouldRevalidate = shouldValidate
|
|
402
|
+
} = optionsRef.current;
|
|
403
|
+
if (isTouched(state, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') {
|
|
404
|
+
intent.validate(event.target.name);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}), [formId, state, defaultValue, constraint, handleSubmit, intent, optionsRef]);
|
|
408
|
+
var form = useMemo(() => getFormMetadata(context, {
|
|
409
|
+
serialize: config.serialize
|
|
410
|
+
}), [context, config.serialize]);
|
|
411
|
+
var fields = useMemo(() => getFieldset(context, {
|
|
412
|
+
serialize: config.serialize
|
|
413
|
+
}), [context, config.serialize]);
|
|
414
|
+
return {
|
|
415
|
+
form,
|
|
416
|
+
fields,
|
|
417
|
+
intent
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* A React hook that provides access to form-level metadata and state.
|
|
423
|
+
* Requires `FormProvider` context when used in child components.
|
|
424
|
+
*
|
|
425
|
+
* @see https://conform.guide/api/react/future/useFormMetadata
|
|
426
|
+
* @example
|
|
427
|
+
* ```tsx
|
|
428
|
+
* function ErrorSummary() {
|
|
429
|
+
* const form = useFormMetadata();
|
|
430
|
+
*
|
|
431
|
+
* if (!form.invalid) return null;
|
|
432
|
+
*
|
|
433
|
+
* return (
|
|
434
|
+
* <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
|
|
435
|
+
* );
|
|
436
|
+
* }
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
function useFormMetadata() {
|
|
440
|
+
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
441
|
+
var config = useContext(FormConfig);
|
|
442
|
+
var context = useFormContext(options.formId);
|
|
443
|
+
var formMetadata = useMemo(() => getFormMetadata(context, {
|
|
444
|
+
serialize: config.serialize
|
|
445
|
+
}), [context, config.serialize]);
|
|
446
|
+
return formMetadata;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A React hook that provides access to a specific field's metadata and state.
|
|
451
|
+
* Requires `FormProvider` context when used in child components.
|
|
452
|
+
*
|
|
453
|
+
* @see https://conform.guide/api/react/future/useField
|
|
454
|
+
* @example
|
|
455
|
+
* ```tsx
|
|
456
|
+
* function FormField({ name, label }) {
|
|
457
|
+
* const field = useField(name);
|
|
458
|
+
*
|
|
459
|
+
* return (
|
|
460
|
+
* <div>
|
|
461
|
+
* <label htmlFor={field.id}>{label}</label>
|
|
462
|
+
* <input id={field.id} name={field.name} defaultValue={field.defaultValue} />
|
|
463
|
+
* {field.errors && <div>{field.errors.join(', ')}</div>}
|
|
464
|
+
* </div>
|
|
465
|
+
* );
|
|
466
|
+
* }
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
function useField(name) {
|
|
470
|
+
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
471
|
+
var config = useContext(FormConfig);
|
|
472
|
+
var context = useFormContext(options.formId);
|
|
473
|
+
var field = useMemo(() => getField(context, {
|
|
474
|
+
name,
|
|
475
|
+
serialize: config.serialize
|
|
476
|
+
}), [context, name, config.serialize]);
|
|
477
|
+
return field;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* A React hook that provides an intent dispatcher for programmatic form actions.
|
|
482
|
+
* Intent dispatchers allow you to trigger form operations like validation, field updates,
|
|
483
|
+
* and array manipulations without manual form submission.
|
|
484
|
+
*
|
|
485
|
+
* @see https://conform.guide/api/react/future/useIntent
|
|
486
|
+
* @example
|
|
487
|
+
* ```tsx
|
|
488
|
+
* function ResetButton() {
|
|
489
|
+
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
490
|
+
* const intent = useIntent(buttonRef);
|
|
491
|
+
*
|
|
492
|
+
* return (
|
|
493
|
+
* <button type="button" ref={buttonRef} onClick={() => intent.reset()}>
|
|
494
|
+
* Reset Form
|
|
495
|
+
* </button>
|
|
496
|
+
* );
|
|
497
|
+
* }
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
function useIntent(formRef) {
|
|
501
|
+
var config = useContext(FormConfig);
|
|
502
|
+
return useMemo(() => createIntentDispatcher(() => getFormElement(formRef), config.intentName), [formRef, config.intentName]);
|
|
503
|
+
}
|
|
5
504
|
|
|
6
505
|
/**
|
|
7
506
|
* A React hook that lets you sync the state of an input and dispatch native form events from it.
|
|
@@ -16,10 +515,10 @@ import { FormContext } from './context.mjs';
|
|
|
16
515
|
function useControl(options) {
|
|
17
516
|
var {
|
|
18
517
|
observer
|
|
19
|
-
} = useContext(
|
|
518
|
+
} = useContext(FormConfig);
|
|
20
519
|
var inputRef = useRef(null);
|
|
21
520
|
var eventDispatched = useRef({});
|
|
22
|
-
var defaultSnapshot =
|
|
521
|
+
var defaultSnapshot = createDefaultSnapshot(options === null || options === void 0 ? void 0 : options.defaultValue, options === null || options === void 0 ? void 0 : options.defaultChecked, options === null || options === void 0 ? void 0 : options.value);
|
|
23
522
|
var snapshotRef = useRef(defaultSnapshot);
|
|
24
523
|
var optionsRef = useRef(options);
|
|
25
524
|
useEffect(() => {
|
|
@@ -41,7 +540,7 @@ function useControl(options) {
|
|
|
41
540
|
value: getRadioGroupValue(input),
|
|
42
541
|
options: getCheckboxGroupValue(input)
|
|
43
542
|
} : getInputSnapshot(input);
|
|
44
|
-
if (
|
|
543
|
+
if (deepEqual(prev, next)) {
|
|
45
544
|
return prev;
|
|
46
545
|
}
|
|
47
546
|
snapshotRef.current = next;
|
|
@@ -59,8 +558,8 @@ function useControl(options) {
|
|
|
59
558
|
eventDispatched.current[listener] = undefined;
|
|
60
559
|
});
|
|
61
560
|
if (listener === 'focus') {
|
|
62
|
-
var _optionsRef$
|
|
63
|
-
(_optionsRef$
|
|
561
|
+
var _optionsRef$current6, _optionsRef$current6$;
|
|
562
|
+
(_optionsRef$current6 = optionsRef.current) === null || _optionsRef$current6 === void 0 || (_optionsRef$current6$ = _optionsRef$current6.onFocus) === null || _optionsRef$current6$ === void 0 || _optionsRef$current6$.call(_optionsRef$current6);
|
|
64
563
|
}
|
|
65
564
|
}
|
|
66
565
|
};
|
|
@@ -88,14 +587,14 @@ function useControl(options) {
|
|
|
88
587
|
} else if (isFieldElement(element)) {
|
|
89
588
|
inputRef.current = element;
|
|
90
589
|
if (shouldHandleFocus) {
|
|
91
|
-
|
|
590
|
+
makeInputFocusable(element);
|
|
92
591
|
}
|
|
93
592
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
94
|
-
var _optionsRef$current$v, _optionsRef$
|
|
593
|
+
var _optionsRef$current$v, _optionsRef$current7;
|
|
95
594
|
// React set the value as empty string incorrectly when the value is undefined
|
|
96
595
|
// This make sure the checkbox value falls back to the default value "on" properly
|
|
97
596
|
// @see https://github.com/facebook/react/issues/17590
|
|
98
|
-
element.value = (_optionsRef$current$v = (_optionsRef$
|
|
597
|
+
element.value = (_optionsRef$current$v = (_optionsRef$current7 = optionsRef.current) === null || _optionsRef$current7 === void 0 ? void 0 : _optionsRef$current7.value) !== null && _optionsRef$current$v !== void 0 ? _optionsRef$current$v : 'on';
|
|
99
598
|
}
|
|
100
599
|
initializeField(element, optionsRef.current);
|
|
101
600
|
} else {
|
|
@@ -108,13 +607,13 @@ function useControl(options) {
|
|
|
108
607
|
}
|
|
109
608
|
inputRef.current = inputs;
|
|
110
609
|
for (var input of inputs) {
|
|
111
|
-
var _optionsRef$
|
|
610
|
+
var _optionsRef$current8;
|
|
112
611
|
if (shouldHandleFocus) {
|
|
113
|
-
|
|
612
|
+
makeInputFocusable(input);
|
|
114
613
|
}
|
|
115
614
|
initializeField(input, {
|
|
116
615
|
// We will not be uitlizing defaultChecked / value on checkbox / radio group
|
|
117
|
-
defaultValue: (_optionsRef$
|
|
616
|
+
defaultValue: (_optionsRef$current8 = optionsRef.current) === null || _optionsRef$current8 === void 0 ? void 0 : _optionsRef$current8.defaultValue
|
|
118
617
|
});
|
|
119
618
|
}
|
|
120
619
|
}
|
|
@@ -122,7 +621,7 @@ function useControl(options) {
|
|
|
122
621
|
change: useCallback(value => {
|
|
123
622
|
if (!eventDispatched.current.change) {
|
|
124
623
|
var _inputRef$current;
|
|
125
|
-
var
|
|
624
|
+
var element = Array.isArray(inputRef.current) ? (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.find(input => {
|
|
126
625
|
var wasChecked = input.checked;
|
|
127
626
|
var isChecked = Array.isArray(value) ? value.some(item => item === input.value) : input.value === value;
|
|
128
627
|
switch (input.type) {
|
|
@@ -138,8 +637,8 @@ function useControl(options) {
|
|
|
138
637
|
return false;
|
|
139
638
|
}
|
|
140
639
|
}) : inputRef.current;
|
|
141
|
-
if (
|
|
142
|
-
|
|
640
|
+
if (element) {
|
|
641
|
+
change(element, typeof value === 'boolean' ? value ? element.value : null : value);
|
|
143
642
|
}
|
|
144
643
|
}
|
|
145
644
|
if (eventDispatched.current.change) {
|
|
@@ -149,9 +648,9 @@ function useControl(options) {
|
|
|
149
648
|
}, []),
|
|
150
649
|
focus: useCallback(() => {
|
|
151
650
|
if (!eventDispatched.current.focus) {
|
|
152
|
-
var
|
|
153
|
-
if (
|
|
154
|
-
|
|
651
|
+
var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
|
|
652
|
+
if (element) {
|
|
653
|
+
focus(element);
|
|
155
654
|
}
|
|
156
655
|
}
|
|
157
656
|
if (eventDispatched.current.focus) {
|
|
@@ -161,9 +660,9 @@ function useControl(options) {
|
|
|
161
660
|
}, []),
|
|
162
661
|
blur: useCallback(() => {
|
|
163
662
|
if (!eventDispatched.current.blur) {
|
|
164
|
-
var
|
|
165
|
-
if (
|
|
166
|
-
|
|
663
|
+
var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
|
|
664
|
+
if (element) {
|
|
665
|
+
blur(element);
|
|
167
666
|
}
|
|
168
667
|
}
|
|
169
668
|
if (eventDispatched.current.blur) {
|
|
@@ -188,23 +687,23 @@ function useControl(options) {
|
|
|
188
687
|
function useFormData(formRef, select, options) {
|
|
189
688
|
var {
|
|
190
689
|
observer
|
|
191
|
-
} = useContext(
|
|
690
|
+
} = useContext(FormConfig);
|
|
192
691
|
var valueRef = useRef();
|
|
193
692
|
var formDataRef = useRef(null);
|
|
194
693
|
var value = useSyncExternalStore(useCallback(callback => {
|
|
195
694
|
var formElement = getFormElement(formRef);
|
|
196
695
|
if (formElement) {
|
|
197
|
-
var
|
|
198
|
-
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ?
|
|
199
|
-
var [key, value] =
|
|
696
|
+
var formData = getFormData(formElement);
|
|
697
|
+
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? formData : new URLSearchParams(Array.from(formData).map(_ref2 => {
|
|
698
|
+
var [key, value] = _ref2;
|
|
200
699
|
return [key, value.toString()];
|
|
201
700
|
}));
|
|
202
701
|
}
|
|
203
702
|
var unsubscribe = observer.onFormUpdate(event => {
|
|
204
703
|
if (event.target === getFormElement(formRef)) {
|
|
205
|
-
var
|
|
206
|
-
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ?
|
|
207
|
-
var [key, value] =
|
|
704
|
+
var _formData = getFormData(event.target, event.submitter);
|
|
705
|
+
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData : new URLSearchParams(Array.from(_formData).map(_ref3 => {
|
|
706
|
+
var [key, value] = _ref3;
|
|
208
707
|
return [key, value.toString()];
|
|
209
708
|
}));
|
|
210
709
|
callback();
|
|
@@ -214,7 +713,7 @@ function useFormData(formRef, select, options) {
|
|
|
214
713
|
}, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
|
|
215
714
|
// @ts-expect-error FIXME
|
|
216
715
|
var result = select(formDataRef.current, valueRef.current);
|
|
217
|
-
if (typeof valueRef.current !== 'undefined' &&
|
|
716
|
+
if (typeof valueRef.current !== 'undefined' && deepEqual(result, valueRef.current)) {
|
|
218
717
|
return valueRef.current;
|
|
219
718
|
}
|
|
220
719
|
valueRef.current = result;
|
|
@@ -223,4 +722,22 @@ function useFormData(formRef, select, options) {
|
|
|
223
722
|
return value;
|
|
224
723
|
}
|
|
225
724
|
|
|
226
|
-
|
|
725
|
+
/**
|
|
726
|
+
* useLayoutEffect is client-only.
|
|
727
|
+
* This basically makes it a no-op on server
|
|
728
|
+
*/
|
|
729
|
+
var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Keep a mutable ref in sync with the latest value.
|
|
733
|
+
* Useful to avoid stale closures in event handlers or async callbacks.
|
|
734
|
+
*/
|
|
735
|
+
function useLatest(value) {
|
|
736
|
+
var ref = useRef(value);
|
|
737
|
+
useSafeLayoutEffect(() => {
|
|
738
|
+
ref.current = value;
|
|
739
|
+
}, [value]);
|
|
740
|
+
return ref;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export { Form, FormConfig, FormProvider, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect };
|