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("");
+ var button = byId("b1");
+ htmx._("shouldCancel")(button).should.equal(true);
+
+ })
+
});
\ No newline at end of file
diff --git a/test/core/regressions.js b/test/core/regressions.js
index 5d0b7555..5525fe12 100644
--- a/test/core/regressions.js
+++ b/test/core/regressions.js
@@ -110,4 +110,21 @@ describe("Core htmx Regression Tests", function(){
defaultPrevented.should.equal(true);
})
+ it('two elements can listen for the same event on another element', function() {
+ this.server.respondWith("GET", "/test", "triggered");
+
+ make('' +
+ ' ');
+
+
+ var div1 = byId("d1");
+ var div2 = byId("d2");
+
+ document.body.click();
+ this.server.respond();
+
+ div2.innerHTML.should.equal("triggered");
+ div1.innerHTML.should.equal("triggered");
+ })
+
})
diff --git a/www/attributes/hx-trigger.md b/www/attributes/hx-trigger.md
index a315873a..64a97df8 100644
--- a/www/attributes/hx-trigger.md
+++ b/www/attributes/hx-trigger.md
@@ -58,6 +58,9 @@ is seen again it will reset the delay.
* `throttle:` - a throttle will occur before an event triggers a request. If the event
is seen again before the delay completes it is ignored, the element will trigger at the end of the delay.
* `from:` - allows the event that triggers a request to come from another element in the document (e.g. listening to a key event on the body, to support hot keys)
+* `target:` - allows you to filter via a CSS selector on the target of the event. This can be useful when you want to listen for
+triggers from elements that might not be in the DOM at the point of initialization, by listening on the body, but with a target filter for a
+child
Here is an example of a search box that searches on `keyup`, but only if the search value has changed
and the user hasn't typed anything new for 1 second: