@conform-to/react 1.15.0 → 1.16.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.
@@ -0,0 +1,318 @@
1
+ import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs';
2
+ import { isFieldElement } from '@conform-to/dom';
3
+ import { DEFAULT_INTENT_NAME, serialize } from '@conform-to/dom/future';
4
+ import { useContext, useMemo, useId, createContext } from 'react';
5
+ import { focusFirstInvalidField, getFormElement, createIntentDispatcher } from './dom.mjs';
6
+ import { useLatest, useConform } from './hooks.mjs';
7
+ import { isTouched, getFormMetadata, getFieldset, getField } from './state.mjs';
8
+ import { isStandardSchemaV1, validateStandardSchemaV1, resolveValidateResult } from './util.mjs';
9
+ import { jsx } from 'react/jsx-runtime';
10
+
11
+ function configureForms() {
12
+ var _config$intentName, _config$serialize, _config$shouldValidat, _ref, _config$shouldRevalid, _config$isSchema, _config$validateSchem;
13
+ var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
14
+ /**
15
+ * Global configuration with defaults applied
16
+ */
17
+ var globalConfig = _objectSpread2(_objectSpread2({}, config), {}, {
18
+ intentName: (_config$intentName = config.intentName) !== null && _config$intentName !== void 0 ? _config$intentName : DEFAULT_INTENT_NAME,
19
+ serialize: (_config$serialize = config.serialize) !== null && _config$serialize !== void 0 ? _config$serialize : serialize,
20
+ shouldValidate: (_config$shouldValidat = config.shouldValidate) !== null && _config$shouldValidat !== void 0 ? _config$shouldValidat : 'onSubmit',
21
+ shouldRevalidate: (_ref = (_config$shouldRevalid = config.shouldRevalidate) !== null && _config$shouldRevalid !== void 0 ? _config$shouldRevalid : config.shouldValidate) !== null && _ref !== void 0 ? _ref : 'onSubmit',
22
+ isSchema: (_config$isSchema = config.isSchema) !== null && _config$isSchema !== void 0 ? _config$isSchema : isStandardSchemaV1,
23
+ validateSchema: (_config$validateSchem = config.validateSchema) !== null && _config$validateSchem !== void 0 ? _config$validateSchem : validateStandardSchemaV1
24
+ });
25
+
26
+ /**
27
+ * React context
28
+ */
29
+ var ReactFormContext = /*#__PURE__*/createContext([]);
30
+
31
+ /**
32
+ * Provides form context to child components.
33
+ * Stacks contexts to support nested forms, with latest context taking priority.
34
+ */
35
+ function FormProvider(props) {
36
+ var stack = useContext(ReactFormContext);
37
+ var value = useMemo(
38
+ // Put the latest form context first to ensure that to be the first one found
39
+ () => [props.context].concat(stack), [stack, props.context]);
40
+ return /*#__PURE__*/jsx(ReactFormContext.Provider, {
41
+ value: value,
42
+ children: props.children
43
+ });
44
+ }
45
+ function useFormContext(formId) {
46
+ var contexts = useContext(ReactFormContext);
47
+ var context = formId ? contexts.find(context => formId === context.formId) : contexts[0];
48
+ if (!context) {
49
+ throw new Error('No form context found. ' + 'Wrap your component with <FormProvider context={form.context}> ' + 'where `form` is returned from useForm().');
50
+ }
51
+ return context;
52
+ }
53
+
54
+ /**
55
+ * The main React hook for form management. Handles form state, validation, and submission
56
+ * while providing access to form metadata, field objects, and form actions.
57
+ *
58
+ * It can be called in two ways:
59
+ * - **Schema first**: Pass a schema as the first argument for automatic validation with type inference
60
+ * - **Manual configuration**: Pass options with custom `onValidate` handler for manual validation
61
+ *
62
+ * @see https://conform.guide/api/react/future/useForm
63
+ * @example Schema first setup with zod:
64
+ *
65
+ * ```tsx
66
+ * const { form, fields } = useForm(zodSchema, {
67
+ * lastResult,
68
+ * shouldValidate: 'onBlur',
69
+ * });
70
+ *
71
+ * return (
72
+ * <form {...form.props}>
73
+ * <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
74
+ * <div>{fields.email.errors}</div>
75
+ * </form>
76
+ * );
77
+ * ```
78
+ *
79
+ * @example Manual configuration setup with custom validation:
80
+ *
81
+ * ```tsx
82
+ * const { form, fields } = useForm({
83
+ * onValidate({ payload, error }) {
84
+ * if (!payload.email) {
85
+ * error.fieldErrors.email = ['Required'];
86
+ * }
87
+ * return error;
88
+ * }
89
+ * });
90
+ *
91
+ * return (
92
+ * <form {...form.props}>
93
+ * <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
94
+ * <div>{fields.email.errors}</div>
95
+ * </form>
96
+ * );
97
+ * ```
98
+ */
99
+
100
+ /**
101
+ * @deprecated Use `useForm(schema, options)` instead for better type inference.
102
+ */
103
+
104
+ function useForm(schemaOrOptions, maybeOptions) {
105
+ var _options$constraint, _globalConfig$getCons, _options$id, _options$onError;
106
+ var schema;
107
+ var options;
108
+ if (globalConfig.isSchema(schemaOrOptions)) {
109
+ schema = schemaOrOptions;
110
+ options = maybeOptions !== null && maybeOptions !== void 0 ? maybeOptions : {};
111
+ } else {
112
+ options = schemaOrOptions;
113
+ }
114
+ var constraint = (_options$constraint = options.constraint) !== null && _options$constraint !== void 0 ? _options$constraint : schema ? (_globalConfig$getCons = globalConfig.getConstraints) === null || _globalConfig$getCons === void 0 ? void 0 : _globalConfig$getCons.call(globalConfig, schema) : undefined;
115
+ var optionsRef = useLatest(options);
116
+ var fallbackId = useId();
117
+ var formId = (_options$id = options.id) !== null && _options$id !== void 0 ? _options$id : "form-".concat(fallbackId);
118
+ var [state, handleSubmit] = useConform(formId, _objectSpread2(_objectSpread2({}, options), {}, {
119
+ serialize: globalConfig.serialize,
120
+ intentName: globalConfig.intentName,
121
+ onError: (_options$onError = options.onError) !== null && _options$onError !== void 0 ? _options$onError : focusFirstInvalidField,
122
+ onValidate(ctx) {
123
+ var _options$onValidate, _options$onValidate2, _options;
124
+ if (schema) {
125
+ var schemaResult = globalConfig.validateSchema(schema, ctx.payload, options.schemaOptions);
126
+ if (schemaResult instanceof Promise) {
127
+ return schemaResult.then(resolvedResult => {
128
+ if (typeof options.onValidate === 'function') {
129
+ throw new Error('The "onValidate" handler is not supported when used with asynchronous schema validation.');
130
+ }
131
+ return resolvedResult;
132
+ });
133
+ }
134
+ if (!options.onValidate) {
135
+ return schemaResult;
136
+ }
137
+
138
+ // Update the schema error in the context
139
+ if (schemaResult.error) {
140
+ ctx.error = schemaResult.error;
141
+ }
142
+ var schemaValue = schemaResult.value;
143
+ ctx.schemaValue = schemaValue;
144
+ var validateResult = resolveValidateResult(options.onValidate(ctx));
145
+ if (validateResult.syncResult) {
146
+ var _validateResult$syncR, _validateResult$syncR2;
147
+ (_validateResult$syncR2 = (_validateResult$syncR = validateResult.syncResult).value) !== null && _validateResult$syncR2 !== void 0 ? _validateResult$syncR2 : _validateResult$syncR.value = schemaValue;
148
+ }
149
+ if (validateResult.asyncResult) {
150
+ validateResult.asyncResult = validateResult.asyncResult.then(result => {
151
+ var _result$value;
152
+ (_result$value = result.value) !== null && _result$value !== void 0 ? _result$value : result.value = schemaValue;
153
+ return result;
154
+ });
155
+ }
156
+ return [validateResult.syncResult, validateResult.asyncResult];
157
+ }
158
+ return (_options$onValidate = (_options$onValidate2 = (_options = options).onValidate) === null || _options$onValidate2 === void 0 ? void 0 : _options$onValidate2.call(_options, ctx)) !== null && _options$onValidate !== void 0 ? _options$onValidate : {
159
+ // To avoid conform falling back to server validation,
160
+ // if neither schema nor validation handler is provided,
161
+ // we just treat it as a valid client submission
162
+ error: null
163
+ };
164
+ }
165
+ }));
166
+ var intent = useIntent(formId);
167
+ var context = useMemo(() => ({
168
+ formId,
169
+ state,
170
+ constraint: constraint !== null && constraint !== void 0 ? constraint : null,
171
+ handleSubmit,
172
+ handleInput(event) {
173
+ var _optionsRef$current$o, _optionsRef$current, _optionsRef$current$s, _ref2, _optionsRef$current$s2;
174
+ if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
175
+ return;
176
+ }
177
+ (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onInput) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, _objectSpread2(_objectSpread2({}, event), {}, {
178
+ target: event.target,
179
+ currentTarget: event.target.form
180
+ }));
181
+ if (event.defaultPrevented) {
182
+ return;
183
+ }
184
+ var shouldValidate = (_optionsRef$current$s = optionsRef.current.shouldValidate) !== null && _optionsRef$current$s !== void 0 ? _optionsRef$current$s : globalConfig.shouldValidate;
185
+ var shouldRevalidate = (_ref2 = (_optionsRef$current$s2 = optionsRef.current.shouldRevalidate) !== null && _optionsRef$current$s2 !== void 0 ? _optionsRef$current$s2 : optionsRef.current.shouldValidate) !== null && _ref2 !== void 0 ? _ref2 : globalConfig.shouldRevalidate;
186
+ if (isTouched(state, event.target.name) ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') {
187
+ intent.validate(event.target.name);
188
+ }
189
+ },
190
+ handleBlur(event) {
191
+ var _optionsRef$current$o2, _optionsRef$current2, _optionsRef$current$s3, _ref3, _optionsRef$current$s4;
192
+ if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
193
+ return;
194
+ }
195
+ (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onBlur) === null || _optionsRef$current$o2 === void 0 || _optionsRef$current$o2.call(_optionsRef$current2, _objectSpread2(_objectSpread2({}, event), {}, {
196
+ target: event.target,
197
+ currentTarget: event.target.form
198
+ }));
199
+ if (event.defaultPrevented) {
200
+ return;
201
+ }
202
+ var shouldValidate = (_optionsRef$current$s3 = optionsRef.current.shouldValidate) !== null && _optionsRef$current$s3 !== void 0 ? _optionsRef$current$s3 : globalConfig.shouldValidate;
203
+ var shouldRevalidate = (_ref3 = (_optionsRef$current$s4 = optionsRef.current.shouldRevalidate) !== null && _optionsRef$current$s4 !== void 0 ? _optionsRef$current$s4 : optionsRef.current.shouldValidate) !== null && _ref3 !== void 0 ? _ref3 : globalConfig.shouldRevalidate;
204
+ if (isTouched(state, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') {
205
+ intent.validate(event.target.name);
206
+ }
207
+ }
208
+ }), [formId, state, constraint, handleSubmit, intent, optionsRef]);
209
+ var form = useMemo(() => getFormMetadata(context, {
210
+ serialize: globalConfig.serialize,
211
+ extendFormMetadata: globalConfig.extendFormMetadata,
212
+ extendFieldMetadata: globalConfig.extendFieldMetadata
213
+ }), [context]);
214
+ var fields = useMemo(() => getFieldset(context, {
215
+ serialize: globalConfig.serialize,
216
+ extendFieldMetadata: globalConfig.extendFieldMetadata
217
+ }), [context]);
218
+ return {
219
+ form,
220
+ fields,
221
+ intent
222
+ };
223
+ }
224
+
225
+ /**
226
+ * A React hook that provides access to form-level metadata and state.
227
+ * Requires `FormProvider` context when used in child components.
228
+ *
229
+ * @see https://conform.guide/api/react/future/useFormMetadata
230
+ * @example
231
+ * ```tsx
232
+ * function ErrorSummary() {
233
+ * const form = useFormMetadata();
234
+ *
235
+ * if (form.valid) return null;
236
+ *
237
+ * return (
238
+ * <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
239
+ * );
240
+ * }
241
+ * ```
242
+ */
243
+ function useFormMetadata() {
244
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
245
+ var context = useFormContext(options.formId);
246
+ var formMetadata = useMemo(() => getFormMetadata(context, {
247
+ serialize: globalConfig.serialize,
248
+ extendFormMetadata: globalConfig.extendFormMetadata,
249
+ extendFieldMetadata: globalConfig.extendFieldMetadata
250
+ }), [context]);
251
+ return formMetadata;
252
+ }
253
+
254
+ /**
255
+ * A React hook that provides access to a specific field's metadata and state.
256
+ * Requires `FormProvider` context when used in child components.
257
+ *
258
+ * @see https://conform.guide/api/react/future/useField
259
+ * @example
260
+ * ```tsx
261
+ * function FormField({ name, label }) {
262
+ * const field = useField(name);
263
+ *
264
+ * return (
265
+ * <div>
266
+ * <label htmlFor={field.id}>{label}</label>
267
+ * <input id={field.id} name={field.name} defaultValue={field.defaultValue} />
268
+ * {field.errors && <div>{field.errors.join(', ')}</div>}
269
+ * </div>
270
+ * );
271
+ * }
272
+ * ```
273
+ */
274
+ function useField(name) {
275
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
276
+ var context = useFormContext(options.formId);
277
+ var field = useMemo(() => getField(context, {
278
+ name,
279
+ serialize: globalConfig.serialize,
280
+ extendFieldMetadata: globalConfig.extendFieldMetadata
281
+ }), [context, name]);
282
+ return field;
283
+ }
284
+
285
+ /**
286
+ * A React hook that provides an intent dispatcher for programmatic form actions.
287
+ * Intent dispatchers allow you to trigger form operations like validation, field updates,
288
+ * and array manipulations without manual form submission.
289
+ *
290
+ * @see https://conform.guide/api/react/future/useIntent
291
+ * @example
292
+ * ```tsx
293
+ * function ResetButton() {
294
+ * const buttonRef = useRef<HTMLButtonElement>(null);
295
+ * const intent = useIntent(buttonRef);
296
+ *
297
+ * return (
298
+ * <button type="button" ref={buttonRef} onClick={() => intent.reset()}>
299
+ * Reset Form
300
+ * </button>
301
+ * );
302
+ * }
303
+ * ```
304
+ */
305
+ function useIntent(formRef) {
306
+ return useMemo(() => createIntentDispatcher(() => getFormElement(formRef), globalConfig.intentName), [formRef]);
307
+ }
308
+ return {
309
+ FormProvider,
310
+ useForm,
311
+ useFormMetadata,
312
+ useField,
313
+ useIntent,
314
+ config: globalConfig
315
+ };
316
+ }
317
+
318
+ export { configureForms };
@@ -15,6 +15,9 @@ export declare function FormProvider(props: {
15
15
  context: FormContext;
16
16
  children: React.ReactNode;
17
17
  }): React.ReactElement;
