diff --git a/src/htmx.js b/src/htmx.js index 9719180d..a8bff632 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -70,6 +70,7 @@ return (function () { scrollBehavior: 'smooth', defaultFocusScroll: false, getCacheBusterParam: false, + globalViewTransitions: false, }, parseInterval:parseInterval, _:internalEval, @@ -471,7 +472,10 @@ return (function () { function removeElement(elt, delay) { elt = resolveTarget(elt); if (delay) { - setTimeout(function(){removeElement(elt);}, delay) + setTimeout(function(){ + removeElement(elt); + elt = null; + }, delay); } else { elt.parentElement.removeChild(elt); } @@ -480,7 +484,10 @@ return (function () { function addClassToElement(elt, clazz, delay) { elt = resolveTarget(elt); if (delay) { - setTimeout(function(){addClassToElement(elt, clazz);}, delay) + setTimeout(function(){ + addClassToElement(elt, clazz); + elt = null; + }, delay); } else { elt.classList && elt.classList.add(clazz); } @@ -489,7 +496,10 @@ return (function () { function removeClassFromElement(elt, clazz, delay) { elt = resolveTarget(elt); if (delay) { - setTimeout(function(){removeClassFromElement(elt, clazz);}, delay) + setTimeout(function(){ + removeClassFromElement(elt, clazz); + elt = null; + }, delay); } else { if (elt.classList) { elt.classList.remove(clazz); @@ -1401,6 +1411,7 @@ return (function () { } else if (triggerSpec.delay) { elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay); } else { + triggerEvent(elt, 'htmx:trigger') handler(elt, evt); } } @@ -1693,6 +1704,13 @@ return (function () { }); } }); + if (!explicitAction && hasAttribute(elt, 'hx-trigger')) { + explicitAction = true + triggerSpecs.forEach(function(triggerSpec) { + // For "naked" triggers, don't do anything at all + addTriggerHandler(elt, triggerSpec, nodeData, function () { }) + }) + } return explicitAction; } @@ -1777,7 +1795,7 @@ return (function () { if (elt.querySelectorAll) { var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : ""; var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-sse], [data-hx-sse], [hx-ws]," + - " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-on], [data-hx-on]"); + " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]"); return results; } else { return []; @@ -2454,6 +2472,9 @@ return (function () { if (modifier.indexOf("settle:") === 0) { swapSpec["settleDelay"] = parseInterval(modifier.substr(7)); } + if (modifier.indexOf("transition:") === 0) { + swapSpec["transition"] = modifier.substr(11) === "true"; + } if (modifier.indexOf("scroll:") === 0) { var scrollSpec = modifier.substr(7); var splitSpec = scrollSpec.split(":"); @@ -3185,9 +3206,13 @@ return (function () { var swapSpec = getSwapSpecification(elt, swapOverride); target.classList.add(htmx.config.swappingClass); + + // optional transition API promise callbacks + var settleResolve = null; + var settleReject = null; + var doSwap = function () { try { - var activeElt = document.activeElement; var selectionInfo = {}; try { @@ -3286,6 +3311,7 @@ return (function () { } handleTrigger(xhr, "HX-Trigger-After-Settle", finalElt); } + maybeCall(settleResolve); } if (swapSpec.settleDelay > 0) { @@ -3295,10 +3321,34 @@ return (function () { } } catch (e) { triggerErrorEvent(elt, 'htmx:swapError', responseInfo); + maybeCall(settleReject); throw e; } }; + var shouldTransition = htmx.config.globalViewTransitions + if(swapSpec.hasOwnProperty('transition')){ + shouldTransition = swapSpec.transition; + } + + if(shouldTransition && + triggerEvent(elt, 'htmx:beforeTransition', responseInfo) && + typeof Promise !== "undefined" && document.startViewTransition){ + var settlePromise = new Promise(function (_resolve, _reject) { + settleResolve = _resolve; + settleReject = _reject; + }); + // wrap the original doSwap() in a call to startViewTransition() + var innerDoSwap = doSwap; + doSwap = function() { + document.startViewTransition(function () { + innerDoSwap(); + return settlePromise; + }); + } + } + + if (swapSpec.swapDelay > 0) { setTimeout(doSwap, swapSpec.swapDelay) } else { @@ -3460,6 +3510,7 @@ return (function () { }; setTimeout(function () { triggerEvent(body, 'htmx:load', {}); // give ready handlers a chance to load up before firing this event + body = null; // kill reference for gc }, 0); }) diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js index a181b979..72086a17 100644 --- a/test/attributes/hx-trigger.js +++ b/test/attributes/hx-trigger.js @@ -11,7 +11,6 @@ describe("hx-trigger attribute", function(){ it('non-default value works', function() { this.server.respondWith("GET", "/test", "Clicked!"); - var form = make('
Click Me!
'); form.click(); form.innerHTML.should.equal("Click Me!"); @@ -756,5 +755,33 @@ describe("hx-trigger attribute", function(){ div.innerHTML.should.equal("test 1"); }); + it("fires the htmx:trigger event when an AJAX attribute is specified", function () { + var param = "foo" + var handler = htmx.on("htmx:trigger", function (evt) { + param = "bar" + }); + try { + this.server.respondWith("GET", "/test1", "test 1"); + var div = make(''); + div.click(); + should.equal(param, "bar"); + } finally { + htmx.off("htmx:trigger", handler); + } + }); + + it("fires the htmx:trigger event when no AJAX attribute is specified", function () { + var param = "foo" + var handler = htmx.on("htmx:trigger", function (evt) { + param = "bar" + }); + try { + var div = make(''); + div.click(); + should.equal(param, "bar"); + } finally { + htmx.off("htmx:trigger", handler); + } + }); }) diff --git a/test/scratch/scratch.html b/test/scratch/scratch.html index 0bc248f8..1229bd64 100644 --- a/test/scratch/scratch.html +++ b/test/scratch/scratch.html @@ -14,6 +14,36 @@ } + + + + @@ -48,6 +78,7 @@ +

Server Options


@@ -62,16 +93,17 @@ Autorespond: - - --- + +

--

diff --git a/www/attributes/hx-swap.md b/www/attributes/hx-swap.md index 2123b63f..397730f8 100644 --- a/www/attributes/hx-swap.md +++ b/www/attributes/hx-swap.md @@ -35,6 +35,12 @@ The `div` will issue a request to `/example` and append the returned content aft The `hx-swap` attributes supports modifiers for changing the behavior of the swap. They are outlined below. +#### Transition: `transition` + +If you want to use the new (View Transitions)[https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API] API +when a swap occurs, you can use the `transition:true` option for your swap. You can also enable this feature globally by +setting the `htmx.config.globalViewTransitions` config setting to `true`. + #### Timing: `swap` & `settle` You can modify the amount of time that htmx will wait after receiving a response to swap the content diff --git a/www/attributes/hx-trigger.md b/www/attributes/hx-trigger.md index c50f3886..ce3dadc9 100644 --- a/www/attributes/hx-trigger.md +++ b/www/attributes/hx-trigger.md @@ -64,7 +64,7 @@ is seen again before the delay completes it is ignored, the element will trigger * `closest ` - finds the closest parent matching the given css selector * `find ` - finds the closest child matching the given css selector * `target:` - 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, for example, listening on the body, +triggers from elements that might not be in the DOM at the point of initialization, by, for example, listening on the body, but with a target filter for a child element * `consume` - if this option is included the event will not trigger any other htmx requests on parents (or on elements listening on parents) @@ -78,7 +78,7 @@ Here is an example of a search box that searches on `keyup`, but only if the sea and the user hasn't typed anything new for 1 second: ```html - ``` @@ -95,10 +95,10 @@ There are some additional non-standard events that htmx supports: * `root:` - a CSS selector of the root element for intersection * `threshold:` - a floating point number between 0.0 and 1.0, indicating what amount of intersection to fire the event on -### Triggering via the `HX-Trigger` header +### Triggering via the `HX-Trigger` header -If you're trying to fire an event from HX-Trigger response header, you will likely want to -use the `from:body` modifier. E.g. if you send a header like this HX-Trigger: my-custom-event +If you're trying to fire an event from HX-Trigger response header, you will likely want to +use the `from:body` modifier. E.g. if you send a header like this HX-Trigger: my-custom-event with a response, an element would likely need to look like this: ```html @@ -108,7 +108,7 @@ with a response, an element would likely need to look like this: ``` in order to fire. - + This is because the header will likely trigger the event in a different DOM hierarchy than the element that you wish to be triggered. For a similar reason, you will often listen for hot keys from the body. @@ -148,3 +148,4 @@ The AJAX request can be triggered via Javascript [`htmx.trigger()`](/api#trigger ### Notes * `hx-trigger` is not inherited +* `hx-trigger` can be used without an AJAX request, in which case it will only fire the `htmx:trigger` event diff --git a/www/docs.md b/www/docs.md index 0c898b08..05264fa0 100644 --- a/www/docs.md +++ b/www/docs.md @@ -417,6 +417,22 @@ The following extensions are available for morph-style swaps: * [Idiomorph](https://github.com/bigskysoftware/idiomorph#htmx) - A newer morphing algorithm developed by us, the creators of htmx. Idiomorph will be available out of the box in htmx 2.0. +#### [View Transitions](#view-transitions) + +The new, experimental [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +gives developers a way to create an animated transition between different DOM states. It is still in active development +and is not available in all browsers, but htmx provides a way to work with this new API that falls back to the non-transition +mechanism if the API is not available in a given browser. + +You can experiment with this new API using the following approaches: + +* Set the `htmx.config.globalViewTransitions` config variable to `true` to use transitions for all swaps +* Use the `transition:true` option in the `hx-swap` attribute +* If an element swap is going to be transitioned due to either of the above configurations, you may catch the + `htmx:beforeTransition` event and call `preventDefault()` on it to cancel the transition. + +View Transitions can be configured using CSS, as outlined in [the Chrome documentation for the feature](https://developer.chrome.com/docs/web-platform/view-transitions/#simple-customization). + ### [Synchronization](#synchronization) Often you want to coordinate the requests between two elements. For example, you may want a request from one element @@ -1402,6 +1418,7 @@ listed below: | `htmx.config.timeout` | defaults to 0 in milliseconds | | `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](/attributes/hx-swap/#focus-scroll) swap modifier. | | `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser | +| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. | diff --git a/www/essays/architectural-sympathy.md b/www/essays/architectural-sympathy.md new file mode 100644 index 00000000..cb106883 --- /dev/null +++ b/www/essays/architectural-sympathy.md @@ -0,0 +1,90 @@ +--- +layout: layout.njk +title: Architectural Sympathy +--- + +# Mechanical Sympathy & Architectural Sympathy + +> You don’t have to be an engineer to be be a racing driver, but you do have to have Mechanical Sympathy. + +_-Jackie Stewart, racing driver_ + +The term "mechanical sympathy" was originally coined by Jackie Steward to capture a characteristic + of race car drivers, who needed a deep and intuitive understanding of how a race car worked in order +to get the best possible performance out of the vehicle. + +This term was applied to software development by Martin Thompson when discussing his [LMAX](https://martinfowler.com/articles/lmax.html) +architecture, which utilized a low level and intuitive understanding of how his cloud system functioned +in order to maximize the performance of it. Thompson maintained [a blog](https://mechanical-sympathy.blogspot.com/) +on the topic for many years, and it is well worth going back and reading the posts there. + +## Architectural Sympathy + +In this brief essay I want to propose another concept and design principle, that of _Architectural Sympathy_: + +> Architectural Sympathy is the characteristic of one piece of software adopting and conforming to the architectural +> design of another piece of software + +This is a design principle that I have kept in mind when designing [htmx](https://htmx.org) and +[hyperscript](https://hyperscript.org) and I wanted to write it down for reference and so others can think about, +criticize and improve it. + +### htmx's Architectural Sympathy for The Web + +htmx is architecturally sympathetic to The Web because it adopts the underlying [REST-ful](/essays/hateoas) architecture +of The Web: exchanging _hypermedia_ in a REST-ful manner with a hypermedia server. As much as is practical, htmx takes +design cues from the existing Web infrastructure: + +* It mimics the core hypermedia-exchange mechanic of links and forms +* It uses CSS selectors for targeting +* It uses standard URL paths for designating end points +* It uses the standard API language for specifying swap types +* Etc. + +htmx attempts to _fold in_ to the existing conceptual architecture of The Web, rather than replace it. + +This is in contrast with the [SPA](https://developer.mozilla.org/en-US/docs/Glossary/SPA) approach to building web +applications. Most SPA frameworks have little architectural sympathy with the original web model. Rather, they largely +_replace_ the original, REST-ful, hypermedia-oriented architecture of the web in favor of a more thick-client like +architecture, exchanging information over an +[RPC-like fixed-data format](/essays/how-did-rest-come-to-mean-the-opposite-of-rest/) network architecture. + +### Advantages Of The Architecturally Sympathetic Approach + +If a new piece of software maintains architectural sympathy with an original piece of software, the following advantages +are obtained: + +* A developer who is familiar with the original piece of software does not need to learn a whole new conceptual approach + when using the new piece of software. +* The design constraints of the original piece of software offer a framework within which to evaluate features for the + new piece of software. This makes it easier to [say "no"](https://grugbrain.dev/#grug-on-saying-no) as you develop the + new software. ("The enemy of art is the absence of limitations." --[Orson Welles](https://quoteinvestigator.com/2014/05/24/art-limit/)) +* Experience gained from working with the original piece of software can directly inform the design and implementation of + the new software +* There will likely be a subjective feeling of "fit" between the new and original software for users of the new software + +### Disadvantages Of The Architecturally Sympathetic Approach + +Of course, as with any design principle, there are trade-offs when using Architectural Sympathy: + +* The shortcomings of the original piece of software are likely to be found in some way in the new software +* The design constraints impressed on the new software by the older software may be so oppressive as to limit progress + and functionality in the new software +* It may be difficult for developers to "see the point" of the new software, if it feels too close to the original software +* By maintaining architectural sympathy with the older, original software, the new software risks appearing old itself, + a danger in the software business that has often favored new and exciting approaches to problems. +* You may not be able to layer as many new concepts as some users might like on top of the original software + +## Craftsmanship & Architectural Sympathy + +A non-software example of architectural sympathy that I like to point to are medieval cathedrals: these cathedrals were +often built, rebuilt and improved over centuries by many different builders and architects (such as they were). And yet +they were able, over those centuries, to maintain a high level of architectural sympathy with the earlier workers. + +Rather than focusing on radically new approaches to building, workers focused on maintaining a coherent whole and, within +that framework, on the craftsmanship of their individual contributions. Yes, there were flourishes and changes along the +way, but these typically did not sacrifice the conceptual coherence of the whole for the sake of innovation. + +Adopting an architecturally sympathetic mindset in software development often means sacrificing how you would like to +do things in favor of how an original piece of software did things. While this constraint can chafe at times, it can +also produce well crafted software that is harmonious and that dovetails well with existing software. \ No newline at end of file diff --git a/www/events.md b/www/events.md index db58e940..2a4c3ae9 100644 --- a/www/events.md +++ b/www/events.md @@ -134,6 +134,20 @@ the documentation on [configuring swapping](/docs#modifying_swapping_behavior_wi * `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes) * `detail.target` - the target of the swap +### Event - [`htmx:beforeTransition`](#htmx:beforeTransition) + +This event is triggered before a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +wrapped swap occurs. If the event is cancelled, the View Transition will not occur and the normal swapping logic will +happen instead. + +##### Details + +* `detail.elt` - the element that dispatched the request +* `detail.xhr` - the `XMLHttpRequest` +* `detail.requestConfig` - the configuration of the AJAX request +* `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes) +* `detail.target` - the target of the swap + ### Event - [`htmx:configRequest`](#htmx:configRequest) This event is triggered after htmx has collected parameters for inclusion in the request. It can be @@ -399,6 +413,16 @@ Timeout time can be set using `htmx.config.timeout` or per element using [`hx-re * `detail.target` - the target of the request * `detail.requestConfig` - the configuration of the AJAX request +### Event - [`htmx:trigger`](#htmx:trigger) + +This event is triggered whenever an AJAX request would be, even if no AJAX request is specified. It +is primarily intended to allow `hx-trigger` to execute client-side scripts; AJAX requests have more +granular events available, like [`htmx:beforeRequest`](#htmx:beforeRequest) or [`htmx:afterSend`](#htmx:afterSend). + +##### Details + +* `detail.elt` - the element that triggered the request + ### Event - [htmx:validation:validate](#htmx:validation:validate) This event is triggered before an element is validated. It can be used with the `elt.setCustomValidity()` method diff --git a/www/reference.md b/www/reference.md index 9821353e..789ae995 100644 --- a/www/reference.md +++ b/www/reference.md @@ -163,6 +163,7 @@ The table below lists all other attributes available in htmx. | [`htmx:swapError`](/events#htmx:swapError) | triggered when an error occurs during the swap phase | [`htmx:targetError`](/events#htmx:targetError) | triggered when an invalid target is specified | [`htmx:timeout`](/events#htmx:timeout) | triggered when a request timeout occurs +| [`htmx:trigger`](/events#htmx:trigger) | triggered by the event specified in `hx-trigger` | [`htmx:validation:validate`](/events#htmx:validation:validate) | triggered before an element is validated | [`htmx:validation:failed`](/events#htmx:validation:failed) | triggered when an element fails validation | [`htmx:validation:halted`](/events#htmx:validation:halted) | triggered when a request is halted due to validation errors