@conform-to/react 1.13.3 → 1.14.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 CHANGED
@@ -7,7 +7,7 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.13.3 / License MIT / Copyright (c) 2025 Edmund Hung
10
+ Version 1.14.0 / License MIT / Copyright (c) 2025 Edmund Hung
11
11
 
12
12
  Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards.
13
13
 
@@ -3,9 +3,9 @@ import type { ErrorContext, FormRef, InputSnapshot, IntentDispatcher } from './t
3
3
  export declare function getFormElement(formRef: FormRef | undefined): HTMLFormElement | null;
4
4
  export declare function getSubmitEvent(event: React.FormEvent<HTMLFormElement>): SubmitEvent;
5
5
  export declare function initializeField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
6
- defaultValue?: string | string[] | File | File[] | null;
7
- defaultChecked?: boolean;
8
- value?: string;
6
+ defaultValue?: string | string[] | File | File[] | null | undefined;
7
+ defaultChecked?: boolean | undefined;
8
+ value?: string | undefined;
9
9
  } | undefined): void;
10
10
  /**
11
11
  * Makes hidden form inputs focusable with visually hidden styles
@@ -27,7 +27,8 @@ export declare function createDefaultSnapshot(defaultValue: string | string[] |
27
27
  * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert)
28
28
  */
29
29
  export declare function focusFirstInvalidField<ErrorShape>(ctx: ErrorContext<ErrorShape>): void;
