diff --git a/package.json b/package.json index a086fd05..2989f5f4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "AJAX", "HTML" ], - "version": "1.2.1", + "version": "1.3.0", "homepage": "https://htmx.org/", "bugs": { "url": "https://github.com/bigskysoftware/htmx/issues" diff --git a/src/htmx.js b/src/htmx.js index a261cda3..ecd5aabc 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -847,12 +847,17 @@ return (function () { triggerSpec.changed = true; } else if (token === "once") { triggerSpec.once = true; + } else if (token === "consume") { + triggerSpec.consume = true; } else if (token === "delay" && tokens[0] === ":") { tokens.shift(); triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); } else if (token === "from" && tokens[0] === ":") { tokens.shift(); triggerSpec.from = consumeUntil(tokens, WHITESPACE_OR_COMMA); + } else if (token === "target" && tokens[0] === ":") { + tokens.shift(); + triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA); } else if (token === "throttle" && tokens[0] === ":") { tokens.shift(); triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); @@ -962,9 +967,20 @@ return (function () { return; } var eventData = getInternalData(evt); + if (eventData.handledFor == null) { + eventData.handledFor = []; + } var elementData = getInternalData(elt); - if (!eventData.handled) { - eventData.handled = true; + if (eventData.handledFor.indexOf(elt) < 0) { + eventData.handledFor.push(elt); + if (triggerSpec.consume) { + evt.stopPropagation(); + } + if (triggerSpec.target && evt.target) { + if (!evt.target.matches(triggerSpec.target)) { + return; + } + } if (triggerSpec.once) { if (elementData.triggeredOnce) { return; @@ -1174,6 +1190,7 @@ return (function () { var settleInfo = makeSettleInfo(elt); selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo) + settleImmediately(settleInfo.tasks) triggerEvent(elt, "htmx:sseMessage", event) }; @@ -1427,7 +1444,7 @@ return (function () { while(historyCache.length > 0){ try { localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache)); - return; + break; } catch (e) { triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache}) historyCache.shift(); // shrink the cache and retry @@ -1923,16 +1940,22 @@ return (function () { function ajaxHelper(verb, path, context) { if (context) { if (context instanceof Element || isType(context, 'String')) { - issueAjaxRequest(verb, path, null, null, null, resolveTarget(context)); + return issueAjaxRequest(verb, path, null, null, null, resolveTarget(context)); } else { - issueAjaxRequest(verb, path, resolveTarget(context.source), context.event, context.handler, resolveTarget(context.target)); + return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event, context.handler, resolveTarget(context.target)); } } else { - issueAjaxRequest(verb, path); + return issueAjaxRequest(verb, path); } } function issueAjaxRequest(verb, path, elt, event, responseHandler, targetOverride) { + var resolve = null; + var reject = null; + var promise = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); if(elt == null) { elt = getDocument().body; } @@ -1970,12 +1993,18 @@ return (function () { // prompt returns null if cancelled and empty string if accepted with no entry if (promptResponse === null || !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) - return endRequestLock(); + resolve(); + endRequestLock(); + return promise; } var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); if (confirmQuestion) { - if(!confirm(confirmQuestion)) return endRequestLock(); + if(!confirm(confirmQuestion)) { + resolve(); + endRequestLock() + return promise; + } } var xhr = new XMLHttpRequest(); @@ -2018,7 +2047,9 @@ return (function () { if(errors && errors.length > 0){ triggerEvent(elt, 'htmx:validation:halted', requestConfig) - return endRequestLock(); + resolve(); + endRequestLock(); + return promise; } var splitPath = path.split("#"); @@ -2071,6 +2102,7 @@ return (function () { } triggerEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerEvent(finalElt, 'htmx:afterOnLoad', responseInfo); + resolve(); endRequestLock(); } } @@ -2082,6 +2114,7 @@ return (function () { } triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(finalElt, 'htmx:sendError', responseInfo); + reject(); endRequestLock(); } xhr.onabort = function() { @@ -2092,9 +2125,14 @@ return (function () { } triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(finalElt, 'htmx:sendAbort', responseInfo); + reject(); endRequestLock(); } - if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)) return endRequestLock(); + if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){ + resolve(); + endRequestLock() + return promise + } var indicators = addRequestIndicatorClasses(elt); forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) { @@ -2109,6 +2147,7 @@ return (function () { }); }); xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters)); + return promise; } function handleAjaxResponse(elt, responseInfo) { diff --git a/test/attributes/hx-swap.js b/test/attributes/hx-swap.js index 4c78f379..78063725 100644 --- a/test/attributes/hx-swap.js +++ b/test/attributes/hx-swap.js @@ -73,26 +73,22 @@ describe("hx-swap attribute", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
*
') + div.click(); this.server.respond(); div.innerText.should.equal("1*"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("21*"); div.click(); this.server.respond(); - div.innerText.should.equal("2**"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("***"); + div.innerText.should.equal("321*"); }); it('swap afterbegin properly with no initial content', function() @@ -100,26 +96,22 @@ describe("hx-swap attribute", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
') + div.click(); this.server.respond(); div.innerText.should.equal("1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("*"); + div.innerText.should.equal("21"); div.click(); this.server.respond(); - div.innerText.should.equal("2*"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("321"); }); it('swap afterend properly', function() @@ -152,58 +144,50 @@ describe("hx-swap attribute", function(){ removeWhiteSpace(parent.innerText).should.equal("***"); }); - it('swap beforeend properly', function() + it('handles beforeend properly', function() { var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
*
') + div.click(); this.server.respond(); div.innerText.should.equal("*1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("*12"); div.click(); this.server.respond(); - div.innerText.should.equal("**2"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("***"); + div.innerText.should.equal("*123"); }); - it('swap beforeend properly with no initial content', function() + it('handles beforeend properly with no initial content', function() { var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
') + div.click(); this.server.respond(); div.innerText.should.equal("1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("*"); + div.innerText.should.equal("12"); div.click(); this.server.respond(); - div.innerText.should.equal("*2"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("123"); }); it('properly parses various swap specifications', function(){ diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js index 5b4411b1..4560951e 100644 --- a/test/attributes/hx-trigger.js +++ b/test/attributes/hx-trigger.js @@ -385,15 +385,18 @@ describe("hx-trigger attribute", function(){ this.server.respond(); requests.should.equal(1); + requests.should.equal(1); + div1.click(); this.server.respond(); div1.innerHTML.should.equal("Clicked"); + requests.should.equal(2); + document.body.click(); this.server.respond(); - // event listener should have been removed when this element was removed - requests.should.equal(1); + requests.should.equal(2); }); it('multiple triggers with from clauses mixed in work', function() @@ -414,4 +417,57 @@ describe("hx-trigger attribute", function(){ div1.innerHTML.should.equal("Requests: 2"); }); + it('event listeners can filter on target', function() + { + var requests = 0; + this.server.respondWith("GET", "/test", function (xhr) { + requests++; + xhr.respond(200, {}, "Requests: " + requests); + }); + + var div1 = make('
' + + '
Requests: 0
' + + '
' + + '
' + + '
'); + var div1 = byId("d1"); + var div2 = byId("d2"); + var div3 = byId("d3"); + + div1.innerHTML.should.equal("Requests: 0"); + document.body.click(); + this.server.respond(); + requests.should.equal(0); + + div1.click(); + this.server.respond(); + requests.should.equal(0); + + div2.click(); + this.server.respond(); + requests.should.equal(0); + + div3.click(); + this.server.respond(); + requests.should.equal(1); + + }); + + it('consume prevents event propogation', function() + { + this.server.respondWith("GET", "/foo", "foo"); + this.server.respondWith("GET", "/bar", "bar"); + var div = make("
" + + "
" + + "
"); + + byId("d1").click(); + this.server.respond(); + + // should not have been replaced by click + byId("d1").parentElement.should.equal(div); + byId("d1").innerText.should.equal("bar"); + }); + + }) diff --git a/test/core/ajax.js b/test/core/ajax.js index d517ada9..942f2713 100644 --- a/test/core/ajax.js +++ b/test/core/ajax.js @@ -84,26 +84,22 @@ describe("Core htmx AJAX Tests", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
*
') + div.click(); this.server.respond(); div.innerText.should.equal("1*"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("21*"); div.click(); this.server.respond(); - div.innerText.should.equal("2**"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("***"); + div.innerText.should.equal("321*"); }); it('handles afterbegin properly with no initial content', function() @@ -111,26 +107,22 @@ describe("Core htmx AJAX Tests", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
') + div.click(); this.server.respond(); div.innerText.should.equal("1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("*"); + div.innerText.should.equal("21"); div.click(); this.server.respond(); - div.innerText.should.equal("2*"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("321"); }); it('handles afterend properly', function() @@ -168,26 +160,22 @@ describe("Core htmx AJAX Tests", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
*
') + div.click(); this.server.respond(); div.innerText.should.equal("*1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("*12"); div.click(); this.server.respond(); - div.innerText.should.equal("**2"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("***"); + div.innerText.should.equal("*123"); }); it('handles beforeend properly with no initial content', function() @@ -195,26 +183,22 @@ describe("Core htmx AJAX Tests", function(){ var i = 0; this.server.respondWith("GET", "/test", function(xhr){ i++; - xhr.respond(200, {}, '' + i + ''); + xhr.respond(200, {}, "" + i); }); - this.server.respondWith("GET", "/test2", "*"); var div = make('
') + div.click(); this.server.respond(); div.innerText.should.equal("1"); - byId("a1").click(); + div.click(); this.server.respond(); - div.innerText.should.equal("*"); + div.innerText.should.equal("12"); div.click(); this.server.respond(); - div.innerText.should.equal("*2"); - - byId("a2").click(); - this.server.respond(); - div.innerText.should.equal("**"); + div.innerText.should.equal("123"); }); it('handles hx-target properly', function() @@ -551,7 +535,7 @@ describe("Core htmx AJAX Tests", function(){ it('text nodes dont screw up settling via variable capture', function() { - this.server.respondWith("GET", "/test", "
fooo"); + this.server.respondWith("GET", "/test", "
fooo"); this.server.respondWith("GET", "/test2", "clicked"); var div = make("
"); div.click(); @@ -561,7 +545,6 @@ describe("Core htmx AJAX Tests", function(){ byId("d1").innerHTML.should.equal("clicked"); }); - it('script nodes evaluate', function() { var globalWasCalled = false; diff --git a/test/core/api.js b/test/core/api.js index 856a7ac1..1d1a7e7a 100644 --- a/test/core/api.js +++ b/test/core/api.js @@ -261,5 +261,36 @@ describe("Core htmx API test", function(){ calledEvent.should.equal(true); }); + it('ajax api works', function() + { + this.server.respondWith("GET", "/test", "foo!"); + var div = make("
"); + htmx.ajax("GET", "/test", div) + this.server.respond(); + div.innerHTML.should.equal("foo!"); + }); + + it('ajax api works by ID', function() + { + this.server.respondWith("GET", "/test", "foo!"); + var div = make("
"); + htmx.ajax("GET", "/test", "#d1") + this.server.respond(); + div.innerHTML.should.equal("foo!"); + }); + + it('ajax returns a promise', function(done) + { + this.server.respondWith("GET", "/test", "foo!"); + var div = make("
"); + var promise = htmx.ajax("GET", "/test", "#d1"); + this.server.respond(); + div.innerHTML.should.equal("foo!"); + promise.then(function(){ + done(); + }) + }); + + }) diff --git a/test/core/internals.js b/test/core/internals.js index 1af2293c..e9513420 100644 --- a/test/core/internals.js +++ b/test/core/internals.js @@ -41,4 +41,24 @@ describe("Core htmx internals Tests", function() { chai.expect(htmx._("tokenizeString")("aa.aa")).to.be.deep.equal(['aa', '.', 'aa']); }) + it("tags respond correctly to shouldCancel", function() { + var anchorThatShouldCancel = make(""); + htmx._("shouldCancel")(anchorThatShouldCancel).should.equal(true); + + var anchorThatShouldNotCancel = make(""); + htmx._("shouldCancel")(anchorThatShouldNotCancel).should.equal(false); + + var form = make("
"); + htmx._("shouldCancel")(form).should.equal(true); + + var form = make("
"); + var input = byId("i1"); + htmx._("shouldCancel")(input).should.equal(true); + + var form = make("