Merge branch 'master' into dev

# Conflicts:
#	www/extensions.md
This commit is contained in:
Carson Gross 2022-10-23 09:20:14 -06:00
commit ff8e146282
9 changed files with 376 additions and 2 deletions

45
src/ext/multi-swap.js Normal file
View File

@ -0,0 +1,45 @@
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension('multi-swap', {
init: function (apiRef) {
api = apiRef;
},
isInlineSwap: function (swapStyle) {
return swapStyle.indexOf('multi:') === 0;
},
handleSwap: function (swapStyle, target, fragment, settleInfo) {
if (swapStyle.indexOf('multi:') === 0) {
var selectorToSwapStyle = {};
var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/);
elements.map(function (element) {
var split = element.split(/\s*:\s*/);
var elementSelector = split[0];
var elementSwapStyle = typeof (split[1]) !== "undefined" ? split[1] : "innerHTML";
if (elementSelector.charAt(0) !== '#') {
console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.");
return;
}
selectorToSwapStyle[elementSelector] = elementSwapStyle;
});
for (var selector in selectorToSwapStyle) {
var swapStyle = selectorToSwapStyle[selector];
var elementToSwap = fragment.querySelector(selector);
if (elementToSwap) {
api.oobSwap(swapStyle, elementToSwap, settleInfo);
} else {
console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.");
}
}
return true;
}
}
});
})();

52
test/ext/multi-swap.js Normal file
View File

@ -0,0 +1,52 @@
describe("multi-swap extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('swap only one element with default innerHTML', function () {
this.server.respondWith("GET", "/test", '<html><body><div class="dummy"><div id="a">New A</div></div></html>');
var content = make('<div>Foo <div id="a">Old A</div></div>');
var btn = make('<button hx-get="/test" hx-ext="multi-swap" hx-swap="multi:#a">Click Me!</button>');
btn.click();
this.server.respond();
should.equal(content.innerHTML, 'Foo <div id="a">New A</div>');
});
it('swap multiple elements with outerHTML, beforeend, afterend, beforebegin and delete methods', function () {
this.server.respondWith("GET", "/test",
'<html><body><div class="abc">' +
'<div id="a">New A</div> foo ' +
'<div id="b"><b>New B</b></div> bar ' +
'<div id="c">New C</div> dummy ' +
'<div id="d">New D</div> lorem ' +
'<div id="e">TO DELETE</div>' +
'</div></html>'
);
var content = make(
'<div>Foo ' +
' <div id="a">Old A</div> A ' +
' <div id="b">Old B</div> B ' +
' <div id="c">Old C</div> C ' +
' <div id="d">Old D</div> D ' +
' <div id="e">Old E</div> E ' +
'</div>'
);
var btn = make('<button hx-get="/test" hx-ext="multi-swap" hx-swap="multi:#a:outerHTML,#b:beforeend,#c:afterend,#d:beforebegin,#e:delete">Click Me!</button>');
btn.click();
this.server.respond();
should.equal(content.outerHTML,
'<div>Foo ' +
' <div id="a">New A</div> A ' +
' <div id="b">Old B<b>New B</b></div> B ' +
' <div id="c">Old C</div>New C C ' +
' New D<div id="d">Old D</div> D ' +
' E ' +
'</div>'
);
});
});

View File

@ -134,6 +134,9 @@
<script src="../src/ext/disable-element.js"></script>
<script src="ext/disable-element.js"></script>
<script src="../src/ext/multi-swap.js"></script>
<script src="ext/multi-swap.js"></script>
<!-- events last so they don't screw up other tests -->
<script src="core/events.js"></script>

View File

