Skip to content

Commit

Permalink
fix: respect basename in useFormAction (#9352)
Browse files Browse the repository at this point in the history
* fix: respect basename in useFormAction

* Add changeset

* Update changeset

* change assertion
  • Loading branch information
brophdawg11 committed Sep 30, 2022
1 parent 779d4af commit 434003d
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-colts-serve.md
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

fix: respect `basename` in `useFormAction` (#9352)
194 changes: 190 additions & 4 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Expand Up @@ -41,9 +41,9 @@ testDomRouter("<DataBrowserRouter>", createBrowserRouter, (url) =>
getWindowImpl(url, false)
);

testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);
// testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
// getWindowImpl(url, true)
// );

let router: Router | null = null;

Expand Down Expand Up @@ -433,7 +433,7 @@ function testDomRouter(

it("handles link navigations when using a basename", async () => {
let testWindow = getWindow("/base/name/foo");
render(
let { container } = render(
<TestDataRouter
basename="/base/name"
window={testWindow}
Expand All @@ -457,6 +457,25 @@ function testDomRouter(
}

assertLocation(testWindow, "/base/name/foo");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<div>
<a
href=\\"/base/name/foo\\"
>
Link to Foo
</a>
<a
href=\\"/base/name/bar\\"
>
Link to Bar
</a>
<h1>
Foo Heading
</h1>
</div>
</div>"
`);

expect(screen.getByText("Foo Heading")).toBeDefined();
fireEvent.click(screen.getByText("Link to Bar"));
Expand Down Expand Up @@ -1329,6 +1348,173 @@ function testDomRouter(
`);
});

it('supports a basename on <Form method="get">', async () => {
let testWindow = getWindow("/base/path");
let { container } = render(
<TestDataRouter basename="/base" window={testWindow} hydrationData={{}}>
<Route path="path" element={<Comp />} />
</TestDataRouter>
);

function Comp() {
let location = useLocation();
return (
<Form
onSubmit={(e) => {
// jsdom doesn't handle submitter so we add it here
// See https://github.com/jsdom/jsdom/issues/3117
// @ts-expect-error
e.nativeEvent.submitter = e.currentTarget.querySelector("button");
}}
>
<p>{location.pathname + location.search}</p>
<input name="a" defaultValue="1" />
<button type="submit" name="b" value="2">
Submit
</button>
</Form>
);
}

assertLocation(testWindow, "/base/path");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<form
action=\\"/base/path\\"
method=\\"get\\"
>
<p>
/path
</p>
<input
name=\\"a\\"
value=\\"1\\"
/>
<button
name=\\"b\\"
type=\\"submit\\"
value=\\"2\\"
>
Submit
</button>
</form>
</div>"
`);

fireEvent.click(screen.getByText("Submit"));
assertLocation(testWindow, "/base/path", "?a=1&b=2");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<form
action=\\"/base/path?a=1&b=2\\"
method=\\"get\\"
>
<p>
/path?a=1&b=2
</p>
<input
name=\\"a\\"
value=\\"1\\"
/>
<button
name=\\"b\\"
type=\\"submit\\"
value=\\"2\\"
>
Submit
</button>
</form>
</div>"
`);
});

it('supports a basename on <Form method="post">', async () => {
let testWindow = getWindow("/base/path");
let { container } = render(
<TestDataRouter basename="/base" window={testWindow} hydrationData={{}}>
<Route path="path" action={() => "action data"} element={<Comp />} />
</TestDataRouter>
);

function Comp() {
let location = useLocation();
let data = useActionData() as string | undefined;
return (
<Form
method="post"
onSubmit={(e) => {
// jsdom doesn't handle submitter so we add it here
// See https://github.com/jsdom/jsdom/issues/3117
// @ts-expect-error
e.nativeEvent.submitter = e.currentTarget.querySelector("button");
}}
>
<p>{location.pathname + location.search}</p>
{data && <p>{data}</p>}
<input name="a" defaultValue="1" />
<button type="submit" name="b" value="2">
Submit
</button>
</Form>
);
}

assertLocation(testWindow, "/base/path");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<form
action=\\"/base/path\\"
method=\\"post\\"
>
<p>
/path
</p>
<input
name=\\"a\\"
value=\\"1\\"
/>
<button
name=\\"b\\"
type=\\"submit\\"
value=\\"2\\"
>
Submit
</button>
</form>
</div>"
`);

fireEvent.click(screen.getByText("Submit"));
await waitFor(() => screen.getByText("action data"));
assertLocation(testWindow, "/base/path");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<form
action=\\"/base/path\\"
method=\\"post\\"
>
<p>
/path
</p>
<p>
action data
</p>
<input
name=\\"a\\"
value=\\"1\\"
/>
<button
name=\\"b\\"
type=\\"submit\\"
value=\\"2\\"
>
Submit
</button>
</form>
</div>"
`);
});

describe("<Form action>", () => {
function NoActionComponent() {
return (
Expand Down
16 changes: 15 additions & 1 deletion packages/react-router-dom/index.tsx
Expand Up @@ -21,6 +21,7 @@ import {
useResolvedPath,
UNSAFE_DataRouterContext as DataRouterContext,
UNSAFE_DataRouterStateContext as DataRouterStateContext,
UNSAFE_NavigationContext as NavigationContext,
UNSAFE_RouteContext as RouteContext,
UNSAFE_enhanceManualRouteObjects as enhanceManualRouteObjects,
} from "react-router";
Expand All @@ -40,6 +41,7 @@ import {
createBrowserHistory,
createHashHistory,
invariant,
joinPaths,
matchPath,
} from "@remix-run/router";

Expand Down Expand Up @@ -858,12 +860,15 @@ export function useFormAction(
action?: string,
{ relative }: { relative?: RelativeRoutingType } = {}
): string {
let { basename } = React.useContext(NavigationContext);
let routeContext = React.useContext(RouteContext);
invariant(routeContext, "useFormAction must be used inside a RouteContext");

let [match] = routeContext.matches.slice(-1);
let resolvedAction = action ?? ".";
let path = useResolvedPath(resolvedAction, { relative });
// Shallow clone path so we can modify it below, otherwise we modify the
// object referenced by useMemo inside useResolvedPath
let path = { ...useResolvedPath(resolvedAction, { relative }) };

// Previously we set the default action to ".". The problem with this is that
// `useResolvedPath(".")` excludes search params and the hash of the resolved
Expand Down Expand Up @@ -894,6 +899,15 @@ export function useFormAction(
: "?index";
}

// If we're operating within a basename, prepend it to the pathname prior
// to creating the form action. If this is a root navigation, then just use
// the raw basename which allows the basename to have full control over the
// presence of a trailing slash on root actions
if (basename !== "/") {
path.pathname =
path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
}

return createPath(path);
}

Expand Down

0 comments on commit 434003d

Please sign in to comment.