18
+ /**
19
+ * @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`.
20
+ */
18
21
  export declare function FormOptionsProvider(props: Partial<GlobalFormOptions> & {
19
22
  children: React.ReactNode;
20
23
  }): React.ReactElement;
@@ -206,18 +209,36 @@ export declare function useControl(options?: {
206
209
  * A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
207
210
  * The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
208
211
  *
212
+ * Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
213
+ * unless a `fallback` is provided.
214
+ *
209
215
  * @see https://conform.guide/api/react/future/useFormData
210
216
  * @example
211
217
  * ```ts
212
- * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
218
+ * const value = useFormData(
219
+ * formRef,
220
+ * formData => formData.get('fieldName') ?? '',
221
+ * );
213
222
  * ```
214
223
  */
215
- export declare function useFormData<Value = any>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions & {
224
+ export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions<Value> & {
216
225
  acceptFiles: true;
226
+ fallback: Value;
217
227
  }): Value;
218
- export declare function useFormData<Value = any>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
219
- acceptFiles?: boolean;
228
+ export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value>, options: UseFormDataOptions & {
229
+ acceptFiles: true;
230
+ }): Value | undefined;
231
+ export declare function useFormData<Value>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options: UseFormDataOptions<Value> & {
232
+ acceptFiles?: false;
233
+ fallback: Value;
220
234
  }): Value;