30
- export declare function updateFormValue(form: HTMLFormElement, intendedValue: Record<string, unknown>, serialize: Serialize): void;
30
+ export declare function updateFormValue(form: HTMLFormElement, targetValue: Record<string, unknown>, serialize: Serialize): void;
31
+ export declare function resetFormValue(form: HTMLFormElement, defaultValue: Record<string, unknown>, serialize: Serialize): void;
31
32
  /**
32
33
  * Creates a proxy that dynamically generates intent dispatch functions.
33
34
  * Each property access returns a function that submits the intent to the form.
@@ -173,22 +173,35 @@ function focusFirstInvalidField(ctx) {
173
173
  }
174
174
  }
175
175
  }
176
- function updateFormValue(form, intendedValue, serialize) {
176
+ function updateFormValue(form, targetValue, serialize) {
177
177
  for (var element of form.elements) {
178
- if (future.isFieldElement(element) && element.name) {
179
- var fieldValue = future.getValueAtPath(intendedValue, element.name);
180
- if (element.type === 'file' && typeof fieldValue === 'undefined') {
181
- // Do not update file inputs unless there's an intended value
178
+ if (future.isFieldElement(element) && element.name && element.type !== 'hidden') {
179
+ var fieldValue = future.getValueAtPath(targetValue, element.name);
180
+ if (element.type === 'file' && fieldValue === undefined) {
181
+ // Do not update file inputs unless there's a target value
182
182
  continue;
183
183
  }
184
- var serializedValue = serialize(fieldValue);
185
- var value = typeof serializedValue !== 'undefined' ? serializedValue : future.getFieldDefaultValue(element);
186
- future.change(element, value, {
184
+ var value = serialize(fieldValue);
185
+
186
+ // Treat undefined as null to clear the field value
187
+ future.change(element, value !== undefined ? value : null, {
187
188
  preventDefault: true
188
189
  });
189
190
  }
190
191
  }
191
192
  }
193
+ function resetFormValue(form, defaultValue, serialize) {
194
+ for (var element of form.elements) {
195
+ if (future.isFieldElement(element) && element.name && element.type !== 'hidden' && element.type !== 'file') {
196
+ var fieldValue = future.getValueAtPath(defaultValue, element.name);
197
+ var value = serialize(fieldValue);
198
+ future.updateField(element, {
199
+ defaultValue: value !== undefined ? value : null
200
+ });
201
+ }
202
+ }
203
+ form.reset();
204
+ }
192
205
 
193
206
  /**
194
207
  * Creates a proxy that dynamically generates intent dispatch functions.
@@ -226,4 +239,5 @@ exports.getRadioGroupValue = getRadioGroupValue;
226
239
  exports.getSubmitEvent = getSubmitEvent;
227
240
  exports.initializeField = initializeField;
228
241
  exports.makeInputFocusable = makeInputFocusable;
242
+ exports.resetFormValue = resetFormValue;
229
243
  exports.updateFormValue = updateFormValue;
@@ -1,4 +1,4 @@
1
- import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath, getFieldDefaultValue, change } from '@conform-to/dom/future';
1
+ import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath, change } from '@conform-to/dom/future';
2
2
  import { serializeIntent } from './intent.mjs';
3
3
 
4
4
  function getFormElement(formRef) {
@@ -169,22 +169,35 @@ function focusFirstInvalidField(ctx) {
169
169
  }
170
170
  }
171
171
  }
172
- function updateFormValue(form, intendedValue, serialize) {
172
+ function updateFormValue(form, targetValue, serialize) {
173
173
  for (var element of form.elements) {
174
- if (isFieldElement(element) && element.name) {
175
- var fieldValue = getValueAtPath(intendedValue, element.name);
176
- if (element.type === 'file' && typeof fieldValue === 'undefined') {
177
- // Do not update file inputs unless there's an intended value
174
+ if (isFieldElement(element) && element.name && element.type !== 'hidden') {
175
+ var fieldValue = getValueAtPath(targetValue, element.name);
176
+ if (element.type === 'file' && fieldValue === undefined) {
177
+ // Do not update file inputs unless there's a target value
178
178
  continue;
179
179
  }
180
- var serializedValue = serialize(fieldValue);
181
- var value = typeof serializedValue !== 'undefined' ? serializedValue : getFieldDefaultValue(element);
182
- change(element, value, {
180
+ var value = serialize(fieldValue);
181
+
182
+ // Treat undefined as null to clear the field value
183
+ change(element, value !== undefined ? value : null, {
183
184
  preventDefault: true
184
185
  });
185
186
  }
186
187
  }
187
188
  }
189
+ function resetFormValue(form, defaultValue, serialize) {
190
+ for (var element of form.elements) {
191
+ if (isFieldElement(element) && element.name && element.type !== 'hidden' && element.type !== 'file') {
192
+ var fieldValue = getValueAtPath(defaultValue, element.name);
193
+ var value = serialize(fieldValue);
194
+ updateField(element, {
195
+ defaultValue: value !== undefined ? value : null
196
+ });
197
+ }
198
+ }
199
+ form.reset();
200
+ }
188
201
 
189
202
  /**
190
203
  * Creates a proxy that dynamically generates intent dispatch functions.
@@ -212,4 +225,4 @@ function createIntentDispatcher(formElement, intentName) {
212
225
  });
213
226
  }
214
227
 
215
- export { createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, updateFormValue };
228
+ export { createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, resetFormValue, updateFormValue };
@@ -1,6 +1,7 @@
1
- import { type Serialize, type SubmissionResult, createGlobalFormsObserver } from '@conform-to/dom/future';
1
+ import { type FormValue, type Serialize, type SubmissionResult, createGlobalFormsObserver } from '@conform-to/dom/future';
2
2
  import { useEffect } from 'react';
3
- import type { FormContext, IntentDispatcher, FormMetadata, Fieldset, GlobalFormOptions, FormOptions, FieldName, FieldMetadata, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef, BaseErrorShape, DefaultErrorShape } from './types';
3
+ import type { FormContext, IntentDispatcher, FormMetadata, Fieldset, GlobalFormOptions, FormOptions, FieldName, FieldMetadata, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef, BaseErrorShape, DefaultErrorShape, BaseSchemaType, InferInput, InferOutput } from './types';
4
+ import { StandardSchemaV1 } from './standard-schema';
4
5
  export declare const INITIAL_KEY = "INITIAL_KEY";
5
6
  export declare const GlobalFormOptionsContext: import("react").Context<GlobalFormOptions & {
6
7
  observer: ReturnType<typeof createGlobalFormsObserver>;
@@ -22,24 +23,46 @@ export declare function useFormContext(formId?: string): FormContext;
22
23
  * Core form hook that manages form state, validation, and submission.
23
24
  * Handles both sync and async validation, intent dispatching, and DOM updates.
24
25
  */
