htmx-extensions icon indicating copy to clipboard operation
htmx-extensions copied to clipboard

Add `server-commands` extension for server-driven real-time applications

Open scriptogre opened this issue 2 months ago • 9 comments

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/

image

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

scriptogre avatar Oct 15 '25 09:10 scriptogre

Deploy Preview for htmx-extensions canceled.

Name Link
Latest commit db110bb0c8bc3ede3c66da9e614fb800bc976729
Latest deploy log https://app.netlify.com/projects/htmx-extensions/deploys/68ef6730fe369e000843a0a5

netlify[bot] avatar Oct 15 '25 09:10 netlify[bot]

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 :)

aral avatar Oct 16 '25 10:10 aral

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

MichaelWest22 avatar Oct 16 '25 11:10 MichaelWest22

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

MichaelWest22 avatar Oct 16 '25 13:10 MichaelWest22

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>

MichaelWest22 avatar Oct 17 '25 03:10 MichaelWest22

Wow!! I can't wait to get on my laptop and look at these enhancements closer. I love where this is going!

scriptogre avatar Oct 17 '25 04:10 scriptogre

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 source element 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.

scriptogre avatar Oct 18 '25 12:10 scriptogre

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 avatar Nov 06 '25 16:11 jadbox

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

scriptogre avatar Nov 06 '25 18:11 scriptogre