summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorcarson <carson@leaddyno.com>2020-10-17 13:21:15 -0600
committercarson <carson@leaddyno.com>2020-10-17 13:21:15 -0600
commit165586b77752a2873cec67bc9561a471ca70e1f6 (patch)
tree6a05379705ce6e47bb7331dfcf56245f93828383
parent6c8c124c420b90b4c0527557af3a9466d74a0b53 (diff)
downloadhtmx-165586b77752a2873cec67bc9561a471ca70e1f6.tar.gz
htmx-165586b77752a2873cec67bc9561a471ca70e1f6.zip
docs and tests for trigger filters
-rw-r--r--src/htmx.js35
-rw-r--r--test/attributes/hx-push-url.js2
-rw-r--r--test/attributes/hx-trigger.js127
-rw-r--r--test/core/tokenizer.js8
-rw-r--r--www/attributes/hx-trigger.md31
-rw-r--r--www/docs.md22
6 files changed, 205 insertions, 20 deletions
diff --git a/src/htmx.js b/src/htmx.js
index a5fa83c5..178ea9e0 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -688,7 +688,7 @@ return (function () {
if (tokens[0] === '[') {
tokens.shift();
var bracketCount = 1;
- var conditional = "(function(" + paramName + "){ return (";
+ var conditionalSource = "(function(" + paramName + "){ return (";
var last = null;
while (tokens.length > 0) {
var token = tokens[0];
@@ -696,23 +696,26 @@ return (function () {
bracketCount--;
if (bracketCount === 0) {
if (last === null) {
- conditional = conditional + "true";
+ conditionalSource = conditionalSource + "true";
}
tokens.shift();
- conditional += ")})";
+ conditionalSource += ")})";
try {
- return eval(conditional);
+ var conditionFunction = eval(conditionalSource);
+ conditionFunction.source = conditionalSource;
+ return conditionFunction;
} catch (e) {
- triggerErrorEvent(getDocument(), "htmx:syntax:error", {error:e})
+ triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource})
+ return null;
}
}
} else if (token === "[") {
bracketCount++;
}
if (isPossibleRelativeReference(token, last, paramName)) {
- conditional += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
+ conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
} else {
- conditional = conditional + token;
+ conditionalSource = conditionalSource + token;
}
last = tokens.shift();
}
@@ -746,7 +749,7 @@ return (function () {
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
} else {
var triggerSpec = {trigger: trigger};
- var eventFilter = maybeGenerateConditional(tokens, "evt");
+ var eventFilter = maybeGenerateConditional(tokens, "event");
if (eventFilter) {
triggerSpec.eventFilter = eventFilter;
}
@@ -836,10 +839,22 @@ return (function () {
return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && evt.ctrlKey;
}
+ function maybeFilterEvent(triggerSpec, evt) {
+ var eventFilter = triggerSpec.eventFilter;
+ if(eventFilter){
+ try {
+ return eventFilter(evt) !== true;
+ } catch(e) {
+ triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source});
+ return true;
+ }
+ }
+ return false;
+ }
+
function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) {
var eventListener = function (evt) {
- if (triggerSpec.eventFilter &&
- triggerSpec.eventFilter(evt) !== true) {
+ if (maybeFilterEvent(triggerSpec, evt)) {
return;
}
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
diff --git a/test/attributes/hx-push-url.js b/test/attributes/hx-push-url.js
index 604a6d3c..80f5f366 100644
--- a/test/attributes/hx-push-url.js
+++ b/test/attributes/hx-push-url.js
@@ -37,7 +37,6 @@ describe("hx-push-url attribute", function() {
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
- console.log(cache);
cache.length.should.equal(2);
cache[1].url.should.equal("/abc123");
});
@@ -193,7 +192,6 @@ describe("hx-push-url attribute", function() {
for (var i = 0; i < 20; i++) {
bigContent += bigContent;
}
- console.log(bigContent.length);
try {
localStorage.removeItem("htmx-history-cache");
htmx._("saveToHistoryCache")("/dummy", bigContent, "Foo", 0);
diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js
index 8752f864..25431301 100644
--- a/test/attributes/hx-trigger.js
+++ b/test/attributes/hx-trigger.js
@@ -152,7 +152,7 @@ describe("hx-trigger attribute", function(){
spec.should.deep.equal([{trigger: 'change'}]);
})
- it('filters properly with filter spec', function(){
+ it('filters properly with false filter spec', function(){
this.server.respondWith("GET", "/test", "Called!");
var form = make('<form hx-get="/test" hx-trigger="evt[foo]">Not Called</form>');
form.click();
@@ -161,10 +161,135 @@ describe("hx-trigger attribute", function(){
form.dispatchEvent(event);
this.server.respond();
form.innerHTML.should.equal("Not Called");
+ })
+
+ it('filters properly with true filter spec', function(){
+ this.server.respondWith("GET", "/test", "Called!");
+ var form = make('<form hx-get="/test" hx-trigger="evt[foo]">Not Called</form>');
+ form.click();
+ form.innerHTML.should.equal("Not Called");
+ var event = htmx._("makeEvent")('evt');
event.foo = true;
form.dispatchEvent(event);
this.server.respond();
form.innerHTML.should.equal("Called!");
})
+ it('filters properly compound filter spec', function(){
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[foo&&bar]">Not Called</div>');
+ var event = htmx._("makeEvent")('evt');
+ event.foo = true;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ event.bar = true;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ })
+
+ it('can refer to target element in condition', function(){
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[target.classList.contains(\'doIt\')]">Not Called</div>');
+ var event = htmx._("makeEvent")('evt');
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ div.classList.add("doIt");
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ })
+
+ it('negative condition', function(){
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[!target.classList.contains(\'disabled\')]">Not Called</div>');
+ div.classList.add("disabled");
+ var event = htmx._("makeEvent")('evt');
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ div.classList.remove("disabled");
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ })
+
+ it('global function call works', function(){
+ window.globalFun = function(evt) {
+ return evt.bar;
+ }
+ try {
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[globalFun(event)]">Not Called</div>');
+ var event = htmx._("makeEvent")('evt');
+ event.bar = false;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ event.bar = true;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ } finally {
+ delete window.globalFun;
+ }
+ })
+
+ it('global property event filter works', function(){
+ window.foo = {
+ bar:false
+ }
+ try {
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[foo.bar]">Not Called</div>');
+ var event = htmx._("makeEvent")('evt');
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ foo.bar = true;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ } finally {
+ delete window.foo;
+ }
+ })
+
+ it('global variable filter works', function(){
+ try {
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[foo]">Not Called</div>');
+ var event = htmx._("makeEvent")('evt');
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Not Called");
+ foo = true;
+ div.dispatchEvent(event);
+ this.server.respond();
+ div.innerHTML.should.equal("Called!");
+ } finally {
+ delete window.foo;
+ }
+ })
+
+ it('bad condition issues error', function(){
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('<div hx-get="/test" hx-trigger="evt[a.b]">Not Called</div>');
+ var errorEvent = null;
+ var handler = htmx.on("htmx:eventFilter:error", function (event) {
+ errorEvent = event;
+ });
+ try {
+ var event = htmx._("makeEvent")('evt');
+ div.dispatchEvent(event);
+ should.not.equal(null, errorEvent);
+ should.not.equal(null, errorEvent.detail.source);
+ console.log(errorEvent.detail.source);
+ } finally {
+ htmx.off("htmx:eventFilter:error", handler);
+ }
+})
+
})
diff --git a/test/core/tokenizer.js b/test/core/tokenizer.js
index aad583a9..30dce2fb 100644
--- a/test/core/tokenizer.js
+++ b/test/core/tokenizer.js
@@ -35,11 +35,11 @@ describe("Core htmx tokenizer tests", function(){
{
var tokens = tokenize("[code==4||(code==5&&foo==true)]");
var conditional = htmx._("maybeGenerateConditional")(tokens);
- console.log(conditional);
var func = eval(conditional);
- console.log(func({code: 5, foo: true}));
- console.log(func({code: 4, foo: false}));
- console.log(func({code: 3, foo: false}));
+ func({code: 5, foo: true}).should.equal(true);
+ func({code: 5, foo: false}).should.equal(false);
+ func({code: 4, foo: false}).should.equal(true);
+ func({code: 3, foo: true}).should.equal(false);
});
diff --git a/www/attributes/hx-trigger.md b/www/attributes/hx-trigger.md
index 6400c930..00012956 100644
--- a/www/attributes/hx-trigger.md
+++ b/www/attributes/hx-trigger.md
@@ -8,7 +8,7 @@ title: </> htmx - hx-trigger
The `hx-trigger` attribute allows you to specify what triggers an AJAX request. A trigger
value can be one of the following:
-* An event name (e.g. "click" or "my-custom-event") followed by a set of event modifiers
+* An event name (e.g. "click" or "my-custom-event") followed by an event filter and a set of event modifiers
* A polling definition of the form `every <timing declaration>`
* A comma-separated list of such events
@@ -20,6 +20,35 @@ A standard event, such as `click` can be specified as the trigger like so:
<div hx-get="/clicked" hx-trigger="click">Click Me</div>
```
+#### Standard Event Filters
+
+Events can be filtered by enclosing a boolean javascript expression in square brackets after the event name. If
+this expression evaluates to `true` the event will be triggered, otherwise it will be ignored.
+
+```html
+<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Control Click Me</div>
+```
+
+This event will trigger if a click event is triggered with the `event.ctrlKey` property set to true.
+
+Conditions can also refer to global functions or state
+
+```html
+<div hx-get="/clicked" hx-trigger="click[checkGlobalState()]">Control Click Me</div>
+```
+
+And can also be combined using the standard javascript syntax
+
+```html
+<div hx-get="/clicked" hx-trigger="click[ctrlKey&&shfitKey]">Control-Shift Click Me</div>
+```
+
+Note that all symbols used in the expression will be resolved first against the triggering event, and then next
+against the global namespace, so `myEvent[foo]` will first look for a property named `foo` on the event, then look
+for a global symbol with the name `foo`
+
+#### Standard Event Modifiers
+
Standard events can also have modifiers that change how they behave. The modifiers are:
* `once` - the event will only trigger once (e.g. the first click)
diff --git a/www/docs.md b/www/docs.md
index 5af7dd61..8433eb55 100644
--- a/www/docs.md
+++ b/www/docs.md
@@ -13,6 +13,8 @@ title: </> htmx - high power tools for html
* [installing](#installing)
* [ajax](#ajax)
* [triggers](#triggers)
+ * [trigger filters](#trigger-filters)
+ * [trigger modifiers](#trigger-modifiers)
* [special events](#special-events)
* [polling](#polling)
* [load polling](#load_polling)
@@ -143,7 +145,23 @@ Here is a `div` that posts to `/mouse_entered` when a mouse enters it:
</div>
```
-If you want a request to only happen once, you can use the `once` modifier for the trigger:
+#### <a name="trigger-filters"></a> [Trigger Filters](#trigger-filters)
+
+You may also apply trigger filters by using square brackets after the event name, enclosing a javascript expression that
+will be evaluated. If the expression evaluates to `true` the event will trigger, otherwise it will not.
+
+Here is an example that triggers only on a Control-Click of the element
+
+```html
+<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Control Click Me</div>
+```
+
+Properties like `ctrlKey` will be resolved against the triggering event first, then the global scope.
+
+#### <a name="trigger-modifiers"></a> [Trigger Modifiers](#trigger-modifiers)
+
+A trigger can also have a few additional modifiers that change its behavior. For example, if you want a request to only
+ happen once, you can use the `once` modifier for the trigger:
```html
<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
@@ -151,7 +169,7 @@ If you want a request to only happen once, you can use the `once` modifier for t
</div>
```
-There are few other modifiers you can use for trigger:
+Other modifiers you can use for triggers are:
* `changed` - only issue a request if the value of the element has changed
* `delay:<time interval>` - wait the given amount of time (e.g. `1s`) before