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