@conform-to/react 1.15.1 → 1.17.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/context.js +1 -0
- package/dist/context.mjs +1 -0
- package/dist/future/dom.d.ts +10 -0
- package/dist/future/dom.js +126 -0
- package/dist/future/dom.mjs +125 -1
- package/dist/future/forms.d.ts +43 -0
- package/dist/future/forms.js +323 -0
- package/dist/future/forms.mjs +319 -0
- package/dist/future/hooks.d.ts +44 -4
- package/dist/future/hooks.js +96 -34
- package/dist/future/hooks.mjs +100 -39
- package/dist/future/index.d.ts +4 -2
- package/dist/future/index.js +5 -0
- package/dist/future/index.mjs +3 -1
- package/dist/future/intent.d.ts +13 -6
- package/dist/future/intent.js +175 -68
- package/dist/future/intent.mjs +175 -69
- package/dist/future/state.d.ts +30 -14
- package/dist/future/state.js +44 -38
- package/dist/future/state.mjs +46 -40
- package/dist/future/types.d.ts +310 -36
- package/dist/future/util.d.ts +44 -1
- package/dist/future/util.js +67 -5
- package/dist/future/util.mjs +64 -6
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +2 -1
- package/dist/helpers.mjs +2 -1
- package/dist/integrations.d.ts +3 -3
- package/package.json +2 -2
package/dist/future/hooks.d.ts
CHANGED
|
@@ -15,6 +15,28 @@ export declare function FormProvider(props: {
|
|
|
15
15
|
context: FormContext;
|
|
16
16
|
children: React.ReactNode;
|
|
17
17
|
}): React.ReactElement;
|
|
18
|
+
/**
|
|
19
|
+
* Preserves form field values when its contents are unmounted.
|
|
20
|
+
* Useful for multi-step forms and virtualized lists.
|
|
21
|
+
*
|
|
22
|
+
* @see https://conform.guide/api/react/future/PreserveBoundary
|
|
23
|
+
*/
|
|
24
|
+
export declare function PreserveBoundary(props: {
|
|
25
|
+
/**
|
|
26
|
+
* A unique name for the boundary within the form. Used to ensure proper
|
|
27
|
+
* unmount/remount behavior and to isolate preserved inputs between boundaries.
|
|
28
|
+
*/
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* The id of the form to associate with. Only needed when the boundary
|
|
32
|
+
* is rendered outside the form element.
|
|
33
|
+
*/
|
|
34
|
+
form?: string;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}): React.ReactElement;
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`.
|
|
39
|
+
*/
|
|
18
40
|
export declare function FormOptionsProvider(props: Partial<GlobalFormOptions> & {
|
|
19
41
|
children: React.ReactNode;
|
|
20
42
|
}): React.ReactElement;
|
|
@@ -206,18 +228,36 @@ export declare function useControl(options?: {
|
|
|
206
228
|
* A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
|
|
207
229
|
* The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
|
|
208
230
|
*
|
|
231
|
+
* Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
|
|
232
|
+
* unless a `fallback` is provided.
|
|
233
|
+
*
|
|
209
234
|
* @see https://conform.guide/api/react/future/useFormData
|
|
210
235
|
* @example
|
|
211
236
|
* ```ts
|
|
212
|
-
* const value = useFormData(
|
|
237
|
+
* const value = useFormData(
|
|
238
|
+
* formRef,
|
|
239
|
+
* formData => formData.get('fieldName') ?? '',
|
|
240
|
+
* );
|
|
213
241
|
* ```
|
|
214
242
|
*/
|
|
215
|
-
export declare function useFormData<Value
|
|
243
|
+
export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions<Value> & {
|
|
216
244
|
acceptFiles: true;
|
|
245
|
+
fallback: Value;
|
|
217
246
|
}): Value;
|
|
218
|
-
export declare function useFormData<Value
|
|
219
|
-
acceptFiles
|
|
247
|
+
export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions & {
|
|
248
|
+
acceptFiles: true;
|
|
249
|
+
}): Value | undefined;
|
|
250
|
+
export declare function useFormData<Value>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options: UseFormDataOptions<Value> & {
|
|
251
|
+
acceptFiles?: false;
|
|
252
|
+
fallback: Value;
|
|
220
253
|
}): Value;
|
|
254
|
+
export declare function useFormData<Value>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
|
|
255
|
+
acceptFiles?: false;
|
|
256
|
+
}): Value | undefined;
|
|
257
|
+
export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value> | Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
|
|
258
|
+
acceptFiles?: boolean;
|
|
259
|
+
fallback?: Value;
|
|
260
|
+
}): Value | undefined;
|
|
221
261
|
/**
|
|
222
262
|
* useLayoutEffect is client-only.
|
|
223
263
|
* This basically makes it a no-op on server
|
package/dist/future/hooks.js
CHANGED
|
@@ -38,6 +38,51 @@ function FormProvider(props) {
|
|
|
38
38
|
children: props.children
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Preserves form field values when its contents are unmounted.
|
|
44
|
+
* Useful for multi-step forms and virtualized lists.
|
|
45
|
+
*
|
|
46
|
+
* @see https://conform.guide/api/react/future/PreserveBoundary
|
|
47
|
+
*/
|
|
48
|
+
function PreserveBoundary(props) {
|
|
49
|
+
// name is used as key so React properly unmounts/remounts when switching
|
|
50
|
+
// between boundaries. Without it, both sides of a ternary share
|
|
51
|
+
// key={undefined} and React reuses the instance (useId and key prop
|
|
52
|
+
// can't help here). This is why name is required.
|
|
53
|
+
return /*#__PURE__*/jsxRuntime.jsx(PreserveBoundaryImpl, _rollupPluginBabelHelpers.objectSpread2({}, props), props.name);
|
|
54
|
+
}
|
|
55
|
+
function PreserveBoundaryImpl(props) {
|
|
56
|
+
var fieldsetRef = react.useRef(null);
|
|
57
|
+
|
|
58
|
+
// useLayoutEffect to restore values before paint, avoiding flash of default values
|
|
59
|
+
useSafeLayoutEffect(() => {
|
|
60
|
+
var fieldset = fieldsetRef.current;
|
|
61
|
+
if (!fieldset || !fieldset.form) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
var form = fieldset.form;
|
|
65
|
+
|
|
66
|
+
// On mount: restore values from preserved inputs
|
|
67
|
+
dom.cleanupPreservedInputs(fieldset, form, props.name);
|
|
68
|
+
return () => {
|
|
69
|
+
// On unmount: preserve input values
|
|
70
|
+
dom.preserveInputs(fieldset.querySelectorAll('input,select,textarea'), form, props.name);
|
|
71
|
+
};
|
|
72
|
+
}, [props.name]);
|
|
73
|
+
return /*#__PURE__*/jsxRuntime.jsx("fieldset", {
|
|
74
|
+
ref: fieldsetRef,
|
|
75
|
+
form: props.form,
|
|
76
|
+
style: {
|
|
77
|
+
display: 'contents'
|
|
78
|
+
},
|
|
79
|
+
children: props.children
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`.
|
|
85
|
+
*/
|
|
41
86
|
function FormOptionsProvider(props) {
|
|
42
87
|
var {
|
|
43
88
|
children
|
|
@@ -73,11 +118,16 @@ function useConform(formRef, options) {
|
|
|
73
118
|
resetKey: INITIAL_KEY
|
|
74
119
|
});
|
|
75
120
|
if (lastResult) {
|
|
76
|
-
|
|
121
|
+
var intent$1 = lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null;
|
|
122
|
+
var result = intent.applyIntent(lastResult, intent$1, {
|
|
123
|
+
handlers: intent.intentHandlers
|
|
124
|
+
});
|
|
125
|
+
state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
|
|
77
126
|
type: 'initialize',
|
|
78
|
-
intent:
|
|
127
|
+
intent: intent$1,
|
|
79
128
|
ctx: {
|
|
80
|
-
handlers: intent.
|
|
129
|
+
handlers: intent.intentHandlers,
|
|
130
|
+
cancelled: result !== lastResult,
|
|
81
131
|
reset: defaultValue => state.initializeState({
|
|
82
132
|
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue,
|
|
83
133
|
resetKey: INITIAL_KEY
|
|
@@ -95,14 +145,17 @@ function useConform(formRef, options) {
|
|
|
95
145
|
var lastAsyncResultRef = react.useRef(null);
|
|
96
146
|
var abortControllerRef = react.useRef(null);
|
|
97
147
|
var handleSubmission = react.useCallback(function (type, result) {
|
|
98
|
-
var _optionsRef$current$o, _optionsRef$current;
|
|
99
148
|
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current;
|
|
100
149
|
var intent$1 = result.submission.intent ? intent.deserializeIntent(result.submission.intent) : null;
|
|
101
|
-
|
|
150
|
+
var finalResult = intent.applyIntent(result, intent$1, {
|
|
151
|
+
handlers: intent.intentHandlers
|
|
152
|
+
});
|
|
153
|
+
setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, finalResult), {}, {
|
|
102
154
|
type,
|
|
103
155
|
intent: intent$1,
|
|
104
156
|
ctx: {
|
|
105
|
-
handlers: intent.
|
|
157
|
+
handlers: intent.intentHandlers,
|
|
158
|
+
cancelled: finalResult !== result,
|
|
106
159
|
reset(defaultValue) {
|
|
107
160
|
return state.initializeState({
|
|
108
161
|
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue
|
|
@@ -113,14 +166,15 @@ function useConform(formRef, options) {
|
|
|
113
166
|
|
|
114
167
|
// TODO: move on error handler to a new effect
|
|
115
168
|
var formElement = dom.getFormElement(formRef);
|
|
116
|
-
if (
|
|
117
|
-
|
|
169
|
+
if (formElement && result.error) {
|
|
170
|
+
var _optionsRef$current$o, _optionsRef$current;
|
|
171
|
+
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
|
|
172
|
+
formElement,
|
|
173
|
+
error: result.error,
|
|
174
|
+
intent: intent$1
|
|
175
|
+
});
|
|
118
176
|
}
|
|
119
|
-
|
|
120
|
-
formElement,
|
|
121
|
-
error: result.error,
|
|
122
|
-
intent: intent$1
|
|
123
|
-
});
|
|
177
|
+
return finalResult;
|
|
124
178
|
}, [formRef, optionsRef]);
|
|
125
179
|
if (options.key !== keyRef.current) {
|
|
126
180
|
keyRef.current = options.key;
|
|
@@ -196,17 +250,11 @@ function useConform(formRef, options) {
|
|
|
196
250
|
if (pendingValueRef.current !== undefined) {
|
|
197
251
|
submission.payload = pendingValueRef.current;
|
|
198
252
|
}
|
|
199
|
-
var value = intent.
|
|
253
|
+
var value = intent.resolveIntent(submission);
|
|
200
254
|
var submissionResult = future.report(submission, {
|
|
201
255
|
keepFiles: true,
|
|
202
256
|
value
|
|
203
257
|
});
|
|
204
|
-
|
|
205
|
-
// If there is target value, keep track of it as pending value
|
|
206
|
-
if (submission.payload !== value) {
|
|
207
|
-
var _ref;
|
|
208
|
-
pendingValueRef.current = (_ref = value !== null && value !== void 0 ? value : optionsRef.current.defaultValue) !== null && _ref !== void 0 ? _ref : {};
|
|
209
|
-
}
|
|
210
258
|
var validateResult =
|
|
211
259
|
// Skip validation on form reset
|
|
212
260
|
value !== undefined ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
|
|
@@ -233,11 +281,11 @@ function useConform(formRef, options) {
|
|
|
233
281
|
}
|
|
234
282
|
if (typeof asyncResult !== 'undefined') {
|
|
235
283
|
// Update the form when the validation result is resolved
|
|
236
|
-
asyncResult.then(
|
|
284
|
+
asyncResult.then(_ref => {
|
|
237
285
|
var {
|
|
238
286
|
error,
|
|
239
287
|
value
|
|
240
|
-
} =
|
|
288
|
+
} = _ref;
|
|
241
289
|
// Update the form with the validation result
|
|
242
290
|
// There is no need to flush the update in this case
|
|
243
291
|
if (!abortController.signal.aborted) {
|
|
@@ -245,7 +293,7 @@ function useConform(formRef, options) {
|
|
|
245
293
|
handleSubmission('server', submissionResult);
|
|
246
294
|
|
|
247
295
|
// If the form is meant to be submitted and there is no error
|
|
248
|
-
if (error === null && !submission.intent) {
|
|
296
|
+
if (submissionResult.error === null && !submission.intent) {
|
|
249
297
|
var _event = future.createSubmitEvent(submitEvent.submitter);
|
|
250
298
|
|
|
251
299
|
// Keep track of the submit event so we can skip validation on the next submit
|
|
@@ -260,15 +308,19 @@ function useConform(formRef, options) {
|
|
|
260
308
|
}
|
|
261
309
|
});
|
|
262
310
|
}
|
|
263
|
-
handleSubmission('client', submissionResult);
|
|
311
|
+
var clientResult = handleSubmission('client', submissionResult);
|
|
312
|
+
if (clientResult.reset || clientResult.targetValue !== undefined) {
|
|
313
|
+
var _ref2, _clientResult$targetV;
|
|
314
|
+
pendingValueRef.current = (_ref2 = (_clientResult$targetV = clientResult.targetValue) !== null && _clientResult$targetV !== void 0 ? _clientResult$targetV : optionsRef.current.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {};
|
|
315
|
+
}
|
|
264
316
|
if (
|
|
265
317
|
// If client validation happens
|
|
266
318
|
(typeof syncResult !== 'undefined' || typeof asyncResult !== 'undefined') && (
|
|
267
319
|
// Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation
|
|
268
|
-
|
|
320
|
+
clientResult.submission.intent || clientResult.error !== null)) {
|
|
269
321
|
event.preventDefault();
|
|
270
322
|
}
|
|
271
|
-
result =
|
|
323
|
+
result = clientResult;
|
|
272
324
|
}
|
|
273
325
|
|
|
274
326
|
// We might not prevent form submission if server validation is required
|
|
@@ -465,11 +517,11 @@ function useForm(schemaOrOptions, maybeOptions) {
|
|
|
465
517
|
}), [formId, state$1, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
|
|
466
518
|
var form = react.useMemo(() => state.getFormMetadata(context, {
|
|
467
519
|
serialize: globalOptions.serialize,
|
|
468
|
-
|
|
520
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
469
521
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
470
522
|
var fields = react.useMemo(() => state.getFieldset(context, {
|
|
471
523
|
serialize: globalOptions.serialize,
|
|
472
|
-
|
|
524
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
473
525
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
474
526
|
return {
|
|
475
527
|
form,
|
|
@@ -502,7 +554,7 @@ function useFormMetadata() {
|
|
|
502
554
|
var context = useFormContext(options.formId);
|
|
503
555
|
var formMetadata = react.useMemo(() => state.getFormMetadata(context, {
|
|
504
556
|
serialize: globalOptions.serialize,
|
|
505
|
-
|
|
557
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
506
558
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
507
559
|
return formMetadata;
|
|
508
560
|
}
|
|
@@ -534,7 +586,7 @@ function useField(name) {
|
|
|
534
586
|
var field = react.useMemo(() => state.getField(context, {
|
|
535
587
|
name,
|
|
536
588
|
serialize: globalOptions.serialize,
|
|
537
|
-
|
|
589
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
538
590
|
}), [context, name, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
539
591
|
return field;
|
|
540
592
|
}
|
|
@@ -757,10 +809,16 @@ function useControl(options) {
|
|
|
757
809
|
* A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
|
|
758
810
|
* The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
|
|
759
811
|
*
|
|
812
|
+
* Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
|
|
813
|
+
* unless a `fallback` is provided.
|
|
814
|
+
*
|
|
760
815
|
* @see https://conform.guide/api/react/future/useFormData
|
|
761
816
|
* @example
|
|
762
817
|
* ```ts
|
|
763
|
-
* const value = useFormData(
|
|
818
|
+
* const value = useFormData(
|
|
819
|
+
* formRef,
|
|
820
|
+
* formData => formData.get('fieldName') ?? '',
|
|
821
|
+
* );
|
|
764
822
|
* ```
|
|
765
823
|
*/
|
|
766
824
|
|
|
@@ -769,7 +827,7 @@ function useFormData(formRef, select, options) {
|
|
|
769
827
|
observer
|
|
770
828
|
} = react.useContext(GlobalFormOptionsContext);
|
|
771
829
|
var valueRef = react.useRef();
|
|
772
|
-
var formDataRef = react.useRef(
|
|
830
|
+
var formDataRef = react.useRef();
|
|
773
831
|
var value = react.useSyncExternalStore(react.useCallback(callback => {
|
|
774
832
|
var formElement = dom.getFormElement(formRef);
|
|
775
833
|
if (formElement) {
|
|
@@ -791,14 +849,17 @@ function useFormData(formRef, select, options) {
|
|
|
791
849
|
});
|
|
792
850
|
return unsubscribe;
|
|
793
851
|
}, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
|
|
794
|
-
//
|
|
852
|
+
// Return fallback if form is not available
|
|
853
|
+
if (formDataRef.current === undefined) {
|
|
854
|
+
return options === null || options === void 0 ? void 0 : options.fallback;
|
|
855
|
+
}
|
|
795
856
|
var result = select(formDataRef.current, valueRef.current);
|
|
796
857
|
if (typeof valueRef.current !== 'undefined' && future.deepEqual(result, valueRef.current)) {
|
|
797
858
|
return valueRef.current;
|
|
798
859
|
}
|
|
799
860
|
valueRef.current = result;
|
|
800
861
|
return result;
|
|
801
|
-
}, () =>
|
|
862
|
+
}, () => options === null || options === void 0 ? void 0 : options.fallback);
|
|
802
863
|
return value;
|
|
803
864
|
}
|
|
804
865
|
|
|
@@ -825,6 +886,7 @@ exports.FormOptionsProvider = FormOptionsProvider;
|
|
|
825
886
|
exports.FormProvider = FormProvider;
|
|
826
887
|
exports.GlobalFormOptionsContext = GlobalFormOptionsContext;
|
|
827
888
|
exports.INITIAL_KEY = INITIAL_KEY;
|
|
889
|
+
exports.PreserveBoundary = PreserveBoundary;
|
|
828
890
|
exports.useConform = useConform;
|
|
829
891
|
exports.useControl = useControl;
|
|
830
892
|
exports.useField = useField;
|
package/dist/future/hooks.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { objectSpread2 as _objectSpread2, objectWithoutProperties as _objectWithoutProperties } from '../_virtual/_rollupPluginBabelHelpers.mjs';
|
|
3
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,
|
|
4
|
+
import { createContext, useContext, useMemo, useRef, useId, useEffect, useSyncExternalStore, useCallback, useLayoutEffect, useState } from 'react';
|
|
5
5
|
import { resolveStandardSchemaResult, resolveValidateResult, appendUniqueItem } from './util.mjs';
|
|
6
6
|
import { isTouched, getFormMetadata, getFieldset, getField, initializeState, updateState } from './state.mjs';
|
|
7
|
-
import { deserializeIntent,
|
|
8
|
-
import { focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, resetFormValue, updateFormValue, getSubmitEvent } from './dom.mjs';
|
|
7
|
+
import { deserializeIntent, applyIntent, intentHandlers, resolveIntent } from './intent.mjs';
|
|
8
|
+
import { cleanupPreservedInputs, preserveInputs, focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, resetFormValue, updateFormValue, getSubmitEvent } from './dom.mjs';
|
|
9
9
|
import { jsx } from 'react/jsx-runtime';
|
|
10
10
|
|
|
11
11
|
var _excluded = ["children"];
|
|
@@ -34,6 +34,51 @@ function FormProvider(props) {
|
|
|
34
34
|
children: props.children
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Preserves form field values when its contents are unmounted.
|
|
40
|
+
* Useful for multi-step forms and virtualized lists.
|
|
41
|
+
*
|
|
42
|
+
* @see https://conform.guide/api/react/future/PreserveBoundary
|
|
43
|
+
*/
|
|
44
|
+
function PreserveBoundary(props) {
|
|
45
|
+
// name is used as key so React properly unmounts/remounts when switching
|
|
46
|
+
// between boundaries. Without it, both sides of a ternary share
|
|
47
|
+
// key={undefined} and React reuses the instance (useId and key prop
|
|
48
|
+
// can't help here). This is why name is required.
|
|
49
|
+
return /*#__PURE__*/jsx(PreserveBoundaryImpl, _objectSpread2({}, props), props.name);
|
|
50
|
+
}
|
|
51
|
+
function PreserveBoundaryImpl(props) {
|
|
52
|
+
var fieldsetRef = useRef(null);
|
|
53
|
+
|
|
54
|
+
// useLayoutEffect to restore values before paint, avoiding flash of default values
|
|
55
|
+
useSafeLayoutEffect(() => {
|
|
56
|
+
var fieldset = fieldsetRef.current;
|
|
57
|
+
if (!fieldset || !fieldset.form) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
var form = fieldset.form;
|
|
61
|
+
|
|
62
|
+
// On mount: restore values from preserved inputs
|
|
63
|
+
cleanupPreservedInputs(fieldset, form, props.name);
|
|
64
|
+
return () => {
|
|
65
|
+
// On unmount: preserve input values
|
|
66
|
+
preserveInputs(fieldset.querySelectorAll('input,select,textarea'), form, props.name);
|
|
67
|
+
};
|
|
68
|
+
}, [props.name]);
|
|
69
|
+
return /*#__PURE__*/jsx("fieldset", {
|
|
70
|
+
ref: fieldsetRef,
|
|
71
|
+
form: props.form,
|
|
72
|
+
style: {
|
|
73
|
+
display: 'contents'
|
|
74
|
+
},
|
|
75
|
+
children: props.children
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`.
|
|
81
|
+
*/
|
|
37
82
|
function FormOptionsProvider(props) {
|
|
38
83
|
var {
|
|
39
84
|
children
|
|
@@ -69,11 +114,16 @@ function useConform(formRef, options) {
|
|
|
69
114
|
resetKey: INITIAL_KEY
|
|
70
115
|
});
|
|
71
116
|
if (lastResult) {
|
|
72
|
-
|
|
117
|
+
var intent = lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null;
|
|
118
|
+
var result = applyIntent(lastResult, intent, {
|
|
119
|
+
handlers: intentHandlers
|
|
120
|
+
});
|
|
121
|
+
state = updateState(state, _objectSpread2(_objectSpread2({}, result), {}, {
|
|
73
122
|
type: 'initialize',
|
|
74
|
-
intent
|
|
123
|
+
intent,
|
|
75
124
|
ctx: {
|
|
76
|
-
handlers:
|
|
125
|
+
handlers: intentHandlers,
|
|
126
|
+
cancelled: result !== lastResult,
|
|
77
127
|
reset: defaultValue => initializeState({
|
|
78
128
|
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue,
|
|
79
129
|
resetKey: INITIAL_KEY
|
|
@@ -91,14 +141,17 @@ function useConform(formRef, options) {
|
|
|
91
141
|
var lastAsyncResultRef = useRef(null);
|
|
92
142
|
var abortControllerRef = useRef(null);
|
|
93
143
|
var handleSubmission = useCallback(function (type, result) {
|
|
94
|
-
var _optionsRef$current$o, _optionsRef$current;
|
|
95
144
|
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current;
|
|
96
145
|
var intent = result.submission.intent ? deserializeIntent(result.submission.intent) : null;
|
|
97
|
-
|
|
146
|
+
var finalResult = applyIntent(result, intent, {
|
|
147
|
+
handlers: intentHandlers
|
|
148
|
+
});
|
|
149
|
+
setState(state => updateState(state, _objectSpread2(_objectSpread2({}, finalResult), {}, {
|
|
98
150
|
type,
|
|
99
151
|
intent,
|
|
100
152
|
ctx: {
|
|
101
|
-
handlers:
|
|
153
|
+
handlers: intentHandlers,
|
|
154
|
+
cancelled: finalResult !== result,
|
|
102
155
|
reset(defaultValue) {
|
|
103
156
|
return initializeState({
|
|
104
157
|
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue
|
|
@@ -109,14 +162,15 @@ function useConform(formRef, options) {
|
|
|
109
162
|
|
|
110
163
|
// TODO: move on error handler to a new effect
|
|
111
164
|
var formElement = getFormElement(formRef);
|
|
112
|
-
if (
|
|
113
|
-
|
|
165
|
+
if (formElement && result.error) {
|
|
166
|
+
var _optionsRef$current$o, _optionsRef$current;
|
|
167
|
+
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
|
|
168
|
+
formElement,
|
|
169
|
+
error: result.error,
|
|
170
|
+
intent
|
|
171
|
+
});
|
|
114
172
|
}
|
|
115
|
-
|
|
116
|
-
formElement,
|
|
117
|
-
error: result.error,
|
|
118
|
-
intent
|
|
119
|
-
});
|
|
173
|
+
return finalResult;
|
|
120
174
|
}, [formRef, optionsRef]);
|
|
121
175
|
if (options.key !== keyRef.current) {
|
|
122
176
|
keyRef.current = options.key;
|
|
@@ -192,17 +246,11 @@ function useConform(formRef, options) {
|
|
|
192
246
|
if (pendingValueRef.current !== undefined) {
|
|
193
247
|
submission.payload = pendingValueRef.current;
|
|
194
248
|
}
|
|
195
|
-
var value =
|
|
249
|
+
var value = resolveIntent(submission);
|
|
196
250
|
var submissionResult = report(submission, {
|
|
197
251
|
keepFiles: true,
|
|
198
252
|
value
|
|
199
253
|
});
|
|
200
|
-
|
|
201
|
-
// If there is target value, keep track of it as pending value
|
|
202
|
-
if (submission.payload !== value) {
|
|
203
|
-
var _ref;
|
|
204
|
-
pendingValueRef.current = (_ref = value !== null && value !== void 0 ? value : optionsRef.current.defaultValue) !== null && _ref !== void 0 ? _ref : {};
|
|
205
|
-
}
|
|
206
254
|
var validateResult =
|
|
207
255
|
// Skip validation on form reset
|
|
208
256
|
value !== undefined ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
|
|
@@ -229,11 +277,11 @@ function useConform(formRef, options) {
|
|
|
229
277
|
}
|
|
230
278
|
if (typeof asyncResult !== 'undefined') {
|
|
231
279
|
// Update the form when the validation result is resolved
|
|
232
|
-
asyncResult.then(
|
|
280
|
+
asyncResult.then(_ref => {
|
|
233
281
|
var {
|
|
234
282
|
error,
|
|
235
283
|
value
|
|
236
|
-
} =
|
|
284
|
+
} = _ref;
|
|
237
285
|
// Update the form with the validation result
|
|
238
286
|
// There is no need to flush the update in this case
|
|
239
287
|
if (!abortController.signal.aborted) {
|
|
@@ -241,7 +289,7 @@ function useConform(formRef, options) {
|
|
|
241
289
|
handleSubmission('server', submissionResult);
|
|
242
290
|
|
|
243
291
|
// If the form is meant to be submitted and there is no error
|
|
244
|
-
if (error === null && !submission.intent) {
|
|
292
|
+
if (submissionResult.error === null && !submission.intent) {
|
|
245
293
|
var _event = createSubmitEvent(submitEvent.submitter);
|
|
246
294
|
|
|
247
295
|
// Keep track of the submit event so we can skip validation on the next submit
|
|
@@ -256,15 +304,19 @@ function useConform(formRef, options) {
|
|
|
256
304
|
}
|
|
257
305
|
});
|
|
258
306
|
}
|
|
259
|
-
handleSubmission('client', submissionResult);
|
|
307
|
+
var clientResult = handleSubmission('client', submissionResult);
|
|
308
|
+
if (clientResult.reset || clientResult.targetValue !== undefined) {
|
|
309
|
+
var _ref2, _clientResult$targetV;
|
|
310
|
+
pendingValueRef.current = (_ref2 = (_clientResult$targetV = clientResult.targetValue) !== null && _clientResult$targetV !== void 0 ? _clientResult$targetV : optionsRef.current.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {};
|
|
311
|
+
}
|
|
260
312
|
if (
|
|
261
313
|
// If client validation happens
|
|
262
314
|
(typeof syncResult !== 'undefined' || typeof asyncResult !== 'undefined') && (
|
|
263
315
|
// Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation
|
|
264
|
-
|
|
316
|
+
clientResult.submission.intent || clientResult.error !== null)) {
|
|
265
317
|
event.preventDefault();
|
|
266
318
|
}
|
|
267
|
-
result =
|
|
319
|
+
result = clientResult;
|
|
268
320
|
}
|
|
269
321
|
|
|
270
322
|
// We might not prevent form submission if server validation is required
|
|
@@ -461,11 +513,11 @@ function useForm(schemaOrOptions, maybeOptions) {
|
|
|
461
513
|
}), [formId, state, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
|
|
462
514
|
var form = useMemo(() => getFormMetadata(context, {
|
|
463
515
|
serialize: globalOptions.serialize,
|
|
464
|
-
|
|
516
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
465
517
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
466
518
|
var fields = useMemo(() => getFieldset(context, {
|
|
467
519
|
serialize: globalOptions.serialize,
|
|
468
|
-
|
|
520
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
469
521
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
470
522
|
return {
|
|
471
523
|
form,
|
|
@@ -498,7 +550,7 @@ function useFormMetadata() {
|
|
|
498
550
|
var context = useFormContext(options.formId);
|
|
499
551
|
var formMetadata = useMemo(() => getFormMetadata(context, {
|
|
500
552
|
serialize: globalOptions.serialize,
|
|
501
|
-
|
|
553
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
502
554
|
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
503
555
|
return formMetadata;
|
|
504
556
|
}
|
|
@@ -530,7 +582,7 @@ function useField(name) {
|
|
|
530
582
|
var field = useMemo(() => getField(context, {
|
|
531
583
|
name,
|
|
532
584
|
serialize: globalOptions.serialize,
|
|
533
|
-
|
|
585
|
+
extendFieldMetadata: globalOptions.defineCustomMetadata
|
|
534
586
|
}), [context, name, globalOptions.serialize, globalOptions.defineCustomMetadata]);
|
|
535
587
|
return field;
|
|
536
588
|
}
|
|
@@ -753,10 +805,16 @@ function useControl(options) {
|
|
|
753
805
|
* A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
|
|
754
806
|
* The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
|
|
755
807
|
*
|
|
808
|
+
* Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
|
|
809
|
+
* unless a `fallback` is provided.
|
|
810
|
+
*
|
|
756
811
|
* @see https://conform.guide/api/react/future/useFormData
|
|
757
812
|
* @example
|
|
758
813
|
* ```ts
|
|
759
|
-
* const value = useFormData(
|
|
814
|
+
* const value = useFormData(
|
|
815
|
+
* formRef,
|
|
816
|
+
* formData => formData.get('fieldName') ?? '',
|
|
817
|
+
* );
|
|
760
818
|
* ```
|
|
761
819
|
*/
|
|
762
820
|
|
|
@@ -765,7 +823,7 @@ function useFormData(formRef, select, options) {
|
|
|
765
823
|
observer
|
|
766
824
|
} = useContext(GlobalFormOptionsContext);
|
|
767
825
|
var valueRef = useRef();
|
|
768
|
-
var formDataRef = useRef(
|
|
826
|
+
var formDataRef = useRef();
|
|
769
827
|
var value = useSyncExternalStore(useCallback(callback => {
|
|
770
828
|
var formElement = getFormElement(formRef);
|
|
771
829
|
if (formElement) {
|
|
@@ -787,14 +845,17 @@ function useFormData(formRef, select, options) {
|
|
|
787
845
|
});
|
|
788
846
|
return unsubscribe;
|
|
789
847
|
}, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
|
|
790
|
-
//
|
|
848
|
+
// Return fallback if form is not available
|
|
849
|
+
if (formDataRef.current === undefined) {
|
|
850
|
+
return options === null || options === void 0 ? void 0 : options.fallback;
|
|
851
|
+
}
|
|
791
852
|
var result = select(formDataRef.current, valueRef.current);
|
|
792
853
|
if (typeof valueRef.current !== 'undefined' && deepEqual(result, valueRef.current)) {
|
|
793
854
|
return valueRef.current;
|
|
794
855
|
}
|
|
795
856
|
valueRef.current = result;
|
|
796
857
|
return result;
|
|
797
|
-
}, () =>
|
|
858
|
+
}, () => options === null || options === void 0 ? void 0 : options.fallback);
|
|
798
859
|
return value;
|
|
799
860
|
}
|
|
800
861
|
|
|
@@ -816,4 +877,4 @@ function useLatest(value) {
|
|
|
816
877
|
return ref;
|
|
817
878
|
}
|
|
818
879
|
|
|
819
|
-
export { FormContextContext, FormOptionsProvider, FormProvider, GlobalFormOptionsContext, INITIAL_KEY, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect };
|
|
880
|
+
export { FormContextContext, FormOptionsProvider, FormProvider, GlobalFormOptionsContext, INITIAL_KEY, PreserveBoundary, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect };
|
package/dist/future/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { FieldName, FormError, FormValue, Submission, SubmissionResult, } from '@conform-to/dom/future';
|
|
2
2
|
export { getFieldValue, parseSubmission, report, isDirty, } from '@conform-to/dom/future';
|
|
3
|
-
export type { Control, DefaultValue, BaseMetadata, CustomMetadata, CustomMetadataDefinition, BaseErrorShape, CustomTypes, FormContext, FormMetadata, FormOptions, FormRef, FieldMetadata, Fieldset, IntentDispatcher, } from './types';
|
|
4
|
-
export {
|
|
3
|
+
export type { Control, DefaultValue, BaseMetadata, BaseFieldMetadata, CustomMetadata, CustomMetadataDefinition, BaseErrorShape, CustomTypes, CustomSchemaTypes, FormsConfig, FormContext, FormMetadata, FormOptions, FormRef, FieldMetadata, Fieldset, IntentDispatcher, InferBaseErrorShape, InferCustomFormMetadata, InferCustomFieldMetadata, } from './types';
|
|
4
|
+
export { configureForms } from './forms';
|
|
5
|
+
export { PreserveBoundary, FormProvider, FormOptionsProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent, } from './hooks';
|
|
6
|
+
export { shape } from './util';
|
|
5
7
|
export { memoize } from './memoize';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/future/index.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var future = require('@conform-to/dom/future');
|
|
6
|
+
var forms = require('./forms.js');
|
|
6
7
|
var hooks = require('./hooks.js');
|
|
8
|
+
var util = require('./util.js');
|
|
7
9
|
var memoize = require('./memoize.js');
|
|
8
10
|
|
|
9
11
|
|
|
@@ -24,12 +26,15 @@ Object.defineProperty(exports, 'report', {
|
|
|
24
26
|
enumerable: true,
|
|
25
27
|
get: function () { return future.report; }
|
|
26
28
|
});
|
|
29
|
+
exports.configureForms = forms.configureForms;
|
|
27
30
|
exports.FormOptionsProvider = hooks.FormOptionsProvider;
|
|
28
31
|
exports.FormProvider = hooks.FormProvider;
|
|
32
|
+
exports.PreserveBoundary = hooks.PreserveBoundary;
|
|
29
33
|
exports.useControl = hooks.useControl;
|
|
30
34
|
exports.useField = hooks.useField;
|
|
31
35
|
exports.useForm = hooks.useForm;
|
|
32
36
|
exports.useFormData = hooks.useFormData;
|
|
33
37
|
exports.useFormMetadata = hooks.useFormMetadata;
|
|
34
38
|
exports.useIntent = hooks.useIntent;
|
|
39
|
+
exports.shape = util.shape;
|
|
35
40
|
exports.memoize = memoize.memoize;
|
package/dist/future/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { getFieldValue, isDirty, parseSubmission, report } from '@conform-to/dom/future';
|
|
2
|
-
export {
|
|
2
|
+
export { configureForms } from './forms.mjs';
|
|
3
|
+
export { FormOptionsProvider, FormProvider, PreserveBoundary, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs';
|
|
4
|
+
export { shape } from './util.mjs';
|
|
3
5
|
export { memoize } from './memoize.mjs';
|