mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
Merge branch 'dev' into hx-on-experiment
# Conflicts: # src/htmx.js # test/scratch/scratch.html
This commit is contained in:
commit
2713a3ada0
61
src/htmx.js
61
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);
|
||||
})
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
17
www/docs.md
17
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.
|
||||
|
||||
#### <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>
|
||||
|
||||
|
90
www/essays/architectural-sympathy.md
Normal file
90
www/essays/architectural-sympathy.md
Normal file
@ -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.
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user