From 01ecfb79ac8579044181a0c08a613f9f56c457b0 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Mon, 29 Jun 2026 00:25:29 +1200 Subject: [PATCH 1/2] handle custom element manual input value collection --- src/htmx.js | 15 ++++++++------- test/tests/unit/__collectFormData.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 35c86ec39..e05f5bcfd 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1813,35 +1813,36 @@ var htmx = (() => { __addInputValues(elt, included, formData, isGet) { let tag = elt.tagName; let inputs = []; - if (tag === 'BUTTON') { - inputs = [elt]; // buttons only send own value, never collect children + if (tag === 'BUTTON' || tag.includes('-')) { + inputs = [elt]; // send own value only, never collect children } else if (['INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET'].includes(tag) || !isGet) { inputs = this.__queryEltAndDescendants(elt, 'input, select, textarea'); } // GET on non-form-control containers (div, etc.) sends nothing — use hx-include for explicit inclusion for (let input of inputs) { - if (!input.name || input.matches(':disabled') || included.has(input)) continue; + let name = input.name || input.getAttribute?.('name'); + if (!name || input.matches(':disabled') || included.has(input)) continue; included.add(input); let type = input.type; if (type === 'checkbox' || type === 'radio') { // Only add if checked if (input.checked) { - formData.append(input.name, input.value); + formData.append(name, input.value); } } else if (type === 'file') { // Add all selected files for (let file of input.files) { - formData.append(input.name, file); + formData.append(name, file); } } else if (type === 'select-multiple') { // Add all selected options for (let option of input.selectedOptions) { - formData.append(input.name, option.value); + formData.append(name, option.value); } } else { - formData.append(input.name, input.value); + formData.append(name, input.value); } } } diff --git a/test/tests/unit/__collectFormData.js b/test/tests/unit/__collectFormData.js index e512bc3a6..c852ceeec 100644 --- a/test/tests/unit/__collectFormData.js +++ b/test/tests/unit/__collectFormData.js @@ -233,4 +233,16 @@ describe('__collectFormData unit tests', function() { let formData = htmx.__collectFormData(form, form, null); assert.deepEqual(formData.getAll('items'), ['a', 'b']); }); + + it('collects form-associated custom element value as standalone trigger (no form)', function () { + let elt = createProcessedHTML(''); + let formData = htmx.__collectFormData(elt, null, null); + assert.equal(formData.get('foo'), 'hello'); + }); + + it('collects form-associated custom element value as standalone trigger with POST', function () { + let elt = createProcessedHTML(''); + let formData = htmx.__collectFormData(elt, null, null, false, false); + assert.equal(formData.get('tenantId'), 'acme'); + }); }); \ No newline at end of file From 2f541b5e00dc9b00cb583f27d6abaafe53c6ef23 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Thu, 2 Jul 2026 11:45:36 +1200 Subject: [PATCH 2/2] Handle hx-include of custom elements as children but not if unchecked checkboxes --- src/htmx.js | 4 +-- test/tests/attributes/hx-include.js | 48 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index e05f5bcfd..cb7ebd3aa 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1816,7 +1816,7 @@ var htmx = (() => { if (tag === 'BUTTON' || tag.includes('-')) { inputs = [elt]; // send own value only, never collect children } else if (['INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET'].includes(tag) || !isGet) { - inputs = this.__queryEltAndDescendants(elt, 'input, select, textarea'); + inputs = this.__queryEltAndDescendants(elt, '[name]:not(button)'); } // GET on non-form-control containers (div, etc.) sends nothing — use hx-include for explicit inclusion @@ -1826,7 +1826,7 @@ var htmx = (() => { included.add(input); let type = input.type; - if (type === 'checkbox' || type === 'radio') { + if (type === 'checkbox' || type === 'radio' || (input.tagName !== 'INPUT' && 'checked' in input)) { // Only add if checked if (input.checked) { formData.append(name, input.value); diff --git a/test/tests/attributes/hx-include.js b/test/tests/attributes/hx-include.js index 6dedf98aa..158d4b6b2 100644 --- a/test/tests/attributes/hx-include.js +++ b/test/tests/attributes/hx-include.js @@ -32,6 +32,54 @@ describe('hx-include attribute', function() { assert.equal(new URL(fetchMock.calls[0].url, location.href).searchParams.get('notify'), null) }) + it('button children inside hx-include container are not sent', async function () { + mockResponse('GET', '/include', "OK") + createProcessedHTML('
') + find('#trigger').click() + await forRequest() + let params = new URL(fetchMock.calls[0].url, location.href).searchParams; + params.get('i1').should.equal('yes'); + assert.equal(params.get('btn'), null); + }) + + it('custom element with checked=true via hx-include sends value', async function () { + mockResponse('GET', '/include', "OK") + createProcessedHTML('
') + let el = document.createElement('my-toggle'); + el.setAttribute('name', 'toggle'); + Object.defineProperty(el, 'checked', { value: true, configurable: true }); + Object.defineProperty(el, 'value', { value: 'on', configurable: true }); + find('#container').appendChild(el); + find('#btn').click() + await forRequest() + new URL(fetchMock.calls[0].url, location.href).searchParams.get('toggle').should.equal('on'); + }) + + it('custom element with checked=false via hx-include is not sent', async function () { + mockResponse('GET', '/include', "OK") + createProcessedHTML('
') + let el = document.createElement('my-toggle'); + el.setAttribute('name', 'toggle'); + Object.defineProperty(el, 'checked', { value: false, configurable: true }); + Object.defineProperty(el, 'value', { value: 'on', configurable: true }); + find('#container').appendChild(el); + find('#btn').click() + await forRequest() + assert.equal(new URL(fetchMock.calls[0].url, location.href).searchParams.get('toggle'), null); + }) + + it('custom element with name and value inside hx-include container is sent', async function () { + mockResponse('GET', '/include', "OK") + createProcessedHTML('
') + let el = document.createElement('my-input'); + el.setAttribute('name', 'custom'); + Object.defineProperty(el, 'value', { value: 'hello', configurable: true }); + find('#container').appendChild(el); + find('#btn').click() + await forRequest() + new URL(fetchMock.calls[0].url, location.href).searchParams.get('custom').should.equal('hello'); + }) + it('non-GET includes closest form', async function () { mockResponse('POST', '/include', "Dummy")