summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorVincent <vichenzo-thebaud@hotmail.com>2023-07-25 20:55:22 +0200
committerGitHub <noreply@github.com>2023-07-25 12:55:22 -0600
commit88a57cbc3927dc6f808220b0d51e09fa5570d2b9 (patch)
tree2d0e37eb1093152bfd1356c60ef8c011a65d64aa
parent6d1adc853f49ac74f1d3e99f2ea2b5a3567e02e8 (diff)
downloadhtmx-88a57cbc3927dc6f808220b0d51e09fa5570d2b9.tar.gz
htmx-88a57cbc3927dc6f808220b0d51e09fa5570d2b9.zip
[Bugfix] Fix submit buttons/inputs handling (#1559)
* Fix submit buttons/inputs handling * Removed useless concatenation * Don't initNode links and forms that shouldn't be initialized * Removed selectors filtering to alleviate PR * Add @1cg 's addValueToValues function to factor code * Use formValues variable for clicked button's value including
-rw-r--r--src/htmx.js85
-rw-r--r--test/core/ajax.js195
-rw-r--r--test/ext/ws.js158
3 files changed, 401 insertions, 37 deletions
diff --git a/src/htmx.js b/src/htmx.js
index 9fd7d63b..25971690 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -1304,7 +1304,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'}];
@@ -1871,8 +1871,8 @@ 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 {
@@ -1880,8 +1880,15 @@ return (function () {
}
}
- 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);
@@ -1893,9 +1900,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;
})
@@ -2004,8 +2011,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');
@@ -2336,6 +2345,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;
@@ -2352,28 +2384,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);
}
@@ -2423,11 +2434,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
diff --git a/test/core/ajax.js b/test/core/ajax.js
index 1b28f70a..749729cf 100644
--- a/test/core/ajax.js
+++ b/test/core/ajax.js
@@ -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']});
+ })
})
diff --git a/test/ext/ws.js b/test/ext/ws.js
index a9f5de55..442872d8 100644
--- a/test/ext/ws.js
+++ b/test/ext/ws.js
@@ -327,4 +327,162 @@ describe("web-sockets extension", function () {
htmx.off("htmx:wsAfterMessage", handle)
})
+
+ it('sends data to the server with non-htmx form + submit button & value', function () {
+ make('<form hx-ext="ws" ws-connect="ws://localhost:8080" ws-send>' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '<button id="b1" type="submit" name="action" value="A">A</button>' +
+ '<button id="b2" type="submit" name="action" value="B">B</button>' +
+ '</form>');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
+
+ it('sends data to the server with non-htmx form + submit input & value', function () {
+ make('<form hx-ext="ws" ws-connect="ws://localhost:8080" ws-send>' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '<input id="b1" type="submit" name="action" value="A">' +
+ '<input id="b2" type="submit" name="action" value="B">' +
+ '</form>');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
+
+ it('sends data to the server with child non-htmx form + submit button & value', function () {
+ make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
+ '<form ws-send>' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '<button id="b1" type="submit" name="action" value="A">A</button>' +
+ '<button id="b2" type="submit" name="action" value="B">B</button>' +
+ '</form>' +
+ '</div>');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
+
+ it('sends data to the server with child non-htmx form + submit input & value', function () {
+ make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
+ '<form ws-send>' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '<input id="b1" type="submit" name="action" value="A">' +
+ '<input id="b2" type="submit" name="action" value="B">' +
+ '</form>' +
+ '</div>');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
+
+ it('sends data to the server with external non-htmx form + submit button & value', function () {
+ make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
+ '<form ws-send id="form">' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '</form>' +
+ '</div>' +
+ '<button id="b1" form="form" type="submit" name="action" value="A">A</button>' +
+ '<button id="b2" form="form" type="submit" name="action" value="B">B</button>');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
+
+ it('sends data to the server with external non-htmx form + submit input & value', function () {
+ make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
+ '<form ws-send id="form">' +
+ '<input type="hidden" name="foo" value="bar">' +
+ '</form>' +
+ '</div>' +
+ '<input id="b1" form="form" type="submit" name="action" value="A">' +
+ '<input id="b2" form="form" type="submit" name="action" value="B">');
+ this.tickMock();
+
+ byId("b1").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(1);
+ this.messages[0].should.contains('"foo":"bar"')
+ this.messages[0].should.contains('"action":"A"')
+
+ byId("b2").click();
+
+ this.tickMock();
+
+ this.messages.length.should.equal(2);
+ this.messages[1].should.contains('"foo":"bar"')
+ this.messages[1].should.contains('"action":"B"')
+ })
});