25
- export declare function useConform<ErrorShape, Value = undefined>(formRef: FormRef, options: {
26
- key?: string;
26
+ export declare function useConform<FormShape extends Record<string, any>, ErrorShape, Value = undefined, SchemaValue = undefined>(formRef: FormRef, options: {
27
+ key?: string | undefined;
28
+ defaultValue?: Record<string, FormValue> | null | undefined;
27
29
  serialize: Serialize;
28
30
  intentName: string;
29
- lastResult?: SubmissionResult<NoInfer<ErrorShape>> | null;
30
- onValidate?: ValidateHandler<ErrorShape, Value>;
31
- onError?: ErrorHandler<ErrorShape>;
32
- onSubmit?: SubmitHandler<NoInfer<ErrorShape>, NoInfer<Value>>;
31
+ lastResult?: SubmissionResult<NoInfer<ErrorShape>> | null | undefined;
32
+ onValidate?: ValidateHandler<ErrorShape, Value, SchemaValue> | undefined;
33
+ onError?: ErrorHandler<ErrorShape> | undefined;
34
+ onSubmit?: SubmitHandler<FormShape, NoInfer<ErrorShape>, NoInfer<Value>> | undefined;
33
35
  }): [FormState<ErrorShape>, (event: React.FormEvent<HTMLFormElement>) => void];
34
36
  /**
35
37
  * The main React hook for form management. Handles form state, validation, and submission
36
38
  * while providing access to form metadata, field objects, and form actions.
37
39
  *
40
+ * It can be called in two ways:
41
+ * - **Schema first**: Pass a schema as the first argument for automatic validation with type inference
42
+ * - **Manual configuration**: Pass options with custom `onValidate` handler for manual validation
43
+ *
38
44
  * @see https://conform.guide/api/react/future/useForm
39
- * @example
45
+ * @example Schema first setup with zod:
46
+ *
47
+ * ```tsx
48
+ * const { form, fields } = useForm(zodSchema, {
49
+ * lastResult,
50
+ * shouldValidate: 'onBlur',
51
+ * });
52
+ *
53
+ * return (
54
+ * <form {...form.props}>
55
+ * <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
56
+ * <div>{fields.email.errors}</div>
57
+ * </form>
58
+ * );
59
+ * ```
60
+ *
61
+ * @example Manual configuration setup with custom validation:
62
+ *
40
63
  * ```tsx
41
64
  * const { form, fields } = useForm({
42
- * onValidate({ payload, error }) {
65
+ * onValidate({ payload, error }) {
43
66
  * if (!payload.email) {
44
67
  * error.fieldErrors.email = ['Required'];
45
68
  * }
@@ -55,7 +78,29 @@ export declare function useConform<ErrorShape, Value = undefined>(formRef: FormR
55
78
  * );
56
79
  * ```
57
80
  */
58
- export declare function useForm<FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = DefaultErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value>): {
81
+ export declare function useForm<Schema extends BaseSchemaType, ErrorShape extends BaseErrorShape = DefaultErrorShape, Value = InferOutput<Schema>>(schema: Schema, options: FormOptions<InferInput<Schema>, ErrorShape, Value, Schema, string extends ErrorShape ? never : 'onValidate'>): {
82
+ form: FormMetadata<ErrorShape>;
83
+ fields: Fieldset<InferInput<Schema>, ErrorShape>;
84
+ intent: IntentDispatcher<InferInput<Schema>>;
85
+ };
86
+ /**
87
+ * @deprecated Use `useForm(schema, options)` instead for better type inference.
88
+ */
89
+ export declare function useForm<FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = DefaultErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value, undefined, undefined extends Value ? 'onValidate' : never> & {
90
+ /**
91
+ * @deprecated Use `useForm(schema, options)` instead for better type inference.
92
+ *
93
+ * Optional standard schema for validation (e.g., Zod, Valibot, Yup).
94
+ * Removes the need for manual onValidate setup.
95
+ *
96
+ */
97
+ schema: StandardSchemaV1<FormShape, Value>;
98
+ }): {
99
+ form: FormMetadata<ErrorShape>;
100
+ fields: Fieldset<FormShape, ErrorShape>;
101
+ intent: IntentDispatcher<FormShape>;
102
+ };
103
+ export declare function useForm<FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = DefaultErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value, undefined, 'onValidate'>): {
59
104
  form: FormMetadata<ErrorShape>;
60
105
  fields: Fieldset<FormShape, ErrorShape>;
61
106
  intent: IntentDispatcher<FormShape>;
@@ -13,6 +13,8 @@ var dom = require('./dom.js');
13
13
  var jsxRuntime = require('react/jsx-runtime');
14
14
 
15
15
  var _excluded = ["children"];
16
+ // Static reset key for consistent hydration during Next.js prerendering
17
+ // See: https://nextjs.org/docs/messages/next-prerender-current-time-client
16
18
  var INITIAL_KEY = 'INITIAL_KEY';
17
19
  var GlobalFormOptionsContext = /*#__PURE__*/react.createContext({
18
20
  intentName: future.DEFAULT_INTENT_NAME,
@@ -66,14 +68,20 @@ function useConform(formRef, options) {
66
68
  lastResult
67
69
  } = options;
68
70
  var [state$1, setState] = react.useState(() => {
69
- var state$1 = state.initializeState(INITIAL_KEY);
71
+ var state$1 = state.initializeState({
72
+ defaultValue: options.defaultValue,
73
+ resetKey: INITIAL_KEY
74
+ });
70
75
  if (lastResult) {
71
76
  state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, lastResult), {}, {
72
77
  type: 'initialize',
73
78
  intent: lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null,
74
79
  ctx: {
75
80
  handlers: intent.actionHandlers,
76
- reset: () => state$1
81
+ reset: defaultValue => state.initializeState({
82
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue,
83
+ resetKey: INITIAL_KEY
84
+ })
77
85
  }
78
86
  }));
79
87
  }
@@ -83,22 +91,27 @@ function useConform(formRef, options) {
83
91
  var resetKeyRef = react.useRef(state$1.resetKey);
84
92
  var optionsRef = useLatest(options);
85
93
  var lastResultRef = react.useRef(lastResult);
86
- var lastIntentedValueRef = react.useRef();
94
+ var pendingValueRef = react.useRef();
87
95
  var lastAsyncResultRef = react.useRef(null);
88
96
  var abortControllerRef = react.useRef(null);
89
- var handleSubmission = react.useCallback((type, result) => {
97
+ var handleSubmission = react.useCallback(function (type, result) {
90
98
  var _optionsRef$current$o, _optionsRef$current;
99
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current;
91
100
  var intent$1 = result.submission.intent ? intent.deserializeIntent(result.submission.intent) : null;
92
101
  setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
93
102
  type,
94
103
  intent: intent$1,
95
104
  ctx: {
96
105
  handlers: intent.actionHandlers,
97
- reset() {
98
- return state.initializeState();
106
+ reset(defaultValue) {
107
+ return state.initializeState({
108
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue
109
+ });
99
110
  }
100
111
  }
101
112
  })));
113
+
114
+ // TODO: move on error handler to a new effect
102
115
  var formElement = dom.getFormElement(formRef);
103
116
  if (!formElement || !result.error) {
104
117
  return;
@@ -109,6 +122,15 @@ function useConform(formRef, options) {
109
122
  intent: intent$1
110
123
  });
111
124
  }, [formRef, optionsRef]);
125
+ if (options.key !== keyRef.current) {
126
+ keyRef.current = options.key;
127
+ setState(state.initializeState({
128
+ defaultValue: options.defaultValue
129
+ }));
130
+ } else if (lastResult && lastResult !== lastResultRef.current) {
131
+ lastResultRef.current = lastResult;
132
+ handleSubmission('server', lastResult, options);
133
+ }
112
134
  react.useEffect(() => {
113
135
  return () => {
114
136
  var _abortControllerRef$c;
@@ -116,42 +138,28 @@ function useConform(formRef, options) {
116
138
  (_abortControllerRef$c = abortControllerRef.current) === null || _abortControllerRef$c === void 0 || _abortControllerRef$c.abort('The component is unmounted');
117
139
  };
118
140
  }, []);
119
- react.useEffect(() => {
120
- // To avoid re-applying the same result twice
121
- if (lastResult && lastResult !== lastResultRef.current) {
122
- handleSubmission('server', lastResult);
123
- lastResultRef.current = lastResult;
124
- }
125
- }, [lastResult, handleSubmission]);
126
- react.useEffect(() => {
127
- // Reset the form state if the form key changes
128
- if (options.key !== keyRef.current) {
129
- keyRef.current = options.key;
130
- setState(state.initializeState());
131
- }
132
- }, [options.key]);
133
- react.useEffect(() => {
141
+ useSafeLayoutEffect(() => {
134
142
  var formElement = dom.getFormElement(formRef);
135
143
 
136
144
  // Reset the form values if the reset key changes
137
145
  if (formElement && state$1.resetKey !== resetKeyRef.current) {
138
146
  resetKeyRef.current = state$1.resetKey;
139
- formElement.reset();
140
- }
141
- }, [formRef, state$1.resetKey]);
142
- react.useEffect(() => {
143
- if (!state$1.clientIntendedValue) {
144
- return;
147
+ dom.resetFormValue(formElement, state$1.defaultValue, optionsRef.current.serialize);
148
+ pendingValueRef.current = undefined;
145
149
  }
146
- var formElement = dom.getFormElement(formRef);
147
- if (!formElement) {
148
- // eslint-disable-next-line no-console
149
- console.error('Failed to update form value; No form element found');
150
- return;
150
+ }, [formRef, state$1.resetKey, state$1.defaultValue, optionsRef]);
151
+ useSafeLayoutEffect(() => {
152
+ if (state$1.targetValue) {
153
+ var formElement = dom.getFormElement(formRef);
154
+ if (!formElement) {
155
+ // eslint-disable-next-line no-console
156
+ console.error('Failed to update form value; No form element found');
157
+ return;
158
+ }
159
+ dom.updateFormValue(formElement, state$1.targetValue, optionsRef.current.serialize);
151
160
  }
152
- dom.updateFormValue(formElement, state$1.clientIntendedValue, optionsRef.current.serialize);
153
- lastIntentedValueRef.current = undefined;
154
- }, [formRef, state$1.clientIntendedValue, optionsRef]);
161
+ pendingValueRef.current = undefined;
162
+ }, [formRef, state$1.targetValue, optionsRef]);
155
163
  var handleSubmit = react.useCallback(event => {
156
164
  var _abortControllerRef$c2, _lastAsyncResultRef$c;
157
165
  var abortController = new AbortController();
@@ -184,22 +192,25 @@ function useConform(formRef, options) {
184
192
  }
185
193
  }
186
194
 
187
- // Override submission value if the last intended value is not applied yet (i.e. batch updates)
188
- if (lastIntentedValueRef.current != null) {
189
- submission.payload = lastIntentedValueRef.current;
195
+ // Override submission value if the pending value is not applied yet (i.e. batch updates)
196
+ if (pendingValueRef.current !== undefined) {
197
+ submission.payload = pendingValueRef.current;
190
198
  }
191
- var intendedValue = intent.applyIntent(submission);
192
-
193
- // Update the last intended value in case there will be another intent dispatched
194
- lastIntentedValueRef.current = intendedValue === submission.payload ? undefined : intendedValue;
199
+ var value = intent.applyIntent(submission);
195
200
  var submissionResult = future.report(submission, {
196
201
  keepFiles: true,
197
- intendedValue
202
+ value
198
203
  });
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
+ }
199
210
  var validateResult =
200
211
  // Skip validation on form reset
201
- intendedValue !== null ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
202
- payload: intendedValue,
212
+ value !== undefined ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
213
+ payload: value,
203
214
  error: {
204
215
  formErrors: [],
205
216
  fieldErrors: {}
@@ -222,11 +233,11 @@ function useConform(formRef, options) {
222
233
  }
223
234
  if (typeof asyncResult !== 'undefined') {
224
235
  // Update the form when the validation result is resolved
225
- asyncResult.then(_ref => {
236
+ asyncResult.then(_ref2 => {
226
237
  var {
227
238
  error,
228
239
  value
229
- } = _ref;
240
+ } = _ref2;
230
241
  // Update the form with the validation result
231
242
  // There is no need to flush the update in this case
232
243
  if (!abortController.signal.aborted) {
@@ -290,11 +301,32 @@ function useConform(formRef, options) {
290
301
  * The main React hook for form management. Handles form state, validation, and submission
291
302
  * while providing access to form metadata, field objects, and form actions.
292
303
  *
304
+ * It can be called in two ways:
305
+ * - **Schema first**: Pass a schema as the first argument for automatic validation with type inference
306
+ * - **Manual configuration**: Pass options with custom `onValidate` handler for manual validation
307
+ *
293
308
  * @see https://conform.guide/api/react/future/useForm
294
- * @example
309
+ * @example Schema first setup with zod:
310
+ *
311
+ * ```tsx
312
+ * const { form, fields } = useForm(zodSchema, {
313
+ * lastResult,
314
+ * shouldValidate: 'onBlur',
315
+ * });
316
+ *
317
+ * return (
318
+ * <form {...form.props}>
319
+ * <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
320
+ * <div>{fields.email.errors}</div>
321
+ * </form>
322
+ * );
323
+ * ```
324
+ *
325
+ * @example Manual configuration setup with custom validation:
326
+ *
295
327
  * ```tsx
296
328
  * const { form, fields } = useForm({
297
- * onValidate({ payload, error }) {
329
+ * onValidate({ payload, error }) {
298
330
  * if (!payload.email) {
299
331
  * error.fieldErrors.email = ['Required'];
300
332
  * }
@@ -310,11 +342,25 @@ function useConform(formRef, options) {
310
342
  * );
311
343
  * ```
312
344
  */
313
- function useForm(options) {
314
- var _optionsRef$current$o4;
345
+
346
+ /**
347
+ * @deprecated Use `useForm(schema, options)` instead for better type inference.
348
+ */
349
+
350
+ function useForm(schemaOrOptions, maybeOptions) {
351
+ var _options$onError;
352
+ var schema;
353
+ var options;
354
+ if (maybeOptions) {
355
+ schema = schemaOrOptions;
356
+ options = maybeOptions;
357
+ } else {
358
+ var fullOptions = schemaOrOptions;
359
+ options = fullOptions;
360
+ schema = fullOptions.schema;
361
+ }
315
362
  var {
316
363
  id,
317
- defaultValue,
318
364
  constraint
319
365
  } = options;
320
366
  var globalOptions = react.useContext(GlobalFormOptionsContext);
@@ -325,11 +371,11 @@ function useForm(options) {
325
371
  var [state$1, handleSubmit] = useConform(formId, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, options), {}, {
326
372
  serialize: globalOptions.serialize,
327
373
  intentName: globalOptions.intentName,
328
- onError: (_optionsRef$current$o4 = optionsRef.current.onError) !== null && _optionsRef$current$o4 !== void 0 ? _optionsRef$current$o4 : dom.focusFirstInvalidField,
374
+ onError: (_options$onError = options.onError) !== null && _options$onError !== void 0 ? _options$onError : dom.focusFirstInvalidField,
329
375
  onValidate(ctx) {
330
- var _options$onValidate, _options$onValidate2;
331
- if (options.schema) {
332
- var standardResult = options.schema['~standard'].validate(ctx.payload);
376
+ var _options$onValidate, _options$onValidate2, _options;
377
+ if (schema) {
378
+ var standardResult = schema['~standard'].validate(ctx.payload);
333
379
  if (standardResult instanceof Promise) {
334
380
  return standardResult.then(actualStandardResult => {
335
381
  if (typeof options.onValidate === 'function') {
@@ -362,7 +408,7 @@ function useForm(options) {
362
408
  }
363
409
  return [validateResult.syncResult, validateResult.asyncResult];
364
410
  }
365
- return (_options$onValidate = (_options$onValidate2 = options.onValidate) === null || _options$onValidate2 === void 0 ? void 0 : _options$onValidate2.call(options, ctx)) !== null && _options$onValidate !== void 0 ? _options$onValidate : {
411
+ 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 : {
366
412
  // To avoid conform falling back to server validation,
367
413
  // if neither schema nor validation handler is provided,
368
414
  // we just treat it as a valid client submission
@@ -374,15 +420,14 @@ function useForm(options) {
374
420
  var context = react.useMemo(() => ({
375
421
  formId,
376
422
  state: state$1,
377
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : null,
378
423
  constraint: constraint !== null && constraint !== void 0 ? constraint : null,
379
- handleSubmit: handleSubmit,
424
+ handleSubmit,
380
425
  handleInput(event) {
381
- var _optionsRef$current$o5, _optionsRef$current4, _globalOptionsRef$cur;
426
+ var _optionsRef$current$o4, _optionsRef$current4, _globalOptionsRef$cur;
382
427
  if (!future.isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) {
383
428
  return;
384
429
  }
385
- (_optionsRef$current$o5 = (_optionsRef$current4 = optionsRef.current).onInput) === null || _optionsRef$current$o5 === void 0 || _optionsRef$current$o5.call(_optionsRef$current4, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, event), {}, {
430
+ (_optionsRef$current$o4 = (_optionsRef$current4 = optionsRef.current).onInput) === null || _optionsRef$current$o4 === void 0 || _optionsRef$current$o4.call(_optionsRef$current4, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, event), {}, {
386
431
  target: event.target,
387
432
  currentTarget: event.target.form
388
433
  }));
@@ -398,11 +443,11 @@ function useForm(options) {
398
443
  }
399
444
  },
400
445
  handleBlur(event) {
401
- var _optionsRef$current$o6, _optionsRef$current5, _globalOptionsRef$cur2;
446
+ var _optionsRef$current$o5, _optionsRef$current5, _globalOptionsRef$cur2;
402
447
  if (!future.isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) {
403
448
  return;
404
449
  }
405
- (_optionsRef$current$o6 = (_optionsRef$current5 = optionsRef.current).onBlur) === null || _optionsRef$current$o6 === void 0 || _optionsRef$current$o6.call(_optionsRef$current5, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, event), {}, {
450
+ (_optionsRef$current$o5 = (_optionsRef$current5 = optionsRef.current).onBlur) === null || _optionsRef$current$o5 === void 0 || _optionsRef$current$o5.call(_optionsRef$current5, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, event), {}, {
406
451
  target: event.target,
407
452
  currentTarget: event.target.form
408
453
  }));
@@ -417,7 +462,7 @@ function useForm(options) {
417
462
  intent.validate(event.target.name);
418
463
  }
419
464
  }
420
- }), [formId, state$1, defaultValue, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
465
+ }), [formId, state$1, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
421
466
  var form = react.useMemo(() => state.getFormMetadata(context, {
422
467
  serialize: globalOptions.serialize,
423
468
  customize: globalOptions.defineCustomMetadata
@@ -614,6 +659,13 @@ function useControl(options) {
614
659
  inputRef.current = null;
615
660
  } else if (future.isFieldElement(element)) {
616
661
  inputRef.current = element;
662
+
663
+ // Conform excludes hidden type inputs by default when updating form values
664
+ // Fix that by using the hidden attribute instead
665
+ if (element.type === 'hidden') {
666
+ element.hidden = true;
667
+ element.removeAttribute('type');
668
+ }
617
669
  if (shouldHandleFocus) {
618
670
  dom.makeInputFocusable(element);
619
671
  }
@@ -722,16 +774,16 @@ function useFormData(formRef, select, options) {
722
774
  var formElement = dom.getFormElement(formRef);
723
775
  if (formElement) {
724
776
  var formData = future.getFormData(formElement);
725
- formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? formData : new URLSearchParams(Array.from(formData).map(_ref2 => {
726
- var [key, value] = _ref2;
777
+ formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? formData : new URLSearchParams(Array.from(formData).map(_ref3 => {
778
+ var [key, value] = _ref3;
727
779
  return [key, value.toString()];
728
780
  }));
729
781
  }
730
782
  var unsubscribe = observer.onFormUpdate(event => {
731
783
  if (event.target === dom.getFormElement(formRef)) {
732
784
  var _formData = future.getFormData(event.target, event.submitter);
733
- formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData : new URLSearchParams(Array.from(_formData).map(_ref3 => {
734
- var [key, value] = _ref3;
785
+ formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData : new URLSearchParams(Array.from(_formData).map(_ref4 => {
786
+ var [key, value] = _ref4;
735
787
  return [key, value.toString()];
736
788
  }));
737
789
  callback();