Skip to content

Commit

Permalink
Add support for <Form encType=text/plain>
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jun 2, 2023
1 parent 8a7df27 commit 253cca7
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 31 deletions.
29 changes: 29 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3307,6 +3307,35 @@ function testDomRouter(
expect(await request.text()).toEqual(body);
});

it('serializes into text on <Form encType="text/plain" submissions', async () => {
let actionSpy = jest.fn();
let router = createTestRouter(
createRoutesFromElements(
<Route path="/" action={actionSpy} element={<FormPage />} />
),
{ window: getWindow("/") }
);
render(<RouterProvider router={router} />);

function FormPage() {
return (
<Form method="post" encType="text/plain">
<input name="a" defaultValue="1" />
<input name="b" defaultValue="2" />
<button type="submit">Submit</button>
</Form>
);
}

fireEvent.click(screen.getByText("Submit"));
expect(await actionSpy.mock.calls[0][0].request.text())
.toMatchInlineSnapshot(`
"a=1
b=2
"
`);
});

it("includes submit button name/value on form submission", async () => {
let actionSpy = jest.fn();
let router = createTestRouter(
Expand Down
7 changes: 7 additions & 0 deletions packages/react-router-dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export interface SubmitOptions {
const supportedFormEncTypes: Set<FormEncType> = new Set([
"application/x-www-form-urlencoded",
"multipart/form-data",
"text/plain",
]);

function getFormEncType(encType: string | null) {
Expand Down Expand Up @@ -259,5 +260,11 @@ export function getFormSubmissionInfo(
body = target;
}

// Send body for <Form encType="text/plain" so we encode it into text
if (formData && encType === "text/plain") {
body = formData;
formData = undefined;
}

return { action, method: method.toLowerCase(), encType, formData, body };
}
8 changes: 4 additions & 4 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,10 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
* `<form encType>` - enhancing beyond the normal string type and limiting
* to the built-in browser supported values
*/
encType?: Extract<
FormEncType,
"application/x-www-form-urlencoded" | "multipart/form-data"
>;
encType?:
| "application/x-www-form-urlencoded"
| "multipart/form-data"
| "text/plain";

/**
* Normal `<form action>` but supports React Router's relative paths.
Expand Down
41 changes: 41 additions & 0 deletions packages/router/__tests__/router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6486,6 +6486,47 @@ describe("a router", () => {
expect(await request.text()).toEqual(body);
});

it("serializes body as text/plain (FormData)", async () => {
let t = setup({
routes: [{ id: "root", path: "/", action: true }],
});

let body = new FormData();
body.append("a", "1");
body.append("b", "2");
let nav = await t.navigate("/", {
formMethod: "post",
formEncType: "text/plain",
body,
});
expect(t.router.state.navigation.text).toMatchInlineSnapshot(`
"a=1
b=2
"
`);
expect(t.router.state.navigation.formData).toBeUndefined();
expect(t.router.state.navigation.json).toBeUndefined();

await nav.actions.root.resolve("ACTION");

expect(nav.actions.root.stub).toHaveBeenCalledWith({
params: {},
request: expect.any(Request),
});

let request = nav.actions.root.stub.mock.calls[0][0].request;
expect(request.method).toBe("POST");
expect(request.url).toBe("http://localhost/");
expect(request.headers.get("Content-Type")).toBe(
"text/plain;charset=UTF-8"
);
expect(await request.text()).toMatchInlineSnapshot(`
"a=1
b=2
"
`);
});

it("serializes body as FormData when encType=undefined", async () => {
let t = setup({
routes: [{ id: "root", path: "/", action: true }],
Expand Down
59 changes: 32 additions & 27 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3095,7 +3095,6 @@ function normalizeNavigateOptions(
}

// Create a Submission on non-GET navigations
let submission: Submission;
let rawFormMethod = opts.formMethod || "get";
let formMethod = normalizeFormMethod
? (rawFormMethod.toUpperCase() as V7_FormMethod)
Expand All @@ -3104,39 +3103,45 @@ function normalizeNavigateOptions(

if (opts.body) {
if (opts.formEncType === "text/plain") {
submission = {
formMethod,
formAction,
formEncType: opts.formEncType,
text:
typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body),
formData: undefined,
json: undefined,
};
let text =
typeof opts.body === "string"
? opts.body
: opts.body instanceof FormData ||
opts.body instanceof URLSearchParams
? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
Array.from(opts.body.entries()).reduce(
(acc, [name, value]) => `${acc}${name}=${value}\n`,
""
)
: String(opts.body);

return { path, submission };
return {
path,
submission: {
formMethod,
formAction,
formEncType: opts.formEncType,
text,
formData: undefined,
json: undefined,
},
};
} else if (opts.formEncType === "application/json") {
try {
if (typeof opts.body === "string") {
submission = {
formMethod,
formAction,
formEncType: opts.formEncType,
text: undefined,
formData: undefined,
json: JSON.parse(opts.body),
};
} else {
submission = {
let json =
typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;

return {
path,
submission: {
formMethod,
formAction,
formEncType: opts.formEncType,
text: undefined,
formData: undefined,
json: opts.body,
};
}
return { path, submission };
json,
},
};
} catch (e) {
return {
path,
Expand Down Expand Up @@ -3178,7 +3183,7 @@ function normalizeNavigateOptions(
}
}

submission = {
let submission: Submission = {
formMethod,
formAction,
formEncType:
Expand Down

0 comments on commit 253cca7

Please sign in to comment.