Merge branch 'dev' into hx-on-experiment

# Conflicts:
#	src/htmx.js
#	test/scratch/scratch.html
This commit is contained in:
Carson Gross 2023-03-31 05:12:32 -06:00
commit 2713a3ada0
9 changed files with 269 additions and 20 deletions

View File

@ -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);
})

View File

@ -11,7 +11,6 @@ describe("hx-trigger attribute", function(){
it('non-default value works', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form hx-get="/test" hx-trigger="click">Click Me!</form>');
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('<button hx-get="/test1">Submit</button>');
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('<button hx-trigger="click">Submit</button>');
div.click();
should.equal(param, "bar");
} finally {
htmx.off("htmx:trigger", handler);
}
});
})

View File

@ -14,6 +14,36 @@
}
</style>
<style>
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
</head>
@ -48,6 +78,7 @@
</script>
<h2>Server Options</h2>
<button onclick="server.respond()">Server Respond</button>
<br/>
@ -62,16 +93,17 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
</div>
<script>
this.server.respondWith("GET", "/demo", 'demo response...');
let requestCount = 0;
this.server.respondWith("GET", "/demo", function(xhr){
let randomStr = (Math.random() + 1).toString(36).substring(7);
xhr.respond(200, {}, "Request #" + requestCount++ + " : " + randomStr)
});
</script>
<button>First</button>
<input hx-get="/demo" hx-target="next output" hx-trigger="keyup changed" hx-on="htmx:beforeRequest: console.log('before', event);
console.log({foo:10,
bar: 20});
htmx:afterRequest: console.log('after', event);
"/>
<output>--</output>
<button hx-get="/demo"
hx-target="next h1"
hx-swap="innerHTML transition:true">Issue Request</button>
<h1 id="h1">--</h1>
</body>
</html>

View File

@ -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

View File

@ -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

View File

@ -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.
#### <a name="view-transitions"></a> [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).
### <a name="synchronization"></a> [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. |
</div>

View File

@ -0,0 +1,90 @@
---
layout: layout.njk
title: Architectural Sympathy
---
# Mechanical Sympathy & Architectural Sympathy
> You dont 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.

View File

@ -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
### <a name="htmx:beforeTransition"></a> 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
### <a name="htmx:configRequest"></a> 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
### <a name="htmx:trigger"></a> 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
### <a name="htmx:validation:validate"></a> 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

View File

@ -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