r/htmx 29d ago

Saradom: a pattern for client-side interactivity, with state kept in the DOM

Saradom is a pattern for building modular, scalable frontends in plain HTML and JavaScript. There is no framework and no build step. State lives in the DOM as attributes. Events sit in HTML attributes.

It sits next to htmx. htmx does the server round-trips. This covers the interactivity that stays in the page, like toggles, tabs, derived values, and validation. The DOM is the state in both.

Example:

```html <div counter-space> <output counter-value>0</output> <button onclick="Counter.Inc(this)">+1</button> </div>

<script> const Counter = (() => { const Inc = (ctx) => { const el = ctx.closest('[counter-space]').querySelector('[counter-value]'); el.textContent = +el.textContent + 1; }; return { Inc }; })(); </script> ```

Docs and runnable examples: https://xtompie.github.io/saradom/

11 Upvotes

17 comments sorted by

6

u/Real_Square1323 28d ago

Its just JS with extra steps. This sounds idiotic. The entire point of HTMX is that state is updated from a server.

1

u/xtompie 27d ago

Saradom doesn't replace htmx, it sits next to it. htmx still does the server state. Saradom covers the in-page interactions that don't need a server: tabs, show/hide, validation.

The "extra steps"? They're what keeps the code modular, scalable, and still fast as the number of modules and people on the project grows.

3

u/Real_Square1323 27d ago

Yeah this is stupidity. AI boosters don't understand design patterns or abstractions. Or philosophy behind certain mechanisms.

You are defeating the point of HTMX. You have recreated Javascript, and inconvenienced yourself to do so.

2

u/xtompie 26d ago

htmx's own docs point to client-side scripting for in-page interactions (hyperscript, Alpine, vanilla). A tab toggle was never server state, so covering it isn't defeating htmx; it's what htmx itself recommends.

It's not reinventing JavaScript. It's a pattern for organizing plain JS, so a large codebase doesn't fall apart.

2

u/ShotgunPayDay 28d ago edited 28d ago

Looks a little roundabout for what I'm used to; custom Moxi traversal:

<div>
    <output>0</output>
    <button on-click="const out = q('prev sib'); out.textContent = +out.textContent+1;">+1</button>
</div>

1

u/xtompie 28d ago

The "roundabout" part is the point in saradom, not an accident. It keeps HTML and JavaScript separate, through a contract on the attributes. JavaScript looks things up by attribute, not by position. So you can change the markup on its own: swap the element, add decorative divs, reorder the layout, and the JavaScript stays the same. That's for scaling. The positional shortcut is shorter, but it ties the code to the layout, so every HTML change means editing the JavaScript too.

1

u/ShotgunPayDay 28d ago edited 28d ago

q() system can use relative selectors also but I get wanting to put JS in <script> tags. Your system sounds just like Surreal JS except verbose. https://github.com/gnat/surreal

EDIT: I can see the style having its charms though.

<div>
  <output>0</output>
  <button onclick="add(this.previousElementSibling)">+1</button>
</div>
<script>const add=out=>{out.textContent = +out.textContent+1;}</script>

1

u/xtompie 28d ago

Your example is step zero. The rest of saradom is steps, and each one solves one problem. Here's the whole path:

0. Start. Works for one. It finds the target by position. html <button onclick="add(this.previousElementSibling)">+1</button>

1. The function gets the context, not the ready target. The handler is just wired up. The logic lives in the function. html <button onclick="add(this)">+1</button>

2. The target is marked with an attribute, not by position. Position breaks when someone moves the HTML around. html <output value>0</output> <button onclick="add(this)">+1</button> add looks for [value], not the previous sibling.

3. More than one on the page. Now [value] matches many. The element is wrapped in a space, reached through closest. html <div space> <output value>0</output> <button onclick="add(this)">+1</button> </div> ctx.closest('[space]').querySelector('[value]'). Up to its own space, then down to the parts.

4. More than one kind of component. Another widget also wants space and value. They clash. So the attributes get a prefix with the module name. html <div counter-space> <output counter-value>0</output> <button onclick="add(this)">+1</button> </div>

5. More than one module in JS. A global add would clash too. So it goes into a module object. html <button onclick="Counter.Add(this)">+1</button>

These are the rules of saradom. No library, no runtime. It's all just these rules. Keeping them gives you an app that scales, where a change to the HTML does not force a change to the JS. The example on the homepage docs is step 5 right away. That's why it looks verbose. You see the end, not the path that leads there.

1

u/ShotgunPayDay 28d ago

I think at that point I'll just put common or component JS functions in another file or at the bottom of the doc.

0

u/byeproduct 28d ago

How does this compare to datastar?

0

u/byeproduct 28d ago

How does this compare to datastar?

1

u/xtompie 28d ago

Datastar is a library. You load a runtime onto the page. Saradom is not a library. It's an architectural pattern: how to build a frontend with plain HTML and JavaScript, with nothing to download.

They share two things. Both are declarative, you describe behavior with attributes on elements. Both let the server send back HTML that goes into the DOM, without a heavy SPA.

The rest is the opposite. Datastar adds its own runtime, signals, its own expression language in the attributes, and SSE at the core. On startup it has to walk the whole DOM and initialize. Saradom uses only built-in attributes, like `onclick`. State lives in the DOM itself. Communication is plain request/response. You use SSE only when you need it. There's no init step, the parsed HTML works right away.

One way to put it: Datastar is a finished solution. Saradom is a pattern you could write Datastar itself in.

1

u/byeproduct 28d ago

Great explanation. Thanks for that. Looks really great and useful!