Skip to content

Commit

Permalink
Add future.v7_normalizeFormMethod flag (#10207)
Browse files Browse the repository at this point in the history
* Add future.v7_normalizeFormMethod flag

* update Form/useSubmit types

* Updates
  • Loading branch information
brophdawg11 authored Mar 15, 2023
1 parent af4b07d commit dff7e64
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 43 deletions.
7 changes: 7 additions & 0 deletions .changeset/normalize-form-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-router": minor
"react-router-dom": minor
"@remix-run/router": minor
---

Add `future.v7_normalizeFormMethod` flag to normalize exposed `useNavigation().formMethod` and `useFetcher().formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior
2 changes: 2 additions & 0 deletions docs/routers/create-browser-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ function createBrowserRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
window?: Window;
}
): RemixRouter;
Expand Down
3 changes: 2 additions & 1 deletion docs/routers/create-memory-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ function createMemoryRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
initialEntries?: InitialEntry[];
initialIndex?: number;
window?: Window;
}
): RemixRouter;
```
Expand Down
8 changes: 4 additions & 4 deletions packages/react-router-dom/dom.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FormEncType, FormMethod } from "@remix-run/router";
import type { FormEncType, HTMLFormMethod } from "@remix-run/router";
import type { RelativeRoutingType } from "react-router";

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

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

/**
* The action URL path used to submit the form. Overrides `<form action>`.
Expand Down
33 changes: 19 additions & 14 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import type {
Fetcher,
FormEncType,
FormMethod,
FutureConfig,
GetScrollRestorationKeyFunction,
HashHistory,
History,
HTMLFormMethod,
HydrationState,
Router as RemixRouter,
V7_FormMethod,
} from "@remix-run/router";
import {
createRouter,
Expand Down Expand Up @@ -71,6 +74,7 @@ export type {
ParamKeyValuePair,
SubmitOptions,
URLSearchParamsInit,
V7_FormMethod,
};
export { createSearchParams };

Expand Down Expand Up @@ -199,16 +203,20 @@ declare global {
//#region Routers
////////////////////////////////////////////////////////////////////////////////

interface DOMRouterOpts {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
window?: Window;
}

export function createBrowserRouter(
routes: RouteObject[],
opts?: {
basename?: string;
hydrationData?: HydrationState;
window?: Window;
}
opts?: DOMRouterOpts
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: opts?.future,
history: createBrowserHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
Expand All @@ -218,14 +226,11 @@ export function createBrowserRouter(

export function createHashRouter(
routes: RouteObject[],
opts?: {
basename?: string;
hydrationData?: HydrationState;
window?: Window;
}
opts?: DOMRouterOpts
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: opts?.future,
history: createHashHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
Expand Down Expand Up @@ -611,7 +616,7 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
* The HTTP verb to use when the form is submit. Supports "get", "post",
* "put", "delete", "patch".
*/
method?: FormMethod;
method?: HTMLFormMethod;

