Add `server-commands` extension for server-driven real-time applications
Summary
hey grugs, we have datastar at home
Description
This extension adds support for <htmx> tags which you can send from your server to do whatever you want on the client, essentially out-of-band swaps on steroids.
It's only 1.6 kb min+gzip, and it works with sse & websockets extensions out of the box.
Read everything about it in the extension's README.md
Here's are some quick examples, should be easy to pick up:
<htmx target="#chat" swap="beforeend show:bottom">
<div>New message!</div>
</htmx>
<htmx trigger="chatUpdated">
<htmx redirect="/new-page">
It uses the same internal APIs that hx-swap / HX-Reswap, hx-target / HX-Retarget & hx-select / HX-Reselect use, except you need to strip the hx- prefix.
Bad apple demo: https://bad-apple.christiantanul.com/
Current Limitations
This extension currently requires a modified htmx build until PR #3425 merges, which exposes history functions to extensions. The modified build only adds 4 function exports and maintains full compatibility.
Once the PR merges, the extension will work with standard htmx builds.
Htmx version: 2.0.6 SSE extension version: 2.2.2
Checklist
- [x] I have read the contribution guidelines
- [x] I ran the test suite locally (
npm run test) and verified that it succeeded
Deploy Preview for htmx-extensions canceled.
| Name | Link |
|---|---|
| Latest commit | db110bb0c8bc3ede3c66da9e614fb800bc976729 |
| Latest deploy log | https://app.netlify.com/projects/htmx-extensions/deploys/68ef6730fe369e000843a0a5 |
This sounds really cool. I could probably look into using it instead of my own custom hacks in Kitten (e.g., see the Streaming HTML tutorial https://kitten.small-web.org/tutorials/streaming-html/) to do similar things :)
sse-capabilities-comparison.md Here is that quick comparison i did for sse and oob and server-commands. Note I used some AI to generate this output so be mindful it may not have made perfect assumption
server-commands.js And here is a quick re-write to see how we could make it work with the current and older versions of htmx so it can be dropped in right away. The core change is to use htmx.ajax to perform all the header functions without having to re-implement them in the extension. just uses one beforeRequest event listener to transform the header only ajax requests into non ajax requests. It works by preventing the default xhr.send processing and aborting the request but before it does it calls xhr.onLoad() with headers and status and keepIndicators so that the swap none ajax request can complete all the post ajax request actions which includes all the Built in HX-Header handling code. This allowed removing of all the duplicated trigger and location handling code from the extension. Could add the Trigger code back if needed for better performance but the push/replace/location ones the performance difference will not be an issue i think. Note my editor linting rules in the project I was in reformated it a bit sorry.
demo of it working https://michaelwest22.github.io/htmx/server-commands-test-mocked.html
Also would be interesting to look at a souce feature. could be useful to allow the server responses to use client side content as the source of the swap instead of server sent data. This would allow you to move or clone content from the live page on demand from the server and allow the use of client side template tags that could store reusable content that can be queried as required.
server-commands-source.js example of how this might look
https://github.com/bigskysoftware/htmx-extensions/commit/cbda8348b75c8d3405c6500b04a87f63fb37c8e5 here is my first history things as a proper git commit diff to make it easier to compare.
and then an example of another commit on top https://github.com/bigskysoftware/htmx-extensions/commit/604fc5bb6eb31c55126885d9a836d53b52e8552b to show how a local source option could maybe be considered. It implements default clone with a move and a id based preserve option that uses htmx's built in hx-preserve with moveBefore support to allow initiating moves of client content from server sent commands. You could implement drag and drop or list reorder operations confirmed and initiated by the server in response to user input or multi user edits for example. Could also use it to store content in local template tags streamed down to the client and then accessed as an example
as a crazy example https://github.com/scriptogre/bad-apple/compare/master...MichaelWest22:bad-apple:local-source-buffer here is bad apple with a buffering stage that adds all the frames as hidden templates to the page and then locally sources them from timed server events🤣
message | <htmx target="#frames" swap="textContent settle:0" source="#frame-0"></htmx> | 16:18:27.596 |
-- | -- | -- | --
| message | <htmx target="#progress-container" swap="innerHTML settle:0"><progress value="1" max="6572"></progress></htmx> | 16:18:27.599 |
| message | <htmx target="#progress-text" swap="textContent settle:0">0.02% / 100%</htmx> | 16:18:27.602 |
| message | <htmx target="#frames" swap="textContent settle:0" source="#frame-1"></htmx> | 16:18:27.615 |
| message | <htmx target="#progress-container" swap="innerHTML settle:0"><progress value="2" max="6572"></progress></htmx> | 16:18:27.619 |
| message | <htmx target="#progress-text" swap="textContent settle:0">0.03% / 100%</htmx> | 16:18:27.624 |
| message | <htmx target="#frames" swap="textContent settle:0" source="#frame-2"></htmx> | 16:18:27.640 |
| message | <htmx target="#progress-container" swap="innerHTML settle:0"><progress value="3" max="6572"></progress></htmx> | 16:18:27.645 |
| message | <htmx target="#progress-text" swap="textContent settle:0">0.05% / 100%</htmx>
Wow!! I can't wait to get on my laptop and look at these enhancements closer. I love where this is going!
Awesome contributions @MichaelWest22 !
I think I get what you're proposing now.
The source attribute lets <htmx> tags reference existing DOM elements (using CSS selectors) instead of always sending new content. So instead of the server sending full HTML, it just tells the client:
"move element X already on the page (source) to location Y (target) using strategy (swap)"
Here's how I'm imagining the drag-and-drop example you mentioned:
<div id="todo-list">
<div id="task-1">Buy groceries</div>
<div id="task-2">Write report</div>
</div>
<div id="done-list"></div>
User drags #task-2 to #done-list, it triggers a POST, server validates it, updates the DB, responds with:
<htmx source="#task-2" target="#done-list" swap="beforeend"></htmx>
The actual DOM element gets moved, not recreated. Huge win for bandwidth efficiency.
On source-mode
I see you've added source-mode with move, clone and preserve options. I'm wondering if we could simplify it by embedding the mode directly in the source value, kind of like hx-swap does?
Instead of:
<htmx source="#element" source-mode="move" ...>
Try:
<htmx source="#element mode:move" ...>
or
<htmx source="#element with:move" ...>
One attribute instead of two, reads naturally, and mirrors how hx-swap modifiers work. What do you think?
Also, what do you think should be the default value? I see you've chosen clone in the code. I'm wondering if move might be a more common scenario of using the feature? I dunno.
On validation
I noticed you've already caught 2 important ones:
- Warn if
source+ inner content are both present (should be one or the other) - Warn if
sourceelement doesn't exist on page
One more worth adding: require target when using source. Without a target, where does the element go?
Real-time collaboration tools could massively benefit from this. And that <template> buffering trick is clever.
I curious what other patterns this feature unlocks.
So instead of the server sending full HTML, it just tells the client: "move element X already on the page (source) to location Y (target) using strategy (swap)"
I love this, and in some cases, improve SSE performance. It works best for content that gets shifted around a lot in your application, like task cards.
@MichaelWest22 What does: swap="innerHTML settle:0" mean?
@jadbox That’s actually a workaround @MichaelWest22 figured out.
The settle:0 part was needed because of a timing issue in the extension. He can probably explain the details better.