diff --git a/src/htmx.js b/src/htmx.js index aa9e0a0d8..8092b9c27 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1787,14 +1787,17 @@ var htmx = (() => { __collectFormData(elt, form, submitter, validate, isGet) { if (validate && form && !form.reportValidity()) return - + + // treat a named submit button that owns hx-post as its own submitter (#4073) + submitter ??= (form && elt.type === 'submit') ? elt : null; + let formData = form ? new FormData(form) : new FormData() let included = form ? new Set(form.elements) : new Set() if (!form) { if (validate && elt.reportValidity && !elt.reportValidity()) return this.__addInputValues(elt, included, formData, isGet); } - if (submitter && submitter.name) { + if (submitter?.name) { formData.append(submitter.name, submitter.value) included.add(submitter); } diff --git a/test/tests/unit/__collectFormData.js b/test/tests/unit/__collectFormData.js index 6da048d1f..47581aaef 100644 --- a/test/tests/unit/__collectFormData.js +++ b/test/tests/unit/__collectFormData.js @@ -296,4 +296,61 @@ describe('__collectFormData unit tests', function() { let formData = htmx.__collectFormData(elt, null, null, false, false); assert.deepEqual(formData.getAll('fruit'), ['banana', 'apple']); }); + + // ── submit button name/value regression ──────────────────────────────── + + it('button[hx-post] inside form sends its own name/value', function () { + let form = createProcessedHTML('
') + let btn = form.querySelector('button') + let formData = htmx.__collectFormData(btn, form, null) + assert.equal(formData.get('txt'), 'hello') + assert.equal(formData.get('btn'), 'B') + }) + + it('form[hx-post] sends clicked submit button name/value via evt.submitter', function () { + let form = createProcessedHTML('
') + let btn = form.querySelector('button') + let formData = htmx.__collectFormData(form, form, btn) + assert.equal(formData.get('txt'), 'hello') + assert.equal(formData.get('btn'), 'B') + }) + + it('standalone button[hx-post] with no form sends its own name/value', function () { + let btn = createProcessedHTML('') + let formData = htmx.__collectFormData(btn, null, null) + assert.equal(formData.get('btn'), 'C') + }) + + it('only the clicked button value is sent when multiple submit buttons exist', function () { + let form = createProcessedHTML('
') + let save = form.querySelectorAll('button')[0] + let del = form.querySelectorAll('button')[1] + let fd1 = htmx.__collectFormData(save, form, null) + assert.equal(fd1.get('action'), 'save') + let fd2 = htmx.__collectFormData(del, form, null) + assert.equal(fd2.get('action'), 'delete') + }) + + it('input[type=submit][hx-post] inside form sends its own name/value', function () { + let form = createProcessedHTML('
') + let submit = form.querySelector('input[type=submit]') + let formData = htmx.__collectFormData(submit, form, null) + assert.equal(formData.get('txt'), 'hello') + assert.equal(formData.get('btn'), 'go') + }) + + it('button with no type (defaults to submit) inside form sends its own name/value', function () { + let form = createProcessedHTML('
') + let btn = form.querySelector('button') + let formData = htmx.__collectFormData(btn, form, null) + assert.equal(formData.get('txt'), 'hello') + assert.equal(formData.get('btn'), 'implicit') + }) + + it('submit button value is not duplicated', function () { + let form = createProcessedHTML('
') + let btn = form.querySelector('button') + let formData = htmx.__collectFormData(btn, form, null) + assert.deepEqual(formData.getAll('btn'), ['B']) + }) });