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

Add support for json/text submissions #6570

Merged
merged 5 commits into from
Jun 21, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/json-text-encoding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": minor
---

Support `application/json` and `text/plain` submission encodings in `useSubmit`/`fetcher.submit`
86 changes: 75 additions & 11 deletions integration/fetcher-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,22 @@ test.describe("useFetcher", () => {
`,

"app/routes/fetcher-echo.jsx": js`
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";

export async function action({ request }) {
await new Promise(r => setTimeout(r, 1000));
let value = (await request.formData()).get('value');
return json({ data: "ACTION " + value })
let contentType = request.headers.get('Content-Type');
let value;
if (contentType.includes('application/json')) {
let json = await request.json();
value = json === null ? json : json.value;
} else if (contentType.includes('text/plain')) {
value = await request.text();
} else {
value = (await request.formData()).get('value');
}
return json({ data: "ACTION (" + contentType + ") " + value })
}

export async function loader({ request }) {
Expand Down Expand Up @@ -190,6 +199,20 @@ test.describe("useFetcher", () => {
let value = document.getElementById('fetcher-input').value;
fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo' })
}}>Submit</button>
<button id="fetcher-submit-json" onClick={() => {
let value = document.getElementById('fetcher-input').value;
fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })
}}>Submit JSON</button>
<button id="fetcher-submit-json-null" onClick={() => {
fetcher.submit(null, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })
}}>Submit Null JSON</button>
<button id="fetcher-submit-text" onClick={() => {
let value = document.getElementById('fetcher-input').value;
fetcher.submit(value, { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })
}}>Submit Text</button>
<button id="fetcher-submit-text-empty" onClick={() => {
fetcher.submit("", { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })
}}>Submit Empty Text</button>

{fetcher.state === 'idle' ? <p id="fetcher-idle">IDLE</p> : null}
<pre>{JSON.stringify(fetcherValues)}</pre>
Expand Down Expand Up @@ -253,6 +276,46 @@ test.describe("useFetcher", () => {
await page.waitForSelector(`pre:has-text("${CHEESESTEAK}")`);
});

test("submit can hit an action with json", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-echo", true);
await page.fill("#fetcher-input", "input value");
await app.clickElement("#fetcher-submit-json");
await page.waitForSelector(`#fetcher-idle`);
expect(await app.getHtml()).toMatch(
'ACTION (application/json) input value"'
);
});

test("submit can hit an action with null json", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-echo", true);
await app.clickElement("#fetcher-submit-json-null");
await new Promise((r) => setTimeout(r, 1000));
await page.waitForSelector(`#fetcher-idle`);
expect(await app.getHtml()).toMatch('ACTION (application/json) null"');
});

test("submit can hit an action with text", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-echo", true);
await page.fill("#fetcher-input", "input value");
await app.clickElement("#fetcher-submit-text");
await page.waitForSelector(`#fetcher-idle`);
expect(await app.getHtml()).toMatch(
'ACTION (text/plain;charset=UTF-8) input value"'
);
});

test("submit can hit an action with empty text", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-echo", true);
await app.clickElement("#fetcher-submit-text-empty");
await new Promise((r) => setTimeout(r, 1000));
await page.waitForSelector(`#fetcher-idle`);
expect(await app.getHtml()).toMatch('ACTION (text/plain;charset=UTF-8) "');
});

test("submit can hit an action only route", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-action-only-call");
Expand Down Expand Up @@ -333,8 +396,8 @@ test.describe("useFetcher", () => {
JSON.stringify([
"idle/undefined",
"submitting/undefined",
"loading/ACTION 1",
"idle/ACTION 1",
"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
])
);

Expand All @@ -345,11 +408,12 @@ test.describe("useFetcher", () => {
JSON.stringify([
"idle/undefined",
"submitting/undefined",
"loading/ACTION 1",
"idle/ACTION 1",
"submitting/ACTION 1", // Preserves old data during resubmissions
"loading/ACTION 2",
"idle/ACTION 2",
"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
// Preserves old data during resubmissions
"submitting/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2",
"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2",
])
);
});
Expand Down
5 changes: 0 additions & 5 deletions integration/form-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,7 @@ test.describe("Forms", () => {

test("submits the submitter's value(s) in tree order in the form data", async ({
page,
javaScriptEnabled,
}) => {
test.fail(
Boolean(javaScriptEnabled),
"<Form> doesn't serialize submit buttons correctly #4342"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does now!

);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/submitter");
Expand Down
114 changes: 91 additions & 23 deletions integration/hook-useSubmit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,78 @@ test.describe("`useSubmit()` returned function", () => {
},
files: {
"app/routes/_index.jsx": js`
import { useLoaderData, useSubmit } from "@remix-run/react";
import { useLoaderData, useSubmit } from "@remix-run/react";

export function loader({ request }) {
let url = new URL(request.url);
return url.searchParams.toString()
}
export function loader({ request }) {
let url = new URL(request.url);
return url.searchParams.toString()
}

export default function Index() {
let submit = useSubmit();
let handleClick = event => {
event.preventDefault()
submit(event.nativeEvent.submitter || event.currentTarget)
}
let data = useLoaderData();
return (
<form>
<input type="text" name="tasks" defaultValue="first" />
<input type="text" name="tasks" defaultValue="second" />

export default function Index() {
let submit = useSubmit();
let handleClick = event => {
event.preventDefault()
submit(event.nativeEvent.submitter || event.currentTarget)
<button onClick={handleClick} name="tasks" value="third">
Prepare Third Task
</button>

<pre>{data}</pre>
</form>
)
}
let data = useLoaderData();
return (
<form>
<input type="text" name="tasks" defaultValue="first" />
<input type="text" name="tasks" defaultValue="second" />
`,
"app/routes/action.jsx": js`
import { json } from "@remix-run/node";
import { useActionData, useSubmit } from "@remix-run/react";

<button onClick={handleClick} name="tasks" value="third">
Prepare Third Task
</button>
export async function action({ request }) {
let contentType = request.headers.get('Content-Type');
if (contentType.includes('application/json')) {
return json({ value: await request.json() });
}
if (contentType.includes('text/plain')) {
return json({ value: await request.text() });
}
let fd = await request.formData();
return json({ value: new URLSearchParams(fd.entries()).toString() })
}

<pre>{data}</pre>
</form>
)
}
`,
export default function Component() {
let submit = useSubmit();
let data = useActionData();
return (
<>
<button id="submit-json" onClick={() => submit(
{ key: 'value' },
{ method: 'post', encType: 'application/json' },
)}>
Submit JSON
</button>
<button id="submit-text" onClick={() => submit(
"raw text",
{ method: 'post', encType: 'text/plain' },
)}>
Submit Text
</button>
<button id="submit-formData" onClick={() => submit(
{ key: 'value' },
{ method: 'post' },
)}>
Submit FrmData
</button>
{data ? <p id="action-data">data: {JSON.stringify(data)}</p> : null}
</>
);
}
`,
},
});

Expand All @@ -64,4 +108,28 @@ test.describe("`useSubmit()` returned function", () => {
`<pre>tasks=first&amp;tasks=second&amp;tasks=third</pre>`
);
});

test("submits json data", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/action", true);
await app.clickElement("#submit-json");
await page.waitForSelector("#action-data");
expect(await app.getHtml()).toMatch('data: {"value":{"key":"value"}}');
});

test("submits text data", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/action", true);
await app.clickElement("#submit-text");
await page.waitForSelector("#action-data");
expect(await app.getHtml()).toMatch('data: {"value":"raw text"}');
});

test("submits form data", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/action", true);
await app.clickElement("#submit-formData");
await page.waitForSelector("#action-data");
expect(await app.getHtml()).toMatch('data: {"value":"key=value"}');
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"@octokit/graphql": "^4.8.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/rest": "^18.12.0",
"@playwright/test": "^1.28.1",
"@playwright/test": "^1.35.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to latest playwright so headless chromium works properly for remix-run/react-router#9865. The 1.28 version was giving a false positive on the FormData check - so it was throwing the exception (indicating it supported the submitter) but then new FormData(form, submitter) wouldn't include the value. It works fine in our browser testing though so we're OK chalking it up to a headless issue for now and can look for more robust solutions if folks run into issues.

"@remix-run/changelog-github": "^0.0.5",
"@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-json": "^4.1.0",
Expand Down
Loading