diff options
author | carson <carson@leaddyno.com> | 2020-10-17 13:21:15 -0600 |
---|---|---|
committer | carson <carson@leaddyno.com> | 2020-10-17 13:21:15 -0600 |
commit | 165586b77752a2873cec67bc9561a471ca70e1f6 (patch) | |
tree | 6a05379705ce6e47bb7331dfcf56245f93828383 | |
parent | 6c8c124c420b90b4c0527557af3a9466d74a0b53 (diff) | |
download | htmx-165586b77752a2873cec67bc9561a471ca70e1f6.tar.gz htmx-165586b77752a2873cec67bc9561a471ca70e1f6.zip |
docs and tests for trigger filters
-rw-r--r-- | src/htmx.js | 35 | ||||
-rw-r--r-- | test/attributes/hx-push-url.js | 2 | ||||
-rw-r--r-- | test/attributes/hx-trigger.js | 127 | ||||
-rw-r--r-- | test/core/tokenizer.js | 8 | ||||
-rw-r--r-- | www/attributes/hx-trigger.md | 31 | ||||
-rw-r--r-- | www/docs.md | 22 |
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 |