Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 10 additions & 9 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
inputs = this.__queryEltAndDescendants(elt, '[name]:not(button)');
}
// 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') {
if (type === 'checkbox' || type === 'radio' || (input.tagName !== 'INPUT' && 'checked' in input)) {
// 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);
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions test/tests/attributes/hx-include.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div id="container"><input name="i1" value="yes"><button name="btn" value="no">Click</button></div><button id="trigger" hx-get="/include" hx-include="#container">Go</button>')
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('<div id="container"></div><button id="btn" hx-get="/include" hx-include="#container">Go</button>')
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('<div id="container"></div><button id="btn" hx-get="/include" hx-include="#container">Go</button>')
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('<div id="container"></div><button id="btn" hx-get="/include" hx-include="#container">Go</button>')
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")
Expand Down
12 changes: 12 additions & 0 deletions test/tests/unit/__collectFormData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<test-input name="foo" value="hello"></test-input>');
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('<test-input name="tenantId" value="acme"></test-input>');
let formData = htmx.__collectFormData(elt, null, null, false, false);
assert.equal(formData.get('tenantId'), 'acme');
});
});
Loading