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", "AJAX",
"HTML" "HTML"
], ],
"version": "1.2.1", "version": "1.3.0",
"homepage": "https://htmx.org/", "homepage": "https://htmx.org/",
"bugs": { "bugs": {
"url": "https://github.com/bigskysoftware/htmx/issues" "url": "https://github.com/bigskysoftware/htmx/issues"

View File

@ -847,12 +847,17 @@ return (function () {
triggerSpec.changed = true; triggerSpec.changed = true;
} else if (token === "once") { } else if (token === "once") {
triggerSpec.once = true; triggerSpec.once = true;
} else if (token === "consume") {
triggerSpec.consume = true;
} else if (token === "delay" && tokens[0] === ":") { } else if (token === "delay" && tokens[0] === ":") {
tokens.shift(); tokens.shift();
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
} else if (token === "from" && tokens[0] === ":") { } else if (token === "from" && tokens[0] === ":") {
tokens.shift(); tokens.shift();
triggerSpec.from = consumeUntil(tokens, WHITESPACE_OR_COMMA); 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] === ":") { } else if (token === "throttle" && tokens[0] === ":") {
tokens.shift(); tokens.shift();
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
@ -962,9 +967,20 @@ return (function () {
return; return;
} }
var eventData = getInternalData(evt); var eventData = getInternalData(evt);
if (eventData.handledFor == null) {
eventData.handledFor = [];
}
var elementData = getInternalData(elt); var elementData = getInternalData(elt);
if (!eventData.handled) { if (eventData.handledFor.indexOf(elt) < 0) {
eventData.handled = true; 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 (triggerSpec.once) {
if (elementData.triggeredOnce) { if (elementData.triggeredOnce) {
return; return;
@ -1174,6 +1190,7 @@ return (function () {
var settleInfo = makeSettleInfo(elt); var settleInfo = makeSettleInfo(elt);
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo) selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
settleImmediately(settleInfo.tasks)
triggerEvent(elt, "htmx:sseMessage", event) triggerEvent(elt, "htmx:sseMessage", event)
}; };
@ -1427,7 +1444,7 @@ return (function () {
while(historyCache.length > 0){ while(historyCache.length > 0){
try { try {
localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache)); localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
return; break;
} catch (e) { } catch (e) {
triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache}) triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache})
historyCache.shift(); // shrink the cache and retry historyCache.shift(); // shrink the cache and retry
@ -1923,16 +1940,22 @@ return (function () {
function ajaxHelper(verb, path, context) { function ajaxHelper(verb, path, context) {
if (context) { if (context) {
if (context instanceof Element || isType(context, 'String')) { 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 { } 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 { } else {
issueAjaxRequest(verb, path); return issueAjaxRequest(verb, path);
} }
} }
function issueAjaxRequest(verb, path, elt, event, responseHandler, targetOverride) { 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) { if(elt == null) {
elt = getDocument().body; elt = getDocument().body;
} }
@ -1970,12 +1993,18 @@ return (function () {
// prompt returns null if cancelled and empty string if accepted with no entry // prompt returns null if cancelled and empty string if accepted with no entry
if (promptResponse === null || if (promptResponse === null ||
!triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target}))
return endRequestLock(); resolve();
endRequestLock();
return promise;
} }
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
if (confirmQuestion) { if (confirmQuestion) {
if(!confirm(confirmQuestion)) return endRequestLock(); if(!confirm(confirmQuestion)) {
resolve();
endRequestLock()
return promise;
}
} }
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
@ -2018,7 +2047,9 @@ return (function () {
if(errors && errors.length > 0){ if(errors && errors.length > 0){
triggerEvent(elt, 'htmx:validation:halted', requestConfig) triggerEvent(elt, 'htmx:validation:halted', requestConfig)
return endRequestLock(); resolve();
endRequestLock();
return promise;
} }
var splitPath = path.split("#"); var splitPath = path.split("#");
@ -2071,6 +2102,7 @@ return (function () {
} }
triggerEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerEvent(finalElt, 'htmx:afterRequest', responseInfo);
triggerEvent(finalElt, 'htmx:afterOnLoad', responseInfo); triggerEvent(finalElt, 'htmx:afterOnLoad', responseInfo);
resolve();
endRequestLock(); endRequestLock();
} }
} }
@ -2082,6 +2114,7 @@ return (function () {
} }
triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo);
triggerErrorEvent(finalElt, 'htmx:sendError', responseInfo); triggerErrorEvent(finalElt, 'htmx:sendError', responseInfo);
reject();
endRequestLock(); endRequestLock();
} }
xhr.onabort = function() { xhr.onabort = function() {
@ -2092,9 +2125,14 @@ return (function () {
} }
triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(finalElt, 'htmx:afterRequest', responseInfo);
triggerErrorEvent(finalElt, 'htmx:sendAbort', responseInfo); triggerErrorEvent(finalElt, 'htmx:sendAbort', responseInfo);
reject();
endRequestLock(); endRequestLock();
} }
if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)) return endRequestLock(); if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){
resolve();
endRequestLock()
return promise
}
var indicators = addRequestIndicatorClasses(elt); var indicators = addRequestIndicatorClasses(elt);
forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) { forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
@ -2109,6 +2147,7 @@ return (function () {
}); });
}); });
xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters)); xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
return promise;
} }
function handleAjaxResponse(elt, responseInfo) { function handleAjaxResponse(elt, responseInfo) {

View File

@ -73,26 +73,22 @@ describe("hx-swap attribute", function(){
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1*"); div.innerText.should.equal("1*");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**"); div.innerText.should.equal("21*");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("2**"); div.innerText.should.equal("321*");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
}); });
it('swap afterbegin properly with no initial content', function() it('swap afterbegin properly with no initial content', function()
@ -100,26 +96,22 @@ describe("hx-swap attribute", function(){
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1"); div.innerText.should.equal("1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*"); div.innerText.should.equal("21");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("2*"); div.innerText.should.equal("321");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
}); });
it('swap afterend properly', function() it('swap afterend properly', function()
@ -152,58 +144,50 @@ describe("hx-swap attribute", function(){
removeWhiteSpace(parent.innerText).should.equal("***"); removeWhiteSpace(parent.innerText).should.equal("***");
}); });
it('swap beforeend properly', function() it('handles beforeend properly', function()
{ {
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*1"); div.innerText.should.equal("*1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**"); div.innerText.should.equal("*12");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**2"); div.innerText.should.equal("*123");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
}); });
it('swap beforeend properly with no initial content', function() it('handles beforeend properly with no initial content', function()
{ {
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1"); div.innerText.should.equal("1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*"); div.innerText.should.equal("12");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*2"); div.innerText.should.equal("123");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
}); });
it('properly parses various swap specifications', function(){ it('properly parses various swap specifications', function(){

View File

@ -385,15 +385,18 @@ describe("hx-trigger attribute", function(){
this.server.respond(); this.server.respond();
requests.should.equal(1); requests.should.equal(1);
requests.should.equal(1);
div1.click(); div1.click();
this.server.respond(); this.server.respond();
div1.innerHTML.should.equal("Clicked"); div1.innerHTML.should.equal("Clicked");
requests.should.equal(2);
document.body.click(); document.body.click();
this.server.respond(); this.server.respond();
// event listener should have been removed when this element was removed requests.should.equal(2);
requests.should.equal(1);
}); });
it('multiple triggers with from clauses mixed in work', function() 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"); 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; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1*"); div.innerText.should.equal("1*");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**"); div.innerText.should.equal("21*");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("2**"); div.innerText.should.equal("321*");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
}); });
it('handles afterbegin properly with no initial content', function() it('handles afterbegin properly with no initial content', function()
@ -111,26 +107,22 @@ describe("Core htmx AJAX Tests", function(){
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1"); div.innerText.should.equal("1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*"); div.innerText.should.equal("21");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("2*"); div.innerText.should.equal("321");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
}); });
it('handles afterend properly', function() it('handles afterend properly', function()
@ -168,26 +160,22 @@ describe("Core htmx AJAX Tests", function(){
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*1"); div.innerText.should.equal("*1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**"); div.innerText.should.equal("*12");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("**2"); div.innerText.should.equal("*123");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
}); });
it('handles beforeend properly with no initial content', function() it('handles beforeend properly with no initial content', function()
@ -195,26 +183,22 @@ describe("Core htmx AJAX Tests", function(){
var i = 0; var i = 0;
this.server.respondWith("GET", "/test", function(xhr){ this.server.respondWith("GET", "/test", function(xhr){
i++; 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>') var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("1"); div.innerText.should.equal("1");
byId("a1").click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*"); div.innerText.should.equal("12");
div.click(); div.click();
this.server.respond(); this.server.respond();
div.innerText.should.equal("*2"); div.innerText.should.equal("123");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
}); });
it('handles hx-target properly', function() 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() 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"); this.server.respondWith("GET", "/test2", "clicked");
var div = make("<div hx-get='/test'/>"); var div = make("<div hx-get='/test'/>");
div.click(); div.click();
@ -561,7 +545,6 @@ describe("Core htmx AJAX Tests", function(){
byId("d1").innerHTML.should.equal("clicked"); byId("d1").innerHTML.should.equal("clicked");
}); });
it('script nodes evaluate', function() it('script nodes evaluate', function()
{ {
var globalWasCalled = false; var globalWasCalled = false;

View File

@ -261,5 +261,36 @@ describe("Core htmx API test", function(){
calledEvent.should.equal(true); 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']); 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); 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 * `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. 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) * `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 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: and the user hasn't typed anything new for 1 second: