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 1 commit
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`
58 changes: 47 additions & 11 deletions integration/fetcher-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,18 @@ 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 = contentType.includes('application/json') ?
(await request.json()).value :
contentType.includes('text/plain') ?
(await request.text()) :
(await request.formData()).get('value');
return json({ data: "ACTION (" + contentType + ") " + value })
}

export async function loader({ request }) {
Expand Down Expand Up @@ -190,6 +195,14 @@ 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-text" onClick={() => {
let value = document.getElementById('fetcher-input').value;
fetcher.submit(value, { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })
}}>Submit JSON</button>

{fetcher.state === 'idle' ? <p id="fetcher-idle">IDLE</p> : null}
<pre>{JSON.stringify(fetcherValues)}</pre>
Expand Down Expand Up @@ -253,6 +266,28 @@ 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 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 only route", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fetcher-action-only-call");
Expand Down Expand Up @@ -333,8 +368,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 +380,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
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"}');
});
});
Loading