Skip to content

Commit 908a40a

Browse files
authored
Ensure Form contains splat portion of pathname when no action is specified (#10933)
1 parent a71b4e2 commit 908a40a

File tree

3 files changed

+52
-7
lines changed

3 files changed

+52
-7
lines changed

.changeset/splat-form-action.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Ensure `<Form>` default action contains splat portion of pathname when no `action` is specified

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2894,7 +2894,7 @@ function testDomRouter(
28942894
let { container } = render(<RouterProvider router={router} />);
28952895

28962896
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2897-
"/foo?a=1"
2897+
"/foo/bar?a=1"
28982898
);
28992899
});
29002900

@@ -2937,6 +2937,44 @@ function testDomRouter(
29372937
"/foo"
29382938
);
29392939
});
2940+
2941+
it("includes splat portion of path when no action is specified (inline splat)", async () => {
2942+
let router = createTestRouter(
2943+
createRoutesFromElements(
2944+
<Route path="/">
2945+
<Route path="foo">
2946+
<Route path="*" element={<NoActionComponent />} />
2947+
</Route>
2948+
</Route>
2949+
),
2950+
{
2951+
window: getWindow("/foo/bar/baz"),
2952+
}
2953+
);
2954+
let { container } = render(<RouterProvider router={router} />);
2955+
2956+
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2957+
"/foo/bar/baz"
2958+
);
2959+
});
2960+
2961+
it("includes splat portion of path when no action is specified (nested splat)", async () => {
2962+
let router = createTestRouter(
2963+
createRoutesFromElements(
2964+
<Route path="/">
2965+
<Route path="foo/*" element={<NoActionComponent />} />
2966+
</Route>
2967+
),
2968+
{
2969+
window: getWindow("/foo/bar/baz"),
2970+
}
2971+
);
2972+
let { container } = render(<RouterProvider router={router} />);
2973+
2974+
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2975+
"/foo/bar/baz"
2976+
);
2977+
});
29402978
});
29412979

29422980
it("allows user to specify search params and hash", async () => {

packages/react-router-dom/index.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,18 +1457,20 @@ export function useFormAction(
14571457
let { basename } = React.useContext(NavigationContext);
14581458
let routeContext = React.useContext(RouteContext);
14591459
invariant(routeContext, "useFormAction must be used inside a RouteContext");
1460+
let location = useLocation();
14601461

14611462
let [match] = routeContext.matches.slice(-1);
14621463
// Shallow clone path so we can modify it below, otherwise we modify the
14631464
// object referenced by useMemo inside useResolvedPath
1464-
let path = { ...useResolvedPath(action ? action : ".", { relative }) };
1465+
let path = {
1466+
...useResolvedPath(action != null ? action : location.pathname, {
1467+
relative,
1468+
}),
1469+
};
14651470

1466-
// Previously we set the default action to ".". The problem with this is that
1467-
// `useResolvedPath(".")` excludes search params of the resolved URL. This is
1468-
// the intended behavior of when "." is specifically provided as
1469-
// the form action, but inconsistent w/ browsers when the action is omitted.
1471+
// If no action was specified, browsers will persist current search params
1472+
// when determining the path, so match that behavior
14701473
// https://github.com/remix-run/remix/issues/927
1471-
let location = useLocation();
14721474
if (action == null) {
14731475
// Safe to write to this directly here since if action was undefined, we
14741476
// would have called useResolvedPath(".") which will never include a search

0 commit comments

Comments
 (0)