@conform-to/react 1.13.3 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/future/dom.d.ts +5 -4
- package/dist/future/dom.js +22 -8
- package/dist/future/dom.mjs +23 -10
- package/dist/future/hooks.d.ts +56 -11
- package/dist/future/hooks.js +120 -68
- package/dist/future/hooks.mjs +121 -69
- package/dist/future/intent.d.ts +1 -1
- package/dist/future/intent.js +10 -14
- package/dist/future/intent.mjs +12 -16
- package/dist/future/state.d.ts +23 -15
- package/dist/future/state.js +53 -20
- package/dist/future/state.mjs +53 -21
- package/dist/future/types.d.ts +63 -46
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Version 1.
|
|
10
|
+
Version 1.14.1 / 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
|
|
package/dist/future/dom.d.ts
CHANGED
|
@@ -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,
|
|
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.
|
package/dist/future/dom.js
CHANGED
|
@@ -173,22 +173,35 @@ function focusFirstInvalidField(ctx) {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
function updateFormValue(form,
|
|
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(
|
|
180
|
-
if (element.type === 'file' &&
|
|
181
|
-
// Do not update file inputs unless there's
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
package/dist/future/dom.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath,
|
|
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,
|
|
172
|
+
function updateFormValue(form, targetValue, serialize) {
|
|
173
173
|
for (var element of form.elements) {
|
|
174
|
-
if (isFieldElement(element) && element.name) {
|
|
175
|
-
var fieldValue = getValueAtPath(
|
|
176
|
-
if (element.type === 'file' &&
|
|
177
|
-
// Do not update file inputs unless there's
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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 };
|
package/dist/future/hooks.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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<
|
|
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>;
|
package/dist/future/hooks.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
188
|
-
if (
|
|
189
|
-
submission.payload =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
payload:
|
|
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(
|
|
236
|
+
asyncResult.then(_ref2 => {
|
|
226
237
|
var {
|
|
227
238
|
error,
|
|
228
239
|
value
|
|
229
|
-
} =
|
|
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
|
-
*
|
|
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
|
-
|
|
314
|
-
|
|
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: (
|
|
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 (
|
|
332
|
-
var standardResult =
|
|
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(
|
|
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
|
|
424
|
+
handleSubmit,
|
|
380
425
|
handleInput(event) {
|
|
381
|
-
var _optionsRef$current$
|
|
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$
|
|
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$
|
|
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$
|
|
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,
|
|
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(
|
|
726
|
-
var [key, value] =
|
|
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(
|
|
734
|
-
var [key, value] =
|
|
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();
|