Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: respect basename in useFormAction #9352

Merged
merged 4 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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