Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bugfix] Fix submit buttons/inputs handling #1559

Merged
merged 6 commits into from
Jul 25, 2023
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
85 changes: 48 additions & 37 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -1293,7 +1293,7 @@ return (function () {
return triggerSpecs;
} else if (matches(elt, 'form')) {
return [{trigger: 'submit'}];
} else if (matches(elt, 'input[type="button"]')){
} else if (matches(elt, 'input[type="button"], input[type="submit"]')){
return [{trigger: 'click'}];
} else if (matches(elt, INPUT_SELECTOR)) {
return [{trigger: 'change'}];
Expand Down Expand Up @@ -1849,17 +1849,24 @@ return (function () {

function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : "";
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-sse], [data-hx-sse], [hx-ws]," +
var boostedElts = hasChanceOfBeingBoosted() ? ", a" : "";
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
return results;
} else {
return [];
}
}

function initButtonTracking(form){
var maybeSetLastButtonClicked = function(evt){
function initButtonTracking(elt) {
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
if (!form) {
return
}

var maybeSetLastButtonClicked = function (evt) {
var elt = closest(evt.target, "button, input[type='submit']");
if (elt !== null) {
var internalData = getInternalData(form);
Expand All @@ -1871,9 +1878,9 @@ return (function () {
// focusin - in case someone tabs in to a button and hits the space bar
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724

form.addEventListener('click', maybeSetLastButtonClicked)
form.addEventListener('focusin', maybeSetLastButtonClicked)
form.addEventListener('focusout', function(evt){
elt.addEventListener('click', maybeSetLastButtonClicked)
elt.addEventListener('focusin', maybeSetLastButtonClicked)
elt.addEventListener('focusout', function(evt){
var internalData = getInternalData(form);
internalData.lastButtonClicked = null;
})
Expand Down Expand Up @@ -1981,8 +1988,10 @@ return (function () {
}
}

if (elt.tagName === "FORM") {
initButtonTracking(elt);
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
if (elt.tagName === "FORM" || (getRawAttribute(elt, "type") === "submit" && hasAttribute(elt, "form"))) {
initButtonTracking(elt)
}

var sseInfo = getAttributeValue(elt, 'hx-sse');
Expand Down Expand Up @@ -2307,6 +2316,29 @@ return (function () {
return true;
}

function addValueToValues(name, value, values) {
// This is a little ugly because both the current value of the named value in the form
// and the new value could be arrays, so we have to handle all four cases :/
if (name != null && value != null) {
var current = values[name];
if (current === undefined) {
values[name] = value;
} else if (Array.isArray(current)) {
if (Array.isArray(value)) {
values[name] = current.concat(value);
} else {
current.push(value);
}
} else {
if (Array.isArray(value)) {
values[name] = [current].concat(value);
} else {
values[name] = [current, value];
}
}
}
}

function processInputValue(processed, values, errors, elt, validate) {
if (elt == null || haveSeenNode(processed, elt)) {
return;
Expand All @@ -2323,28 +2355,7 @@ return (function () {
if (elt.files) {
value = toArray(elt.files);
}
// This is a little ugly because both the current value of the named value in the form
// and the new value could be arrays, so we have to handle all four cases :/
if (name != null && value != null) {
var current = values[name];
if (current !== undefined) {
if (Array.isArray(current)) {
if (Array.isArray(value)) {
values[name] = current.concat(value);
} else {
current.push(value);
}
} else {
if (Array.isArray(value)) {
values[name] = [current].concat(value);
} else {
values[name] = [current, value];
}
}
} else {
values[name] = value;
}
}
addValueToValues(name, value, values);
if (validate) {
validateElement(elt, errors);
}
Expand Down Expand Up @@ -2394,11 +2405,11 @@ return (function () {
processInputValue(processed, values, errors, elt, validate);

// if a button or submit was clicked last, include its value
if (internalData.lastButtonClicked) {
var name = getRawAttribute(internalData.lastButtonClicked,"name");
if (name) {
values[name] = internalData.lastButtonClicked.value;
}
if (internalData.lastButtonClicked || elt.tagName === "BUTTON" ||
(elt.tagName === "INPUT" && getRawAttribute(elt, "type") === "submit")) {
var button = internalData.lastButtonClicked || elt
var name = getRawAttribute(button, "name")
addValueToValues(name, button.value, formValues)
}

// include any explicit includes
Expand Down
195 changes: 195 additions & 0 deletions test/core/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -989,4 +989,199 @@ describe("Core htmx AJAX Tests", function(){
btn.innerHTML.should.equal('<with:colon id="foobar">Foobar</with:colon>');
});

it('properly handles clicked submit button with a value inside a htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<button id="submit" type="submit" name="b1" value="buttonValue">button</button>' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit input with a value inside a htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<input id="submit" type="submit" name="b1" value="buttonValue">' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit button with a value inside a non-htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form>' +
'<input type="text" name="t1" value="textValue">' +
'<button id="submit" type="submit" name="b1" value="buttonValue" hx-post="/test">button</button>' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit input with a value inside a non-htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form>' +
'<input type="text" name="t1" value="textValue">' +
'<input id="submit" type="submit" name="b1" value="buttonValue" hx-post="/test">' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit button with a value outside a htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'</form>' +
'<button id="submit" form="externalForm" type="submit" name="b1" value="buttonValue">button</button>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit input with a value outside a htmx form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'</form>' +
'<input id="submit" form="externalForm" type="submit" name="b1" value="buttonValue">');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: 'buttonValue'});
})

it('properly handles clicked submit button with a value stacking with regular input', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form hx-post="/test">' +
'<input type="hidden" name="action" value="A">' +
'<button id="btnA" type="submit">A</button>' +
'<button id="btnB" type="submit" name="action" value="B">B</button>' +
'<button id="btnC" type="submit" name="action" value="C">C</button>' +
'</form>');

byId("btnA").click();
this.server.respond();
values.should.deep.equal({action: 'A'});

byId("btnB").click();
this.server.respond();
values.should.deep.equal({action: ['A', 'B']});

byId("btnC").click();
this.server.respond();
values.should.deep.equal({action: ['A', 'C']});
})

it('properly handles clicked submit input with a value stacking with regular input', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form hx-post="/test">' +
'<input type="hidden" name="action" value="A">' +
'<input id="btnA" type="submit">A</input>' +
'<input id="btnB" type="submit" name="action" value="B">B</input>' +
'<input id="btnC" type="submit" name="action" value="C">C</input>' +
'</form>');

byId("btnA").click();
this.server.respond();
values.should.deep.equal({action: 'A'});

byId("btnB").click();
this.server.respond();
values.should.deep.equal({action: ['A', 'B']});

byId("btnC").click();
this.server.respond();
values.should.deep.equal({action: ['A', 'C']});
})

it('properly handles clicked submit button with a value inside a form, referencing another form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<input type="hidden" name="b1" value="inputValue">' +
'</form>' +
'<form hx-post="/test2">' +
'<button id="submit" form="externalForm" type="submit" name="b1" value="buttonValue">button</button>' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: ['inputValue', 'buttonValue']});
})

it('properly handles clicked submit input with a value inside a form, referencing another form', function () {
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});

make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<input type="hidden" name="b1" value="inputValue">' +
'</form>' +
'<form hx-post="/test2">' +
'<input id="submit" form="externalForm" type="submit" name="b1" value="buttonValue">' +
'</form>');

byId("submit").click();
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: ['inputValue', 'buttonValue']});
})
})
Loading