Skip to content

Commit 108e5c2

Browse files
feat: GET forms now expose a submission on the loading navigation (#9695)
* feat: GET forms now expose a submission on the loading navigation * Enhance fetcher loader submission test Co-authored-by: Matt Brophy <[email protected]>
1 parent c6c8c3b commit 108e5c2

File tree

4 files changed

+87
-43
lines changed

4 files changed

+87
-43
lines changed

.changeset/thin-kids-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
GET forms now expose a submission on the loading navigation

packages/router/__tests__/router-test.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3372,9 +3372,9 @@ describe("a router", () => {
33723372
});
33733373
let navigation = t.router.state.navigation;
33743374
expect(navigation.state).toBe("loading");
3375-
expect(navigation.formData).toBeUndefined();
3376-
expect(navigation.formMethod).toBeUndefined();
3377-
expect(navigation.formEncType).toBeUndefined();
3375+
expect(navigation.formData).toEqual(createFormData({ gosh: "dang" }));
3376+
expect(navigation.formMethod).toBe("get");
3377+
expect(navigation.formEncType).toBe("application/x-www-form-urlencoded");
33783378
expect(navigation.location).toMatchObject({
33793379
pathname: "/foo",
33803380
search: "?gosh=dang",
@@ -3400,9 +3400,9 @@ describe("a router", () => {
34003400

34013401
let navigation = t.router.state.navigation;
34023402
expect(navigation.state).toBe("loading");
3403-
expect(navigation.formData).toBeUndefined();
3404-
expect(navigation.formMethod).toBeUndefined();
3405-
expect(navigation.formEncType).toBeUndefined();
3403+
expect(navigation.formData).toEqual(createFormData({ gosh: "dang" }));
3404+
expect(navigation.formMethod).toBe("get");
3405+
expect(navigation.formEncType).toBe("application/x-www-form-urlencoded");
34063406
expect(navigation.location?.pathname).toBe("/bar");
34073407

34083408
await B.loaders.bar.resolve("B");
@@ -3872,8 +3872,10 @@ describe("a router", () => {
38723872
pathname: "/tasks",
38733873
search: "?key=value",
38743874
});
3875-
expect(t.router.state.navigation.formMethod).toBeUndefined();
3876-
expect(t.router.state.navigation.formData).toBeUndefined();
3875+
expect(t.router.state.navigation.formMethod).toBe("get");
3876+
expect(t.router.state.navigation.formData).toEqual(
3877+
createFormData({ key: "value" })
3878+
);
38773879
});
38783880

38793881
it("converts formData to URLSearchParams for formMethod=get", async () => {
@@ -3890,8 +3892,10 @@ describe("a router", () => {
38903892
pathname: "/tasks",
38913893
search: "?key=value",
38923894
});
3893-
expect(t.router.state.navigation.formMethod).toBeUndefined();
3894-
expect(t.router.state.navigation.formData).toBeUndefined();
3895+
expect(t.router.state.navigation.formMethod).toBe("get");
3896+
expect(t.router.state.navigation.formData).toEqual(
3897+
createFormData({ key: "value" })
3898+
);
38953899
});
38963900

38973901
it("does not preserve existing 'action' URLSearchParams for formMethod='get'", async () => {
@@ -3908,8 +3912,10 @@ describe("a router", () => {
39083912
pathname: "/tasks",
39093913
search: "?key=2",
39103914
});
3911-
expect(t.router.state.navigation.formMethod).toBeUndefined();
3912-
expect(t.router.state.navigation.formData).toBeUndefined();
3915+
expect(t.router.state.navigation.formMethod).toBe("get");
3916+
expect(t.router.state.navigation.formData).toEqual(
3917+
createFormData({ key: "2" })
3918+
);
39133919
});
39143920

39153921
it("preserves existing 'action' URLSearchParams for formMethod='post'", async () => {
@@ -4656,6 +4662,20 @@ describe("a router", () => {
46564662
signal: nav3.loaders.tasks.stub.mock.calls[0][0].request.signal,
46574663
}),
46584664
});
4665+
4666+
let nav4 = await t.navigate("/tasks#hash", {
4667+
formData: createFormData({ foo: "bar" }),
4668+
});
4669+
expect(nav4.loaders.tasks.stub).toHaveBeenCalledWith({
4670+
params: {},
4671+
request: new Request("http://localhost/tasks?foo=bar", {
4672+
signal: nav4.loaders.tasks.stub.mock.calls[0][0].request.signal,
4673+
}),
4674+
});
4675+
4676+
expect(t.router.state.navigation.formAction).toBe("/tasks");
4677+
expect(t.router.state.navigation?.location?.pathname).toBe("/tasks");
4678+
expect(t.router.state.navigation?.location?.search).toBe("?foo=bar");
46594679
});
46604680

46614681
it("handles errors thrown from loaders", async () => {
@@ -6165,8 +6185,8 @@ describe("a router", () => {
61656185
pathname: "/tasks",
61666186
search: "?key=value",
61676187
},
6168-
formMethod: undefined,
6169-
formData: undefined,
6188+
formMethod: "get",
6189+
formData: createFormData({ key: "value" }),
61706190
},
61716191
revalidation: "loading",
61726192
loaderData: {
@@ -6933,6 +6953,10 @@ describe("a router", () => {
69336953
formData: createFormData({ key: "value" }),
69346954
});
69356955
expect(A.fetcher.state).toBe("loading");
6956+
expect(A.fetcher.formMethod).toBe("get");
6957+
expect(A.fetcher.formAction).toBe("/foo");
6958+
expect(A.fetcher.formData).toEqual(createFormData({ key: "value" }));
6959+
expect(A.fetcher.formEncType).toBe("application/x-www-form-urlencoded");
69366960
expect(
69376961
new URL(
69386962
A.loaders.foo.stub.mock.calls[0][0].request.url

packages/router/router.ts

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
Submission,
2222
SuccessResult,
2323
AgnosticRouteMatch,
24-
SubmissionFormMethod,
24+
MutationFormMethod,
2525
} from "./utils";
2626
import {
2727
DeferredData,
@@ -521,15 +521,20 @@ interface QueryRouteResponse {
521521
response: Response;
522522
}
523523

524-
const validActionMethodsArr: SubmissionFormMethod[] = [
524+
const validMutationMethodsArr: MutationFormMethod[] = [
525525
"post",
526526
"put",
527527
"patch",
528528
"delete",
529529
];
530-
const validActionMethods = new Set<SubmissionFormMethod>(validActionMethodsArr);
530+
const validMutationMethods = new Set<MutationFormMethod>(
531+
validMutationMethodsArr
532+
);
531533

532-
const validRequestMethodsArr: FormMethod[] = ["get", ...validActionMethodsArr];
534+
const validRequestMethodsArr: FormMethod[] = [
535+
"get",
536+
...validMutationMethodsArr,
537+
];
533538
const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
534539

535540
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
@@ -811,7 +816,8 @@ export function createRouter(init: RouterInit): Router {
811816
};
812817

813818
let historyAction =
814-
(opts && opts.replace) === true || submission != null
819+
(opts && opts.replace) === true ||
820+
(submission != null && isMutationMethod(submission.formMethod))
815821
? HistoryAction.Replace
816822
: HistoryAction.Push;
817823
let preventScrollReset =
@@ -935,7 +941,11 @@ export function createRouter(init: RouterInit): Router {
935941
pendingError = {
936942
[findNearestBoundary(matches).route.id]: opts.pendingError,
937943
};
938-
} else if (opts && opts.submission) {
944+
} else if (
945+
opts &&
946+
opts.submission &&
947+
isMutationMethod(opts.submission.formMethod)
948+
) {
939949
// Call action if we received an action submission
940950
let actionOutput = await handleAction(
941951
request,
@@ -1095,6 +1105,7 @@ export function createRouter(init: RouterInit): Router {
10951105
formAction: undefined,
10961106
formEncType: undefined,
10971107
formData: undefined,
1108+
...submission,
10981109
};
10991110
loadingNavigation = navigation;
11001111
}
@@ -1259,15 +1270,15 @@ export function createRouter(init: RouterInit): Router {
12591270
let { path, submission } = normalizeNavigateOptions(href, opts, true);
12601271
let match = getTargetMatch(matches, path);
12611272

1262-
if (submission) {
1273+
if (submission && isMutationMethod(submission.formMethod)) {
12631274
handleFetcherAction(key, routeId, path, match, matches, submission);
12641275
return;
12651276
}
12661277

12671278
// Store off the match so we can call it's shouldRevalidate on subsequent
12681279
// revalidations
12691280
fetchLoadMatches.set(key, [path, match, matches]);
1270-
handleFetcherLoader(key, routeId, path, match, matches);
1281+
handleFetcherLoader(key, routeId, path, match, matches, submission);
12711282
}
12721283

12731284
// Call the action for the matched fetcher.submit(), and then handle redirects,
@@ -1494,7 +1505,8 @@ export function createRouter(init: RouterInit): Router {
14941505
routeId: string,
14951506
path: string,
14961507
match: AgnosticDataRouteMatch,
1497-
matches: AgnosticDataRouteMatch[]
1508+
matches: AgnosticDataRouteMatch[],
1509+
submission?: Submission
14981510
) {
14991511
let existingFetcher = state.fetchers.get(key);
15001512
// Put this fetcher into it's loading state
@@ -1504,6 +1516,7 @@ export function createRouter(init: RouterInit): Router {
15041516
formAction: undefined,
15051517
formEncType: undefined,
15061518
formData: undefined,
1519+
...submission,
15071520
data: existingFetcher && existingFetcher.data,
15081521
};
15091522
state.fetchers.set(key, loadingFetcher);
@@ -1635,12 +1648,12 @@ export function createRouter(init: RouterInit): Router {
16351648
let { formMethod, formAction, formEncType, formData } = state.navigation;
16361649

16371650
// If this was a 307/308 submission we want to preserve the HTTP method and
1638-
// re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
1651+
// re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
16391652
// redirected location
16401653
if (
16411654
redirectPreserveMethodStatusCodes.has(redirect.status) &&
16421655
formMethod &&
1643-
isSubmissionMethod(formMethod) &&
1656+
isMutationMethod(formMethod) &&
16441657
formEncType &&
16451658
formData
16461659
) {
@@ -2096,7 +2109,7 @@ export function unstable_createStaticHandler(
20962109
);
20972110

20982111
try {
2099-
if (isSubmissionMethod(request.method.toLowerCase())) {
2112+
if (isMutationMethod(request.method.toLowerCase())) {
21002113
let result = await submit(
21012114
request,
21022115
matches,
@@ -2409,17 +2422,19 @@ function normalizeNavigateOptions(
24092422
}
24102423

24112424
// Create a Submission on non-GET navigations
2412-
if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2413-
return {
2414-
path,
2415-
submission: {
2416-
formMethod: opts.formMethod,
2417-
formAction: stripHashFromPath(path),
2418-
formEncType:
2419-
(opts && opts.formEncType) || "application/x-www-form-urlencoded",
2420-
formData: opts.formData,
2421-
},
2425+
let submission: Submission | undefined;
2426+
if (opts.formData) {
2427+
submission = {
2428+
formMethod: opts.formMethod || "get",
2429+
formAction: stripHashFromPath(path),
2430+
formEncType:
2431+
(opts && opts.formEncType) || "application/x-www-form-urlencoded",
2432+
formData: opts.formData,
24222433
};
2434+
2435+
if (isMutationMethod(submission.formMethod)) {
2436+
return { path, submission };
2437+
}
24232438
}
24242439

24252440
// Flatten submission onto URLSearchParams for GET submissions
@@ -2444,7 +2459,7 @@ function normalizeNavigateOptions(
24442459
};
24452460
}
24462461

2447-
return { path: createPath(parsedPath) };
2462+
return { path: createPath(parsedPath), submission };
24482463
}
24492464

24502465
// Filter out all routes below any caught error as they aren't going to
@@ -2767,7 +2782,7 @@ function createClientSideRequest(
27672782
let url = createClientSideURL(stripHashFromPath(location)).toString();
27682783
let init: RequestInit = { signal };
27692784

2770-
if (submission) {
2785+
if (submission && isMutationMethod(submission.formMethod)) {
27712786
let { formMethod, formEncType, formData } = submission;
27722787
init.method = formMethod.toUpperCase();
27732788
init.body =
@@ -3115,8 +3130,8 @@ function isValidMethod(method: string): method is FormMethod {
31153130
return validRequestMethods.has(method as FormMethod);
31163131
}
31173132

3118-
function isSubmissionMethod(method: string): method is SubmissionFormMethod {
3119-
return validActionMethods.has(method as SubmissionFormMethod);
3133+
function isMutationMethod(method?: string): method is MutationFormMethod {
3134+
return validMutationMethods.has(method as MutationFormMethod);
31203135
}
31213136

31223137
async function resolveDeferredResults(

packages/router/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ export type DataResult =
6161
| RedirectResult
6262
| ErrorResult;
6363

64-
export type SubmissionFormMethod = "post" | "put" | "patch" | "delete";
65-
export type FormMethod = "get" | SubmissionFormMethod;
64+
export type MutationFormMethod = "post" | "put" | "patch" | "delete";
65+
export type FormMethod = "get" | MutationFormMethod;
6666

6767
export type FormEncType =
6868
| "application/x-www-form-urlencoded"
@@ -74,7 +74,7 @@ export type FormEncType =
7474
* external consumption
7575
*/
7676
export interface Submission {
77-
formMethod: SubmissionFormMethod;
77+
formMethod: FormMethod;
7878
formAction: string;
7979
formEncType: FormEncType;
8080
formData: FormData;

0 commit comments

Comments
 (0)