235
+ export declare function useFormData<Value>(formRef: FormRef, select: Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
236
+ acceptFiles?: false;
237
+ }): Value | undefined;
238
+ export declare function useFormData<Value>(formRef: FormRef, select: Selector<FormData, Value> | Selector<URLSearchParams, Value>, options?: UseFormDataOptions & {
239
+ acceptFiles?: boolean;
240
+ fallback?: Value;
241
+ }): Value | undefined;
221
242
  /**
222
243
  * useLayoutEffect is client-only.
223
244
  * This basically makes it a no-op on server
@@ -38,6 +38,10 @@ function FormProvider(props) {
38
38
  children: props.children
39
39
  });
40
40
  }
41
+
42
+ /**
43
+ * @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`.
44
+ */
41
45
  function FormOptionsProvider(props) {
42
46
  var {
43
47
  children
@@ -465,11 +469,11 @@ function useForm(schemaOrOptions, maybeOptions) {
465
469
  }), [formId, state$1, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
466
470
  var form = react.useMemo(() => state.getFormMetadata(context, {
467
471
  serialize: globalOptions.serialize,
468
- customize: globalOptions.defineCustomMetadata
472
+ extendFieldMetadata: globalOptions.defineCustomMetadata
469
473
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
470
474
  var fields = react.useMemo(() => state.getFieldset(context, {
471
475
  serialize: globalOptions.serialize,
472
- customize: globalOptions.defineCustomMetadata
476
+ extendFieldMetadata: globalOptions.defineCustomMetadata
473
477
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
474
478
  return {
475
479
  form,
@@ -502,7 +506,7 @@ function useFormMetadata() {
502
506
  var context = useFormContext(options.formId);
503
507
  var formMetadata = react.useMemo(() => state.getFormMetadata(context, {
504
508
  serialize: globalOptions.serialize,
505
- customize: globalOptions.defineCustomMetadata
509
+ extendFieldMetadata: globalOptions.defineCustomMetadata
506
510
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
507
511
  return formMetadata;
508
512
  }
@@ -534,7 +538,7 @@ function useField(name) {
534
538
  var field = react.useMemo(() => state.getField(context, {
535
539
  name,
536
540
  serialize: globalOptions.serialize,
537
- customize: globalOptions.defineCustomMetadata
541
+ extendFieldMetadata: globalOptions.defineCustomMetadata
538
542
  }), [context, name, globalOptions.serialize, globalOptions.defineCustomMetadata]);
539
543
  return field;
540
544
  }
@@ -757,10 +761,16 @@ function useControl(options) {
757
761
  * A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
758
762
  * The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
759
763
  *
764
+ * Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
765
+ * unless a `fallback` is provided.
766
+ *
760
767
  * @see https://conform.guide/api/react/future/useFormData
761
768
  * @example
762
769
  * ```ts
763
- * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
770
+ * const value = useFormData(
771
+ * formRef,
772
+ * formData => formData.get('fieldName') ?? '',
773
+ * );
764
774
  * ```
765
775
  */
766
776
 
@@ -769,7 +779,7 @@ function useFormData(formRef, select, options) {
769
779
  observer
770
780
  } = react.useContext(GlobalFormOptionsContext);
771
781
  var valueRef = react.useRef();
772
- var formDataRef = react.useRef(null);
782
+ var formDataRef = react.useRef();
773
783
  var value = react.useSyncExternalStore(react.useCallback(callback => {
774
784
  var formElement = dom.getFormElement(formRef);
775
785
  if (formElement) {
@@ -791,14 +801,17 @@ function useFormData(formRef, select, options) {
791
801
  });
792
802
  return unsubscribe;
793
803
  }, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
794
- // @ts-expect-error FIXME
804
+ // Return fallback if form is not available
805
+ if (formDataRef.current === undefined) {
806
+ return options === null || options === void 0 ? void 0 : options.fallback;
807
+ }
795
808
  var result = select(formDataRef.current, valueRef.current);
796
809
  if (typeof valueRef.current !== 'undefined' && future.deepEqual(result, valueRef.current)) {
797
810
  return valueRef.current;
798
811
  }
799
812
  valueRef.current = result;
800
813
  return result;
801
- }, () => select(null, undefined));
814
+ }, () => options === null || options === void 0 ? void 0 : options.fallback);
802
815
  return value;
803
816
  }
804
817
 
@@ -34,6 +34,10 @@ function FormProvider(props) {
34
34
  children: props.children
35
35
  });
36
36
  }
37
+
38
+ /**
39
+ * @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`.
40
+ */
37
41
  function FormOptionsProvider(props) {
38
42
  var {
39
43
  children
@@ -461,11 +465,11 @@ function useForm(schemaOrOptions, maybeOptions) {
461
465
  }), [formId, state, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
462
466
  var form = useMemo(() => getFormMetadata(context, {
463
467
  serialize: globalOptions.serialize,
464
- customize: globalOptions.defineCustomMetadata
468
+ extendFieldMetadata: globalOptions.defineCustomMetadata
465
469
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
466
470
  var fields = useMemo(() => getFieldset(context, {
467
471
  serialize: globalOptions.serialize,
468
- customize: globalOptions.defineCustomMetadata
472
+ extendFieldMetadata: globalOptions.defineCustomMetadata
469
473
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
470
474
  return {
471
475
  form,
@@ -498,7 +502,7 @@ function useFormMetadata() {
498
502
  var context = useFormContext(options.formId);
499
503
  var formMetadata = useMemo(() => getFormMetadata(context, {
500
504
  serialize: globalOptions.serialize,
501
- customize: globalOptions.defineCustomMetadata
505
+ extendFieldMetadata: globalOptions.defineCustomMetadata
502
506
  }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
503
507
  return formMetadata;
504
508
  }
@@ -530,7 +534,7 @@ function useField(name) {
530
534
  var field = useMemo(() => getField(context, {
531
535
  name,
532
536
  serialize: globalOptions.serialize,
533
- customize: globalOptions.defineCustomMetadata
537
+ extendFieldMetadata: globalOptions.defineCustomMetadata
534
538
  }), [context, name, globalOptions.serialize, globalOptions.defineCustomMetadata]);
535
539
  return field;
536
540
  }
@@ -753,10 +757,16 @@ function useControl(options) {
753
757
  * A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
754
758
  * The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
755
759
  *
760
+ * Returns `undefined` when the form element is not available (e.g., on SSR or initial client render),
761
+ * unless a `fallback` is provided.
762
+ *
756
763
  * @see https://conform.guide/api/react/future/useFormData
757
764
  * @example
758
765
  * ```ts
759
- * const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
766
+ * const value = useFormData(
767
+ * formRef,
768
+ * formData => formData.get('fieldName') ?? '',
769
+ * );
760
770
  * ```
761
771
  */
762
772
 
@@ -765,7 +775,7 @@ function useFormData(formRef, select, options) {
765
775
  observer
766
776
  } = useContext(GlobalFormOptionsContext);
767
777
  var valueRef = useRef();
768
- var formDataRef = useRef(null);
778
+ var formDataRef = useRef();
769
779
  var value = useSyncExternalStore(useCallback(callback => {
770
780
  var formElement = getFormElement(formRef);
771
781
  if (formElement) {
@@ -787,14 +797,17 @@ function useFormData(formRef, select, options) {
787
797
  });
788
798
  return unsubscribe;
789
799
  }, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
790
- // @ts-expect-error FIXME
800
+ // Return fallback if form is not available
801
+ if (formDataRef.current === undefined) {
802
+ return options === null || options === void 0 ? void 0 : options.fallback;
803
+ }
791
804
  var result = select(formDataRef.current, valueRef.current);
792
805
  if (typeof valueRef.current !== 'undefined' && deepEqual(result, valueRef.current)) {
793
806
  return valueRef.current;
794
807
  }
795
808
  valueRef.current = result;
796
809
  return result;
797
- }, () => select(null, undefined));
810
+ }, () => options === null || options === void 0 ? void 0 : options.fallback);
798
811
  return value;
799
812
  }
800
813
 
@@ -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';
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';
4
5
  export { 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,6 +26,7 @@ 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;
29
32
  exports.useControl = hooks.useControl;
@@ -32,4 +35,5 @@ exports.useForm = hooks.useForm;
32
35
  exports.useFormData = hooks.useFormData;
33
36
  exports.useFormMetadata = hooks.useFormMetadata;
34
37
  exports.useIntent = hooks.useIntent;
38
+ exports.shape = util.shape;
35
39
  exports.memoize = memoize.memoize;
@@ -1,3 +1,5 @@
1
1
  export { getFieldValue, isDirty, parseSubmission, report } from '@conform-to/dom/future';
2
+ export { configureForms } from './forms.mjs';
2
3
  export { FormOptionsProvider, FormProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs';
4
+ export { shape } from './util.mjs';
3
5
  export { memoize } from './memoize.mjs';
@@ -1,5 +1,5 @@
1
1
  import { type FieldName, type ValidationAttributes, type Serialize } from '@conform-to/dom/future';
2
- import type { FieldMetadata, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, ActionHandler, CustomMetadataDefinition } from './types';
2
+ import type { FieldMetadata, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, ActionHandler, BaseFieldMetadata, BaseFormMetadata, DefineConditionalField } from './types';
3
3
  export declare function initializeState<ErrorShape>(options?: {
4
4
  defaultValue?: Record<string, unknown> | null | undefined;
5
5
  resetKey?: string | undefined;
@@ -39,32 +39,47 @@ export declare function isValid(state: FormState<any>, name?: string): boolean;
39
39
  * e.g. "array[0].key" falls back to "array[].key" if specific constraint not found.
40
40
  */
41
41
  export declare function getConstraint(context: FormContext<any>, name: string): ValidationAttributes | undefined;
42
- export declare function getFormMetadata<ErrorShape>(context: FormContext<ErrorShape>, options?: {
42
+ export declare function getFormMetadata<ErrorShape, CustomFormMetadata extends Record<string, unknown> = {}, CustomFieldMetadata extends Record<string, unknown> = {}>(context: FormContext<ErrorShape>, options?: {
43
43
  serialize?: Serialize | undefined;
44
- customize?: CustomMetadataDefinition | undefined;
45
- }): FormMetadata<ErrorShape>;
46
- export declare function getField<FieldShape, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
44
+ extendFormMetadata?: ((metadata: BaseFormMetadata<ErrorShape>) => CustomFormMetadata) | undefined;
45
+ extendFieldMetadata?: (<FieldShape>(metadata: BaseFieldMetadata<FieldShape, ErrorShape>, ctx: {
46
+ form: BaseFormMetadata<ErrorShape>;
47
+ when: DefineConditionalField;
48
+ }) => CustomFieldMetadata) | undefined;
49
+ }): FormMetadata<ErrorShape, CustomFormMetadata, CustomFieldMetadata>;
50
+ export declare function getField<FieldShape, ErrorShape = string, CustomFieldMetadata extends Record<string, unknown> = {}>(context: FormContext<ErrorShape>, options: {
47
51
  name: FieldName<FieldShape>;
48
52
  serialize?: Serialize | undefined;
49
- customize?: CustomMetadataDefinition | undefined;
53
+ extendFieldMetadata?: (<F>(metadata: BaseFieldMetadata<F, ErrorShape>, ctx: {
54
+ form: BaseFormMetadata<ErrorShape>;
55
+ when: DefineConditionalField;
56
+ }) => CustomFieldMetadata) | undefined;
57
+ form?: BaseFormMetadata<ErrorShape, CustomFieldMetadata> | undefined;
50
58
  key?: string | undefined;
51
- }): FieldMetadata<FieldShape, ErrorShape>;
59
+ }): FieldMetadata<FieldShape, ErrorShape, CustomFieldMetadata>;
52
60
  /**
53
61
  * Creates a proxy that dynamically generates field objects when properties are accessed.
54
62
  */
55
- export declare function getFieldset<FieldShape = Record<string, any>, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
63
+ export declare function getFieldset<FieldShape = Record<string, any>, ErrorShape = string, CustomFieldMetadata extends Record<string, unknown> = {}>(context: FormContext<ErrorShape>, options: {
56
64
  name?: FieldName<FieldShape> | undefined;
57
65
  serialize?: Serialize | undefined;
58
- customize?: CustomMetadataDefinition | undefined;
59
- }): Fieldset<FieldShape, ErrorShape>;
66
+ extendFieldMetadata?: (<F>(metadata: BaseFieldMetadata<F, ErrorShape>, ctx: {
67
+ form: BaseFormMetadata<ErrorShape>;
68
+ when: DefineConditionalField;
69
+ }) => CustomFieldMetadata) | undefined;
70
+ form?: BaseFormMetadata<ErrorShape, CustomFieldMetadata> | undefined;
71
+ }): Fieldset<FieldShape, ErrorShape, CustomFieldMetadata>;
60
72
  /**
61
73
  * Creates an array of field objects for list/array inputs
62
74
  */
63
- export declare function getFieldList<FieldShape = Array<any>, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
75
+ export declare function getFieldList<FieldShape = Array<any>, ErrorShape = string, CustomFieldMetadata extends Record<string, unknown> = {}>(context: FormContext<ErrorShape>, options: {
64
76
  name: FieldName<FieldShape>;
65
77
  serialize?: Serialize | undefined;
66
- customize?: CustomMetadataDefinition | undefined;
78
+ extendFieldMetadata?: (<F>(metadata: BaseFieldMetadata<F, ErrorShape>, ctx: {
79
+ form: BaseFormMetadata<ErrorShape>;
80
+ when: DefineConditionalField;
81
+ }) => CustomFieldMetadata) | undefined;
67
82
  }): FieldMetadata<[
68
83
  FieldShape
69
- ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, ErrorShape>[];
84
+ ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, ErrorShape, CustomFieldMetadata>[];
70
85
  //# sourceMappingURL=state.d.ts.map