@ -952,6 +952,7 @@ Htmx includes some extensions that are tested against the htmx code base. Here
| [`client-side-templates`](/extensions/client-side-templates) | support for client side template processing of JSON responses
| [`path-deps`](/extensions/path-deps) | an extension for expressing path-based dependencies [similar to intercoolerjs](http://intercoolerjs.org/docs.html#dependencies)
| [`class-tools`](/extensions/class-tools) | an extension for manipulating timed addition and removal of classes on HTML elements
| [`multi-swap`](/extensions/multi-swap) | allows to swap multiple elements with different swap methods
See the [extensions page](/extensions#included) for a complete list.
@ -1386,7 +1387,7 @@ listed below:
| `htmx.config.wsReconnectDelay` | defaults to `full-jitter`
| `htmx.config.disableSelector` | defaults to `[disable-htmx], [data-disable-htmx]`, htmx will not process elements with this attribute on it or a parent
| `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 overriden using the [focus-scroll](/attributes/hx-swap/#focus-scroll) swap modifier.
| `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.
</div>

View File

@ -0,0 +1,200 @@
---
layout: layout.njk
title: When To Use Hypermedia?
---
# When Should you Use Hypermedia?
> The trade-off, though, is that a uniform interface degrades efficiency, since information is transferred in a
> standardized form rather than one which is specific to an application's needs. The REST interface is designed to be
> efficient for large-grain hypermedia data transfer, optimizing for the common case of the Web, but resulting in an
> interface that is not optimal for other forms of architectural interaction.
_-Roy Fielding, <https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5>_
We are obviously big fans of hypermedia and think that it can address, at least in part, many of the problems that the web
development world is facing today:
* Hypermedia is typically less complex than an SPA approach would be for a given problem
* Hypermedia allows your application API to be much more aggressively changed and optimized
* Hypermedia takes pressure off adopting a particular back-end technology
With [htmx](/) and the additional UX possibilities that it gives you, many modern web applications can be built
using HTML and the hypermedia paradigm.
With that being said, as with all technical choices, there are tradeoffs associated with hypermedia. In this article
outlines when we think hypermedia *is* likely to be a good fit, and when it *is not* likely to be a good fit.
## Transitional Applications & Hypermedia
Before we get into the details of when hypermedia is a good choice, we'd like to clarify that adopting hypermedia is not
an [either/or](https://en.wikipedia.org/wiki/Either/Or) decision when building a web application. Even the most Single-y
of Single Page Applications utilizes hypermedia after all, as a bootstrap mechanism, to start the application.
In his talk, [Have SPAs Ruined The Web](https://www.youtube.com/watch?v=860d8usGC0o), Rich Harris gives us the term
"Transitional" Applications, that is applications that mix both hypermedia and non-hypermedia (SPA) concepts. We
have responded to Mr. Harris' talk [in more detail here](/essays/a-response-to-rich-harris/), but suffice to say we agree
with him that a pragmatic "Transitional" approach to web development is best: use the right tool for the job.
Where we would likely disagree with Mr. Harris is just where "the line" is between what can be achieved with hypermedia
and the point at which it is better to reach for a more involved client-side library. We feel that, with htmx, hypermedia
can go much, much further than many web developers believe is possible. And that, for many applications, it can
address many or all of their UX needs.
## Hypermedia: A Good Fit If...
So, when *is* hypermedia a good choice for an application and/or feature?
### If Your UI is mostly text & images
In [The Mother Of All htmx Demos](/essays/a-real-world-react-to-htmx-port/), David Buillot of Contexte shows how replacing
react with htmx lead to a 67% reduction in the total codebase, along with numerous other eye-popping results.
As much as we would like to say that every team moving from react to htmx would experience these results, the fact is that the
Contexte web application is *extremely amenable* to the hypermedia style.
What makes Contexte so perfect for hypermedia is that it is a media-oriented web application, showing articles consisting
of text and images for reading. It has a sophisticated filtering mechanism and other niceties, but the crux of the
application is displaying and categorizing articles. This is exactly the sort of thing that hypermedia was designed to
do, and that is why htmx and hypermedia worked so well for this application.
### If Your UI is CRUD-y
Another area where hypermedia has a long track-record of success is [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)-y
web applications, in the [Ruby on Rails](https://rubyonrails.org/) style. If your main application mechanic is showing
forms and saving the forms into a database, hypermedia can work very well.
And, with htmx, it can also be [very smooth](https://htmx.org/examples/click-to-edit/), and not just constrained
to a simple [detail view](https://htmx.org/examples/edit-row/) approach.
### If Your UI is "nested", with updates mostly taking place within well-defined blocks
One area where hypermedia can start to go a little wobbly is when you have UI dependencies that span structural
areas. A good example of this, and one that often comes up when criticizing the hypermedia approach, is the issue
count number shown in the ["Issues" tab](https://github.com/bigskysoftware/htmx/issues) in Github. When you close
an issue on Github, for a long time, the tab count did not update properly, because the tab itself wasn't replaced
by the hypermedia request.
"Ah ha!", exclaims the SPA partisan, "See, even GitHub can't get this right!"
Well, yes, but there are [a few techniques for making this work](https://htmx.org/examples/update-other-content/), and,
if you watch their talk, Contexte handled this situation easily, using events.
But, let us grant that this is an area where the hypermedia approach can get into trouble. To avoid this problem, one
strategy is to colocate and next the dependent elements for a given resource within an area in the application.
Consider a contact application whose detail screen for displaying and editing a contact has:
* An area for basic contact information
* An area for emails
* An area for phone numbers
The UI could be laid out in the following manner:
![Nested Example](/img/nesting-example.png)
Where each sub-section has its own dedicated hypermedia end-points:
* `/contacts/<id>/details` for the first name/last name/ etc. info
* `/contacts/<id>/emails` for the email section
* `/contacts/<id>/phonenumbers` for the phone numbers section
The crux here is that the email count and phone count are co-located with their collections, which allows you to
[target](/attributes/hx-target) just that particular area for update when a modification is made to the respective
collections. All the data dependencies are co-located within a single area that can be updated via a single, simple
and obvious target, and that don't interfere with one another.
Each area effectively forms a sort of server-side component, independent of the other areas on the screen, and they are
all nested within a broader contact detail user interface.
#### UI Driven Hypermedia APIs
Note that our hypermedia API (that is, our end-points) in this case is _driven by the UI_, we have a particular layout
that we want to achieve and we adapt our API to that. If the UI changed, we would have no qualms with completely changing
the API to satisfy the new requirements. This is a [unique aspect](https://htmx.org/essays/hateoas/) of developing with
hypermedia, and we [discuss it in more detail here](https://htmx.org/essays/hypermedia-apis-vs-data-apis/).
Of course, there may be UI requirements that do not allow for grouping of dependent element in this manner and, if
the techniques [mentioned above](https://htmx.org/examples/update-other-content/) aren't satisfactory, then it may be
time to consider an alternative approach.
### If You need "deep links" and good first-render performance
A final area where hypermedia outperforms other options is when you need "deep links", that is, links into your
application that go beyond the landing page, or when you need excellent first-render performance.
Since hypermedia is the natural language of the web, and since browsers are very good at rendering HTML given a URL,
using this approach is hard to beat for "traditional" web features such as these.
## Hypermedia: Not A Good Fit If...
Of course, there are times when hypermedia isn't a good choice. Let's review some of them:
### If Your UI has many dynamic interdependencies
As we discussed above in the section on "nested" UIs, one area where hypermedia can have trouble is when there are
many UI dependencies spread across your UI and you can't afford to "update the whole UI". This is what Roy Fielding was
getting at in the quote at the top of this article: the web was designed for large-grain hypermedia data transfers, not
lots of small data exchanges.
Particularly difficult for hypermedia to handle is when these dependencies are dynamic, that is, they depend on information
that cannot be determined at render-time. A good example of this is something like a spreadsheet: a user can enter an
arbitrary function into a cell and introduce all sorts of dependencies on the screen. Something like Google Sheets
would be a poor fit for the hypermedia approach.
(Note, however, that for many applications, the ["editable row"](https://htmx.org/examples/edit-row/) pattern is an
acceptable alternative to more general spreadsheet-like behavior, and does play well with hypermedia, by isolating the
edits within a bounded area.)
### If you require offline functionality
The hypermedia distributed architecture leans heavily on the server side for rendering representations of resources.
When a server is down or unreachable, the architecture will obviously have trouble. It is possible to use Web Workers
to handle offline requests, and it is possible to detect when a hypermedia application is offline and show a message.
But if your application requires full functionality in an offiline environment, then the hypermedia approach is not
going to be acceptable.
### Your UI state is updated extremely frequently
Another situation where hypermedia is not going to be a good approach is if your UI state is updated frequently. A good
example is an online game that needs to capture mouse movements. Putting a hypermedia network request in-between a mouse
move and a UI update will not work well, and you would be far better off writing your own client-side state management
for the game and syncing with a server using a different technology.
Of course, your game may also have a setting page, and that setting page might be better done with hypermedia than
whatever solution you use for the core of your game. There is nothing wrong with mxing and matching approaches!
### Your team is not on board
A final reason to not choose hypermedia isn't technical, but rather sociological: currently, hypermedia simply isn't
in favor in web development. Many companies have adopted react as their standard library for building web applications.
Many developers and consultants have bet their careers on it. Many hiring managers have never heard of hypermedia, let
alone htmx, but put react on every job they post out of habit.
While this is frustrating, it is also a real phenomenon and should be borne in mind with humility. Although Contexte
was able to rewrite their application quickly and every effectively in htmx, not all teams are as small, agile and
passionate, nor are all applications such slam dunks for the approach. It may be better to adopt hypermedia around
the edges, perhaps for internal tools first, to prove its value first, before taking a broader look at it.
## Conclusion
We are often asked what sorts of applications **wouldn't** htmx be good for. Again, we prefer to think about things on a
feature-by-feature basis, and we hope that this article has given you some design points to think about when
considering hypermedia and htmx.
But, at a high level and to close with some easy-to-remember applications:
We think that applications like Twitter or GMail could be built very effectively using hypermedia due to their
focus on text-and-images, but that applications like Google Sheets or Google Maps could not due to their arbitrary
data dependencies and UI paradigms.
Of course, the vast majority of web applications are nowhere near the scale of these examples, and almost every web
application has parts where the hypermedia approach could be better: simpler, faster and cleaner.
Having hypermedia as a tool in your tool-chest will improve your ability to address engineering problems as a web
developer, even if you don't reach for it as your favorite hammer, like we do. There is a
good [theoretical basis](https://htmx.org/essays/a-real-world-react-to-htmx-port/) for the approach, as well as
[practical benefits for many applications](https://htmx.org/essays/a-real-world-react-to-htmx-port/).
Not the least being: this is how the web is supposed to work!

View File

@ -72,9 +72,11 @@ See the individual extension documentation for more details.
| [`event-header`](/extensions/event-header) | includes a JSON serialized version of the triggering event, if any
| [`include-vals`](/extensions/include-vals) | allows you to include additional values in a request
| [`json-enc`](/extensions/json-enc) | use JSON encoding in the body of requests, rather than the default `x-www-form-urlencoded`
| [`idiomoroph`](https://github.com/bigskysoftware/idiomorph) | an extension for using the idimorph morphing algorightm as a swapping mechanism
| [`loading-states`](/extensions/loading-states) | allows you to disable inputs, add and remove CSS classes to any element while a request is in-flight.
| [`method-override`](/extensions/method-override) | use the `X-HTTP-Method-Override` header for non-`GET` and `POST` requests
| [`morphdom-swap`](/extensions/morphdom-swap) | an extension for using the [morphdom](https://github.com/patrick-steele-idem/morphdom) library as the swapping mechanism in htmx.
| [`multi-swap`](/extensions/multi-swap) | allows to swap multiple elements with different swap methods
| [`path-deps`](/extensions/path-deps) | an extension for expressing path-based dependencies [similar to intercoolerjs](http://intercoolerjs.org/docs.html#dependencies)
| [`preload`](/extensions/preload) | preloads selected `href` and `hx-get` targets based on rules you control.
| [`remove-me`](/extensions/remove-me) | allows you to remove an element after a given amount of time

View File

@ -0,0 +1,71 @@
---
layout: layout.njk
title: </> htmx - multi-swap extension
---
## The `multi-swap` extension
This extension allows you to swap multiple elements marked with the `id` attribute from the HTML response. You can also choose for each element which [swap method](/docs/#swapping) should be used.
Multi-swap can help in cases where OOB ([Out of Band Swaps](/docs/#oob_swaps)) is not enough for you. OOB requires HTML tags marked with `hx-swap-oob` attributes to be at the TOP level of HTML, which significantly limited its use. With OOB is not possible to swap multiple elements arbitrarily placed and nested in the DOM tree.
It is a very powerful tool in conjunction with `hx-boost` and `preload` extension.
#### Usage
1. Set `hx-ext="multi-swap"` attribute on `<body>`, on some parent element, or on each action element that should trigger an action (typically anchors or buttons).
2. On your action elements set `hx-swap="multi:ID-SELECTORS"`, e.g. `hx-swap="multi:#id1,#id2:outerHTML,#id3:afterend"`.
3. If you're not using e.g. `hx-get` to enable HTMX behavior, set `hx-boost="true"` on your action elements, or on some parent element, so that all elements inherit the hx-boost setting.
Selectors must be separated by a comma (without surrounding spaces) and a colon with the desired swap method can optionally be placed after the selector. Default swap method is `innerHTML`.
```html
<body hx-boost="true" hx-ext="multi-swap">
<-- simple example how to swap #id1 and #id2 from /example by innerHTML (default swap method) -->
<button hx-get="/example" hx-swap="multi:#id1,#id2">Click to swap #id1 and #id2 content</button>
<-- advanced example how to swap multiple elements from /example by different swap methods -->
<a href="/example" hx-swap="multi:#id1,#id2:outerHTML,#id3:beforeend,#id4:delete">Click to swap #id1 and #id2, extend #id3 content and delete #id4 element</a>
<div id="id1">Old 1 content</div>
<div id="id2">Old 2 content</div>
<div id="id3">Old 3 content</div>
<div id="id4">Old 4 content</div>
</body>
```
**Real world example with preloading**
The use case below shows how to ensure that only the `#submenu` and `#content` elements are redrawn when the main menu items are clicked. Thanks to the combination with the preload extension, the page, including its images, is preloaded on `mouseover` event.
```html
<head>
<script src="/path/to/htmx.js"></script>
<script src="/path/to/ext/multi-swap.js"></script>
<script src="/path/to/ext/preload.js"></script>
</head>
<body hx-ext="multi-swap,preload">
<header>...</header>
<menu hx-boost="true">
<ul>
<li><a href="/page-1" hx-swap="multi:#submenu,#content" preload="mouseover" preload-images="true">Page 1</a></li>
<li><a href="/page-2" hx-swap="multi:#submenu,#content" preload="mouseover" preload-images="true">Page 2</a></li>
</ul>
<div id="submenu">... submenu contains items by selected top-level menu ...</div>
<menu>
<main id="content">...</div>
<footer>...</footer>
</body>
```
#### Notes and limitations
* Attribute `hx-swap` value **must not contain spaces**, otherwise only the part of the value up to the first space will be accepted.
* If the `delete` swap method is used, the HTML response must also contain deleted element (it can be empty div with `id` attribute).
* Only elements with an `id` selector are supported, as the function internally uses OOB internal method. So it is not possible to use `class` or any other selectors.
#### Source
<https://unpkg.com/htmx.org/dist/ext/multi-swap.js>

BIN
www/img/nesting-example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -64,7 +64,7 @@ The table below lists all other attributes available in htmx.
| [`hx-request`](/attributes/hx-request) | configures various aspects of the request
| [`hx-sse`](/extensions/server-sent-events) | has been moved to an extension. [Documentation for older versions](/attributes/hx-sse)
| [`hx-sync`](/attributes/hx-sync) | control how requests made be different elements are synchronized
| [`hx-vars`](/attributes/hx-vars) | adds values dynamically to the parameters to submit with the request (deprecated, please use `hx-vals`)
| [`hx-vars`](/attributes/hx-vars) | adds values dynamically to the parameters to submit with the request (deprecated, please use [`hx-vals`](/attributes/hx-vals))
| [`hx-ws`](/extensions/web-sockets) | has been moved to an extension. [Documentation for older versions](/attributes/hx-ws)