/**
* Normal `<form action>` but supports React Router's relative paths.
Expand Down Expand Up @@ -696,7 +701,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
forwardedRef
) => {
let submit = useSubmitImpl(fetcherKey, routeId);
let formMethod: FormMethod =
let formMethod: HTMLFormMethod =
method.toLowerCase() === "get" ? "get" : "post";
let formAction = useFormAction(action, { relative });
let submitHandler: React.FormEventHandler<HTMLFormElement> = (event) => {
Expand All @@ -708,7 +713,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
.submitter as HTMLFormSubmitter | null;

let submitMethod =
(submitter?.getAttribute("formmethod") as FormMethod | undefined) ||
(submitter?.getAttribute("formmethod") as HTMLFormMethod | undefined) ||
method;

submit(submitter || event.currentTarget, {
Expand Down Expand Up @@ -965,7 +970,7 @@ function useSubmitImpl(fetcherKey?: string, routeId?: string): SubmitFunction {
replace: options.replace,
preventScrollReset: options.preventScrollReset,
formData,
formMethod: method as FormMethod,
formMethod: method as HTMLFormMethod,
formEncType: encType as FormEncType,
};
if (fetcherKey) {
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
To,
InitialEntry,
LazyRouteFunction,
FutureConfig,
} from "@remix-run/router";
import {
AbortedDeferredError,
Expand Down Expand Up @@ -233,13 +234,15 @@ export function createMemoryRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
initialEntries?: InitialEntry[];
initialIndex?: number;
}
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: opts?.future,
history: createMemoryHistory({
initialEntries: opts?.initialEntries,
initialIndex: opts?.initialIndex,
Expand Down
115 changes: 115 additions & 0 deletions packages/router/__tests__/router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
RouterNavigateOptions,
StaticHandler,
StaticHandlerContext,
FutureConfig,
} from "../index";
import {
createMemoryHistory,
Expand Down Expand Up @@ -289,6 +290,7 @@ type SetupOpts = {
initialEntries?: InitialEntry[];
initialIndex?: number;
hydrationData?: HydrationState;
future?: FutureConfig;
};

function setup({
Expand All @@ -297,6 +299,7 @@ function setup({
initialEntries,
initialIndex,
hydrationData,
future,
}: SetupOpts) {
let guid = 0;
// Global "active" helpers, keyed by navType:guid:loaderOrAction:routeId.
Expand Down Expand Up @@ -424,6 +427,7 @@ function setup({
history,
routes: enhanceRoutes(routes),
hydrationData,
future,
}).initialize();

function getRouteHelpers(
Expand Down Expand Up @@ -3620,6 +3624,117 @@ describe("a router", () => {
"childIndex",
]);
});

describe("formMethod casing", () => {
it("normalizes to lowercase in v6", async () => {
let t = setup({
routes: [
{
id: "root",
path: "/",
children: [
{
id: "child",
path: "child",
loader: true,
action: true,
},
],
},
],
});
let A = await t.navigate("/child", {
formMethod: "get",
formData: createFormData({}),
});
expect(t.router.state.navigation.formMethod).toBe("get");
await A.loaders.child.resolve("LOADER");
expect(t.router.state.navigation.formMethod).toBeUndefined();
await t.router.navigate("/");

let B = await t.navigate("/child", {
formMethod: "POST",
formData: createFormData({}),
});
expect(t.router.state.navigation.formMethod).toBe("post");
await B.actions.child.resolve("ACTION");
await B.loaders.child.resolve("LOADER");
expect(t.router.state.navigation.formMethod).toBeUndefined();
await t.router.navigate("/");

let C = await t.fetch("/child", "key", {
formMethod: "GET",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("get");
await C.loaders.child.resolve("LOADER FETCH");
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();

let D = await t.fetch("/child", "key", {
formMethod: "post",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("post");
await D.actions.child.resolve("ACTION FETCH");
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
});

it("normalizes to uppercase in v7 via v7_normalizeFormMethod", async () => {
let t = setup({
routes: [
{
id: "root",
path: "/",
children: [
{
id: "child",
path: "child",
loader: true,
action: true,
},
],
},
],
future: {
v7_normalizeFormMethod: true,
},
});
let A = await t.navigate("/child", {
formMethod: "get",
formData: createFormData({}),
});
expect(t.router.state.navigation.formMethod).toBe("GET");
await A.loaders.child.resolve("LOADER");
expect(t.router.state.navigation.formMethod).toBeUndefined();
await t.router.navigate("/");

let B = await t.navigate("/child", {
formMethod: "POST",
formData: createFormData({}),
});
expect(t.router.state.navigation.formMethod).toBe("POST");
await B.actions.child.resolve("ACTION");
await B.loaders.child.resolve("LOADER");
expect(t.router.state.navigation.formMethod).toBeUndefined();
await t.router.navigate("/");

let C = await t.fetch("/child", "key", {
formMethod: "GET",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("GET");
await C.loaders.child.resolve("LOADER FETCH");
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();

let D = await t.fetch("/child", "key", {
formMethod: "post",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get("key")?.formMethod).toBe("POST");
await D.actions.child.resolve("ACTION FETCH");
expect(t.router.state.fetchers.get("key")?.formMethod).toBeUndefined();
});
});
});

describe("action errors", () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type {
TrackedPromise,
FormEncType,
FormMethod,
HTMLFormMethod,
JsonFunction,
LoaderFunction,
LoaderFunctionArgs,
Expand All @@ -22,7 +23,7 @@ export type {
PathPattern,
RedirectFunction,
ShouldRevalidateFunction,
Submission,
V7_FormMethod,
} from "./utils";

export {
Expand Down
Loading

0 comments on commit dff7e64

Please sign in to comment.