@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.
@@ -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(formRef, formData => formData?.get('fieldName') ?? '');
237
+ * const value = useFormData(
238
+ * formRef,
239
+ * formData => formData.get('fieldName') ?? '',
240
+ * );
213
241
  * ```
214
242
  */
215
- export declare function useFormData<Value = any>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions & {
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 = any>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
219
- acceptFiles?: boolean;
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
@@ -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
- state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, lastResult), {}, {
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: lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null,
127
+ intent: intent$1,
79
128
  ctx: {
80
- handlers: intent.actionHandlers,
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
- setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
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.actionHandlers,
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 (!formElement || !result.error) {
117
- return;
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
- (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
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.applyIntent(submission);
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(_ref2 => {
284
+ asyncResult.then(_ref => {
237
285
  var {
238
286
  error,
239
287
  value
240
- } = _ref2;
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
- submissionResult.submission.intent || submissionResult.error !== null)) {
320
+ clientResult.submission.intent || clientResult.error !== null)) {
269
321
  event.preventDefault();
270
322
  }
271
- result = submissionResult;
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
- customize: globalOptions.defineCustomMetadata
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
- customize: globalOptions.defineCustomMetadata
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
- customize: globalOptions.defineCustomMetadata
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
- customize: globalOptions.defineCustomMetadata
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(formRef, formData => formData?.get('fieldName') ?? '');
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(null);
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
- // @ts-expect-error FIXME
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
- }, () => select(null, undefined));
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;
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs';
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, useId, useRef, useEffect, useSyncExternalStore, useCallback, useState, useLayoutEffect } from 'react';
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, actionHandlers, applyIntent } from './intent.mjs';
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
- state = updateState(state, _objectSpread2(_objectSpread2({}, lastResult), {}, {
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: lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null,
123
+ intent,
75
124
  ctx: {
76
- handlers: actionHandlers,
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
- setState(state => updateState(state, _objectSpread2(_objectSpread2({}, result), {}, {
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: actionHandlers,
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 (!formElement || !result.error) {
113
- return;
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
- (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
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 = applyIntent(submission);
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(_ref2 => {
280
+ asyncResult.then(_ref => {
233
281
  var {
234
282
  error,
235
283
  value
236
- } = _ref2;
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
- submissionResult.submission.intent || submissionResult.error !== null)) {
316
+ clientResult.submission.intent || clientResult.error !== null)) {
265
317
  event.preventDefault();
266
318
  }
267
- result = submissionResult;
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
- customize: globalOptions.defineCustomMetadata
516
+ extendFieldMetadata: globalOptions.defineCustomMetadata
465
517
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
466
518
  var fields = useMemo(() => getFieldset(context, {
467
519
  serialize: globalOptions.serialize,
468
- customize: globalOptions.defineCustomMetadata
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
- customize: globalOptions.defineCustomMetadata
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
- customize: globalOptions.defineCustomMetadata
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(formRef, formData => formData?.get('fieldName') ?? '');
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(null);
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
- // @ts-expect-error FIXME
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
- }, () => select(null, undefined));
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 };
@@ -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 { FormProvider, FormOptionsProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent, } from './hooks';
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
@@ -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;
@@ -1,3 +1,5 @@
1
1
  export { getFieldValue, isDirty, parseSubmission, report } from '@conform-to/dom/future';
2
- export { FormOptionsProvider, FormProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs';
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';