Skip to content

Commit

Permalink
Respect forms with enctype set for view transitions (#9466)
Browse files Browse the repository at this point in the history
* Respect forms with enctype set for view transitions

* Add changeset

* Revert "Respect forms with enctype set for view transitions"

This reverts commit 6d3e04a.

* Review feedback

* Handle submitter case

* Move comment

* Update .changeset/rude-geckos-rush.md

* Add tests

---------

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
  • Loading branch information
knpwrs and natemoo-re committed Dec 20, 2023
1 parent 1f3d72b commit 5062d27
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-geckos-rush.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

Updates view transitions `form` handling with logic for the [`enctype`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype) attribute
@@ -1,13 +1,15 @@
---
import Layout from '../components/Layout.astro';
const method = Astro.url.searchParams.get('method') ?? 'POST';
const enctype = Astro.url.searchParams.get('enctype');
const postShowThrow = Astro.url.searchParams.has('throw') ?? false;
---

<Layout>
<h2>Contact Form</h2>
<form action="/contact" method={method}>
<input type="hidden" name="name" value="Testing">
{postShowThrow ? <input type="hidden" name="throw" value="true"> : ''}
<input type="submit" value="Submit" id="submit">
<form action="/contact" method={method} {...enctype ? { enctype } : {}}>
<input type="hidden" name="name" value="Testing" />
{postShowThrow ? <input type="hidden" name="throw" value="true" /> : ''}
<input type="submit" value="Submit" id="submit" />
</form>
</Layout>
77 changes: 77 additions & 0 deletions packages/astro/e2e/view-transitions.test.js
Expand Up @@ -976,6 +976,83 @@ test.describe('View Transitions', () => {
).toEqual(1);
});

test('form POST defaults to multipart/form-data (Astro 4.x compatibility)', async ({
page,
astro,
}) => {
const loads = [];

page.addListener('load', async (p) => {
loads.push(p);
});

const postedEncodings = [];

await page.route('**/contact', async (route) => {
const request = route.request();

if (request.method() === 'POST') {
postedEncodings.push(request.headers()['content-type'].split(';')[0]);
}

await route.continue();
});

await page.goto(astro.resolveUrl('/form-one'));

// Submit the form
await page.click('#submit');

expect(
loads.length,
'There should be only 1 page load. No additional loads for the form submission'
).toEqual(1);

expect(
postedEncodings,
'There should be 1 POST, with encoding set to `multipart/form-data`'
).toEqual(['multipart/form-data']);
});

test('form POST respects enctype attribute', async ({ page, astro }) => {
const loads = [];

page.addListener('load', async (p) => {
loads.push(p);
});

const postedEncodings = [];

await page.route('**/contact', async (route) => {
const request = route.request();

if (request.method() === 'POST') {
postedEncodings.push(request.headers()['content-type'].split(';')[0]);
}

await route.continue();
});

await page.goto(
astro.resolveUrl(
`/form-one?${new URLSearchParams({ enctype: 'application/x-www-form-urlencoded' })}`
)
);

// Submit the form
await page.click('#submit');

expect(
loads.length,
'There should be only 1 page load. No additional loads for the form submission'
).toEqual(1);

expect(
postedEncodings,
'There should be 1 POST, with encoding set to `multipart/form-data`'
).toEqual(['application/x-www-form-urlencoded']);
});

test('Route announcer is invisible on page transition', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/no-directive-one'));

Expand Down
20 changes: 19 additions & 1 deletion packages/astro/src/transitions/router.ts
Expand Up @@ -463,7 +463,25 @@ async function transition(
const init: RequestInit = {};
if (preparationEvent.formData) {
init.method = 'POST';
init.body = preparationEvent.formData;
const form =
preparationEvent.sourceElement instanceof HTMLFormElement
? preparationEvent.sourceElement
: preparationEvent.sourceElement instanceof HTMLElement &&
'form' in preparationEvent.sourceElement
? (preparationEvent.sourceElement.form as HTMLFormElement)
: preparationEvent.sourceElement?.closest('form');
// Form elements without enctype explicitly set default to application/x-www-form-urlencoded.
// In order to maintain compatibility with Astro 4.x, we need to check the value of enctype
// on the attributes property rather than accessing .enctype directly. Astro 5.x may
// introduce defaulting to application/x-www-form-urlencoded as a breaking change, and then
// we can access .enctype directly.
//
// Note: getNamedItem can return null in real life, even if TypeScript doesn't think so, hence
// the ?.
init.body =
form?.attributes.getNamedItem('enctype')?.value === 'application/x-www-form-urlencoded'
? new URLSearchParams(preparationEvent.formData as any)
: preparationEvent.formData;
}
const response = await fetchHTML(href, init);
// If there is a problem fetching the new page, just do an MPA navigation to it.
Expand Down

0 comments on commit 5062d27

Please sign in to comment.