Skip to content

Commit dff7e64

Browse files
authored
Add future.v7_normalizeFormMethod flag (#10207)
* Add future.v7_normalizeFormMethod flag * update Form/useSubmit types * Updates
1 parent af4b07d commit dff7e64

File tree

10 files changed

+228
-43
lines changed

10 files changed

+228
-43
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"react-router": minor
3+
"react-router-dom": minor
4+
"@remix-run/router": minor
5+
---
6+
7+
Add `future.v7_normalizeFormMethod` flag to normalize exposed `useNavigation().formMethod` and `useFetcher().formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior

docs/routers/create-browser-router.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ function createBrowserRouter(
4747
routes: RouteObject[],
4848
opts?: {
4949
basename?: string;
50+
future?: FutureConfig;
51+
hydrationData?: HydrationState;
5052
window?: Window;
5153
}
5254
): RemixRouter;

docs/routers/create-memory-router.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ function createMemoryRouter(
5252
routes: RouteObject[],
5353
opts?: {
5454
basename?: string;
55+
future?: FutureConfig;
56+
hydrationData?: HydrationState;
5557
initialEntries?: InitialEntry[];
5658
initialIndex?: number;
57-
window?: Window;
5859
}
5960
): RemixRouter;
6061
```

packages/react-router-dom/dom.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { FormEncType, FormMethod } from "@remix-run/router";
1+
import type { FormEncType, HTMLFormMethod } from "@remix-run/router";
22
import type { RelativeRoutingType } from "react-router";
33

4-
export const defaultMethod = "get";
5-
const defaultEncType = "application/x-www-form-urlencoded";
4+
export const defaultMethod: HTMLFormMethod = "get";
5+
const defaultEncType: FormEncType = "application/x-www-form-urlencoded";
66

77
export function isHtmlElement(object: any): object is HTMLElement {
88
return object != null && typeof object.tagName === "string";
@@ -110,7 +110,7 @@ export interface SubmitOptions {
110110
* The HTTP method used to submit the form. Overrides `<form method>`.
111111
* Defaults to "GET".
112112
*/
113-
method?: FormMethod;
113+
method?: HTMLFormMethod;
114114

115115
/**
116116
* The action URL path used to submit the form. Overrides `<form action>`.

packages/react-router-dom/index.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ import type {
3030
Fetcher,
3131
FormEncType,
3232
FormMethod,
33+
FutureConfig,
3334
GetScrollRestorationKeyFunction,
3435
HashHistory,
3536
History,
37+
HTMLFormMethod,
3638
HydrationState,
3739
Router as RemixRouter,
40+
V7_FormMethod,
3841
} from "@remix-run/router";
3942
import {
4043
createRouter,
@@ -71,6 +74,7 @@ export type {
7174
ParamKeyValuePair,
7275
SubmitOptions,
7376
URLSearchParamsInit,
77+
V7_FormMethod,
7478
};
7579
export { createSearchParams };
7680

@@ -199,16 +203,20 @@ declare global {
199203
//#region Routers
200204
////////////////////////////////////////////////////////////////////////////////
201205

206+
interface DOMRouterOpts {
207+
basename?: string;
208+
future?: FutureConfig;
209+
hydrationData?: HydrationState;
210+
window?: Window;
211+
}
212+
202213
export function createBrowserRouter(
203214
routes: RouteObject[],
204-
opts?: {
205-
basename?: string;
206-
hydrationData?: HydrationState;
207-
window?: Window;
208-
}
215+
opts?: DOMRouterOpts
209216
): RemixRouter {
210217
return createRouter({
211218
basename: opts?.basename,
219+
future: opts?.future,
212220
history: createBrowserHistory({ window: opts?.window }),
213221
hydrationData: opts?.hydrationData || parseHydrationData(),
214222
routes,
@@ -218,14 +226,11 @@ export function createBrowserRouter(
218226

219227
export function createHashRouter(
220228
routes: RouteObject[],
221-
opts?: {
222-
basename?: string;
223-
hydrationData?: HydrationState;
224-
window?: Window;
225-
}
229+
opts?: DOMRouterOpts
226230
): RemixRouter {
227231
return createRouter({
228232
basename: opts?.basename,
233+
future: opts?.future,
229234
history: createHashHistory({ window: opts?.window }),
230235
hydrationData: opts?.hydrationData || parseHydrationData(),
231236
routes,
@@ -611,7 +616,7 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
611616
* The HTTP verb to use when the form is submit. Supports "get", "post",
612617
* "put", "delete", "patch".
613618
*/
614-
method?: FormMethod;
619+
method?: HTMLFormMethod;
615620

616621
/**
617622
* Normal `<form action>` but supports React Router's relative paths.
@@ -696,7 +701,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
696701
forwardedRef
697702
) => {
698703
let submit = useSubmitImpl(fetcherKey, routeId);
699-
let formMethod: FormMethod =
704+
let formMethod: HTMLFormMethod =
700705
method.toLowerCase() === "get" ? "get" : "post";
701706
let formAction = useFormAction(action, { relative });
702707
let submitHandler: React.FormEventHandler<HTMLFormElement> = (event) => {
@@ -708,7 +713,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
708713
.submitter as HTMLFormSubmitter | null;
709714

710715
let submitMethod =
711-
(submitter?.getAttribute("formmethod") as FormMethod | undefined) ||
716+
(submitter?.getAttribute("formmethod") as HTMLFormMethod | undefined) ||
712717
method;
713718

714719
submit(submitter || event.currentTarget, {
@@ -965,7 +970,7 @@ function useSubmitImpl(fetcherKey?: string, routeId?: string): SubmitFunction {
965970
replace: options.replace,
966971
preventScrollReset: options.preventScrollReset,
967972
formData,
968-
formMethod: method as FormMethod,
973+
formMethod: method as HTMLFormMethod,
969974
formEncType: encType as FormEncType,
970975
};
971976
if (fetcherKey) {

packages/react-router/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
To,
2222
InitialEntry,
2323
LazyRouteFunction,
24+
FutureConfig,
2425
} from "@remix-run/router";
2526
import {
2627
AbortedDeferredError,
@@ -233,13 +234,15 @@ export function createMemoryRouter(
233234
routes: RouteObject[],
234235
opts?: {
235236
basename?: string;
237+
future?: FutureConfig;
236238
hydrationData?: HydrationState;
237239
initialEntries?: InitialEntry[];
238240
initialIndex?: number;
239241
}
240242
): RemixRouter {
241243
return createRouter({
242244
basename: opts?.basename,
245+
future: opts?.future,
243246
history: createMemoryHistory({
244247
initialEntries: opts?.initialEntries,
245248
initialIndex: opts?.initialIndex,

packages/router/__tests__/router-test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
RouterNavigateOptions,
1111
StaticHandler,
1212
StaticHandlerContext,
13+
FutureConfig,
1314
} from "../index";
1415
import {
1516
createMemoryHistory,
@@ -289,6 +290,7 @@ type SetupOpts = {
289290
initialEntries?: InitialEntry[];
290291
initialIndex?: number;
291292
hydrationData?: HydrationState;
293+
future?: FutureConfig;
292294
};
293295

294296
function setup({
@@ -297,6 +299,7 @@ function setup({
297299
initialEntries,
298300
initialIndex,
299301
hydrationData,
302+
future,
300303
}: SetupOpts) {
301304
let guid = 0;
302305
// Global "active" helpers, keyed by navType:guid:loaderOrAction:routeId.
@@ -424,6 +427,7 @@ function setup({
424427
history,
425428
routes: enhanceRoutes(routes),
426429
hydrationData,
430+
future,
427431
}).initialize();
428432

429433
function getRouteHelpers(
@@ -3620,6 +3624,117 @@ describe("a router", () => {
36203624
"childIndex",
36213625
]);
36223626
});
3627+
3628+
describe("formMethod casing", () => {
3629+
it("normalizes to lowercase in v6", async () => {
3630+
let t = setup({
3631+
routes: [
3632+
{
3633+
id: "root",
3634+
path: "/",
3635+
children: [
3636+
{
3637+
id: "child",
3638+
path: "child",
3639+
loader: true,
3640+
action: true,
3641+
},
3642+
],
3643+
},
3644+
],
3645+
});
3646+
let A = await t.navigate("/child", {
3647+
formMethod: "get",
3648+
formData: createFormData({}),
3649+
});
3650+
expect(t.router.state.navigation.formMethod).toBe("get");
3651+
await A.loaders.child.resolve("LOADER");
3652+
expect(t.router.state.navigation.formMethod).toBeUndefined();
3653+
await t.router.navigate("/");
3654+
3655+
let B = await t.navigate("/child", {
3656+
formMethod: "POST",
3657+
formData: createFormData({}),
3658+
});
3659+
expect(t.router.state.navigation.formMethod).toBe("post");
3660+
await B.actions.child.resolve("ACTION");
3661+
await B.loaders.child.resolve("LOADER");
3662+
expect(t.router.state.navigation.formMethod).toBeUndefined();
3663+
await t.router.navigate("/");
3664+
3665+
let C = await t.fetch("/child", "key", {
3666+
formMethod: "GET",
3667+
formData: createFormData({}),
3668+
});
3669+
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("get");
3670+
await C.loaders.child.resolve("LOADER FETCH");
3671+
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
3672+
3673+
let D = await t.fetch("/child", "key", {
3674+
formMethod: "post",
3675+
formData: createFormData({}),
3676+
});
3677+
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("post");
3678+
await D.actions.child.resolve("ACTION FETCH");
3679+
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
3680+
});
3681+
3682+
it("normalizes to uppercase in v7 via v7_normalizeFormMethod", async () => {
3683+
let t = setup({
3684+
routes: [
3685+
{
3686+
id: "root",
3687+
path: "/",
3688+
children: [
3689+
{
3690+
id: "child",
3691+
path: "child",
3692+
loader: true,
3693+
action: true,
3694+
},
3695+
],
3696+
},
3697+
],
3698+
future: {
3699+
v7_normalizeFormMethod: true,
3700+
},
3701+
});
3702+
let A = await t.navigate("/child", {
3703+
formMethod: "get",
3704+
formData: createFormData({}),
3705+
});
3706+
expect(t.router.state.navigation.formMethod).toBe("GET");
3707+
await A.loaders.child.resolve("LOADER");
3708+
expect(t.router.state.navigation.formMethod).toBeUndefined();
3709+
await t.router.navigate("/");
3710+
3711+
let B = await t.navigate("/child", {
3712+
formMethod: "POST",
3713+
formData: createFormData({}),
3714+
});
3715+
expect(t.router.state.navigation.formMethod).toBe("POST");
3716+
await B.actions.child.resolve("ACTION");
3717+
await B.loaders.child.resolve("LOADER");
3718+
expect(t.router.state.navigation.formMethod).toBeUndefined();
3719+
await t.router.navigate("/");
3720+
3721+
let C = await t.fetch("/child", "key", {
3722+
formMethod: "GET",
3723+
formData: createFormData({}),
3724+
});
3725+
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("GET");
3726+
await C.loaders.child.resolve("LOADER FETCH");
3727+
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
3728+
3729+
let D = await t.fetch("/child", "key", {
3730+
formMethod: "post",
3731+
formData: createFormData({}),
3732+
});
3733+
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("POST");
3734+
await D.actions.child.resolve("ACTION FETCH");
3735+
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
3736+
});
3737+
});
36233738
});
36243739

36253740
describe("action errors", () => {

packages/router/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type {
1313
TrackedPromise,
1414
FormEncType,
1515
FormMethod,
16+
HTMLFormMethod,
1617
JsonFunction,
1718
LoaderFunction,
1819
LoaderFunctionArgs,
@@ -22,7 +23,7 @@ export type {
2223
PathPattern,
2324
RedirectFunction,
2425
ShouldRevalidateFunction,
25-
Submission,
26+
V7_FormMethod,
2627
} from "./utils";
2728

2829
export {

0 commit comments

Comments
 (0)