From 633be5d2e0d450e6b09802b4218012a4266cd68d Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 1 Jul 2026 22:53:25 +0200 Subject: [PATCH 1/5] Extend tests for external inputs --- tests/unit/forms.test.ts | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unit/forms.test.ts b/tests/unit/forms.test.ts index 05a5f0f..2a129de 100644 --- a/tests/unit/forms.test.ts +++ b/tests/unit/forms.test.ts @@ -15,6 +15,8 @@ const createButton = (html: string) => { return new window.DOMParser().parseFromString(html, 'text/html').querySelector('button')!; }; +const parseDoc = (html: string) => new window.DOMParser().parseFromString(html, 'text/html'); + describe('appendQueryParams', () => { const empty = new FormData(); const data = new FormData(); @@ -225,3 +227,60 @@ describe('getFormInfo', () => { }); }); }); + +describe('getFormInfo with the form attribute', () => { + it('includes inputs associated via the form attribute in GET params', () => { + const doc = parseDoc( + '
' + + '' + ); + const form = doc.querySelector('form')!; + expect(getFormInfo(form)).toMatchObject({ + href: 'http://localhost:3000/path?a=b#anchor', + url: '/path?a=b', + hash: '#anchor', + method: 'GET' + }); + }); + + it('includes inputs associated via the form attribute in POST body', () => { + const doc = parseDoc( + '
' + + '' + ); + const form = doc.querySelector('form')!; + expect(getFormInfo(form)).toMatchObject({ + url: '/path', + method: 'POST', + body: new URLSearchParams({ a: 'b' }) + }); + }); + + it('includes the value of an externally associated submitter', () => { + const doc = parseDoc( + '
' + + '' + ); + const form = doc.querySelector('form')!; + const submitter = doc.querySelector('button'); + expect(getFormInfo(form, submitter)).toMatchObject({ + url: '/path', + method: 'POST', + body: new URLSearchParams({ btn: 'go' }) + }); + }); + + it('reads formaction/formmethod from an externally associated submitter', () => { + const doc = parseDoc( + '
' + + '' + ); + const form = doc.querySelector('form')!; + const submitter = doc.querySelector('button'); + expect(getFormInfo(form, submitter)).toMatchObject({ + action: 'http://localhost:3000/other', + url: '/other', + method: 'POST' + }); + }); +}); From 2fc6fec8bf1554a3a221a923162c6676aec8478e Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 1 Jul 2026 22:54:32 +0200 Subject: [PATCH 2/5] Add tests for navigating with external inputs --- tests/unit/plugin.test.ts | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/unit/plugin.test.ts b/tests/unit/plugin.test.ts index 0706080..bfc71ea 100644 --- a/tests/unit/plugin.test.ts +++ b/tests/unit/plugin.test.ts @@ -11,6 +11,14 @@ const createForm = (html: string) => { return form; }; +const appendToBody = (html: string) => { + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + const el = wrapper.firstElementChild as T; + document.body.appendChild(el); + return el; +}; + const submitForm = (form: HTMLFormElement, submitter?: HTMLButtonElement | null, key?: string) => { if (key) { document.dispatchEvent(new KeyboardEvent('keydown', { key })); @@ -45,6 +53,7 @@ describe('SwupFormsPlugin', () => { afterEach(() => { swup.unuse(plugin); swup.destroy(); + document.body.innerHTML = ''; }); it('does not call beforeFormSubmit for ignored forms', async () => { @@ -79,6 +88,52 @@ describe('SwupFormsPlugin', () => { expect(spy).toHaveBeenCalledWith(expect.objectContaining({ submitter })); }); + it('calls beforeFormSubmit for an externally associated submit button', async () => { + const spy = vitest.spyOn(plugin, 'beforeFormSubmit').mockImplementation(() => {}); + swup.use(plugin); + + const form = createForm('
'); + const submitter = appendToBody(''); + submitForm(form, submitter); + + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ delegateTarget: form, submitter })); + }); + + it('navigates to the formaction of an externally associated submitter', async () => { + swup.use(plugin); + + const navigateSpy = vitest.spyOn(swup, 'navigate').mockImplementation(() => {}); + + const form = createForm('
'); + const submitter = appendToBody( + '' + ); + submitForm(form, submitter); + + expect(navigateSpy).toHaveBeenCalledWith( + 'http://localhost:3000/other', + expect.objectContaining({ method: 'GET', cache: { read: false, write: true } }), + expect.objectContaining({ el: form }) + ); + }); + + it('includes externally associated inputs when navigating', async () => { + swup.use(plugin); + + const navigateSpy = vitest.spyOn(swup, 'navigate').mockImplementation(() => {}); + + const form = createForm('
'); + appendToBody(''); + const submitter = appendToBody(''); + submitForm(form, submitter); + + expect(navigateSpy).toHaveBeenCalledWith( + 'http://localhost:3000/path?a=b', + expect.objectContaining({ method: 'GET' }), + expect.objectContaining({ el: form }) + ); + }); + it('calls the form:submit hook', async () => { swup.use(plugin); From 458f40efba2d2298e02698311d5b5a542ef366e1 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 1 Jul 2026 23:00:55 +0200 Subject: [PATCH 3/5] Add readme section on supported form attributes --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a73ab18..0fe21c4 100755 --- a/README.md +++ b/README.md @@ -40,9 +40,29 @@ const swup = new Swup({ ## Server response +The server response must be a valid page with all containers to be replaced by swup. + +## Form attributes + Action, method and encoding type attributes set on the form are respected. -The server response must be a valid page with all containers to be replaced by swup. +[Externally associated inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form) are +supported. Inputs and submit buttons that live outside the form but reference it via `form="form-id"` are +included in the submission. + +Submitter overrides are supported. If the submitter is a button, its `formaction`, `formmethod`, `formenctype` +and `formtarget` attributes override the form's corresponding attributes, matching native browser behavior. + +```html + + + + + + + + +``` ## Custom animations From 1e9bdd19a834cac359e1f4453f9c5997d1363e29 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 1 Jul 2026 23:01:10 +0200 Subject: [PATCH 4/5] Add note about inline form container replacement --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0fe21c4..7976440 100755 --- a/README.md +++ b/README.md @@ -104,6 +104,10 @@ If you give a form an additional attribute `[data-swup-inline-form]`, swup will: } ``` +> **Note** Inline forms only replace the form's own container. Controls associated with an +> inline form via the `form` attribute but placed outside of it are **not** +> re-rendered on submit. + ## Options ### `formSelector` From 4355e24d1fac1d833cd149c8092563f775e60254 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 1 Jul 2026 23:05:17 +0200 Subject: [PATCH 5/5] Autoformat --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7976440..bcd01c0 100755 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ and `formtarget` attributes override the form's corresponding attributes, matchi - +