diff options
author | Vincent <vichenzo-thebaud@hotmail.com> | 2023-07-25 20:55:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-25 12:55:22 -0600 |
commit | 88a57cbc3927dc6f808220b0d51e09fa5570d2b9 (patch) | |
tree | 2d0e37eb1093152bfd1356c60ef8c011a65d64aa | |
parent | 6d1adc853f49ac74f1d3e99f2ea2b5a3567e02e8 (diff) | |
download | htmx-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.js | 85 | ||||
-rw-r--r-- | test/core/ajax.js | 195 | ||||
-rw-r--r-- | test/ext/ws.js | 158 |
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"') + }) }); |