This commit is contained in:
Bo Peng 2021-02-27 21:47:22 -06:00
commit 8d655da761
9 changed files with 222 additions and 89 deletions

View File

@ -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"

View File

@ -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) {

View File

@ -73,26 +73,22 @@ describe("hx-swap attribute", function(){
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
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(){

View File

@ -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('<div>' +
'<div id="d1" hx-trigger="click from:body target:#d3" hx-get="/test">Requests: 0</div>' +
'<div id="d2"></div>' +
'<div id="d3"></div>' +
'</div>');
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("<div hx-trigger='click' hx-get='/foo'>" +
" <div id='d1' hx-trigger='click consume' hx-get='/bar'></div>" +
"</div>");
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");
});
})

View File

@ -84,26 +84,22 @@ describe("Core htmx AJAX Tests", function(){
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
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, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
xhr.respond(200, {}, "" + i);
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
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", "<div id='d1' hx-get='/test2'></div>fooo");
this.server.respondWith("GET", "/test", "<div id='d1' hx-trigger='click consume' hx-get='/test2'></div>fooo");
this.server.respondWith("GET", "/test2", "clicked");
var div = make("<div hx-get='/test'/>");
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;

View File

@ -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("<div></div>");
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("<div id='d1'></div>");
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("<div id='d1'></div>");
var promise = htmx.ajax("GET", "/test", "#d1");
this.server.respond();
div.innerHTML.should.equal("foo!");
promise.then(function(){
done();
})
});
})

View File

@ -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("<a href='/foo'></a>");
htmx._("shouldCancel")(anchorThatShouldCancel).should.equal(true);
var anchorThatShouldNotCancel = make("<a href='#'></a>");
htmx._("shouldCancel")(anchorThatShouldNotCancel).should.equal(false);
var form = make("<form></form>");
htmx._("shouldCancel")(form).should.equal(true);
var form = make("<form><input id='i1' type='submit'></form>");
var input = byId("i1");
htmx._("shouldCancel")(input).should.equal(true);
var form = make("<form><button id='b1' type='submit'></form>");
var button = byId("b1");
htmx._("shouldCancel")(button).should.equal(true);
})
});

View File

@ -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('<div id="d1" hx-trigger="click from:body" hx-get="/test"></div>' +
' <div id="d2" hx-trigger="click from:body" hx-get="/test"></div>');
var div1 = byId("d1");
var div2 = byId("d2");
document.body.click();
this.server.respond();
div2.innerHTML.should.equal("triggered");
div1.innerHTML.should.equal("triggered");
})
})

View File

@ -58,6 +58,9 @@ is seen again it will reset the delay.
* `throttle:<timing declaration>` - 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:<CSS selector>` - 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:<CSS selector>` - 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: