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('
Not Called
'); 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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('
Not Called
'); + 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 ` * 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:
Click Me
``` +#### 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 +
Control Click Me
+``` + +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 +
Control Click Me
+``` + +And can also be combined using the standard javascript syntax + +```html +
Control-Shift Click Me
+``` + +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: ``` -If you want a request to only happen once, you can use the `once` modifier for the trigger: +#### [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 +
Control Click Me
+``` + +Properties like `ctrlKey` will be resolved against the triggering event first, then the global scope. + +#### [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
@@ -151,7 +169,7 @@ If you want a request to only happen once, you can use the `once` modifier for t
``` -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: