HTMx: The Missing Link Between Traditional MVC and Modern Interactivity
If you've been building web apps for a while, you probably remember when everything was simple: a form posts to an endpoint, the server processes it, returns HTML, the page refreshes. Then SPA frameworks arrived and everything got complicated.
HTMx brings us back to simplicity while still allowing dynamic, interactive applications.
What is HTMx?
HTMx is a JavaScript library that extends HTML with modern capabilities. Instead of learning a new syntax, you just use HTML attributes:
<button hx-get="/api/users" hx-target="#user-list">
Load Users
</button>
That's it. No JavaScript, no frameworks, no build steps.
Why HTMx for Soli?
Soli already has LiveView for real-time interactivity, but not everyone needs WebSocket connections. HTMx is perfect when you want:
- Simple HTTP request/response patterns
- No WebSocket overhead
- Progressive enhancement of plain HTML
- Small bundle size (~14KB vs React's 100KB+)
Getting Started
First, add HTMx to your layout:
# www/app/views/layouts/application.html.slv
<script src="<%= public_path("js/htmx.min.js") %>"></script>
Download HTMx from htmx.org and place it in public/js/.
HTMx Helper Functions
To make HTMx even easier to use in Soli, let's create some helper functions:
# stdlib/htmx.sl
fn hx_get(url)
'hx-get="' + url + '"'
end
fn hx_post(url)
'hx-post="' + url + '"'
end
fn hx_target(selector)
'hx-target="' + selector + '"'
end
fn hx_swap(method)
'hx-swap="' + method + '"'
end
fn hx_trigger(event)
'hx-trigger="' + event + '"'
end
fn hx_push_url(enabled)
'hx-push-url="' + (enabled ? "true" : "false") + '"'
end
Now using HTMx in your templates is clean and readable:
<button <%= hx_get("/users") %>>Load Users</button>
<div id="user-list"></div>
Example: Todo List
Let's build a simple todo list with HTMx:
# app/controllers/todos_controller.sl
fn index(req)
let todos = Todo.all
render("todos/index", {"todos": todos})
end
fn create(req)
let params = req["params"]
let todo = Todo.create({"title": params["title"], "done": false})
render("todos/_todo", {"todo": todo})
end
fn toggle(req)
let id = req["params"]["id"]
let todo = Todo.find(id)
todo["done"] = !todo["done"]
todo.save
render("todos/_todo", {"todo": todo})
end
# app/views/todos/index.html.slv
<h1>My Todos</h1>
<form <%= hx_post("/todos", target: "#todos") %>>
<input type="text" name="title" placeholder="New todo...">
<button type="submit">Add</button>
</form>
<div id="todos">
<%= render("_todos", {"todos": todos}) %>
</div>
# app/views/todos/_todo.html.slv
<div class="todo <%= todo["done"] ? "completed" : "" %>">
<input
type="checkbox"
<%= todo["done"] ? "checked" : "" %>
<%= hx_patch("/todos/" + todo["id"], target: "#todo-" + todo["id"]) %>
>
<span><%= todo["title"] %></span>
</div>
The server returns only the partial HTML fragment. HTMx swaps it into the page - no full page refresh, no client-side rendering.
Why This Matters
-
Server-side rendering - Your HTML is generated on the server, where you have all your database connections, business logic, and security
-
No JS knowledge required - You write templates, not JavaScript components
-
Progressive enhancement - Works even if JS is disabled (mostly)
-
Small footprint - 14KB total, no dependencies
-
SEO friendly - Full HTML is served on initial load
When to Choose HTMx vs LiveView
| Use HTMx when... | Use LiveView when... |
|---|---|
| Simple request/response | Real-time updates needed |
| Standard CRUD operations | Frequent state changes |
| Forms and page navigations | Collaborative features |
| You want simplicity | You need WebSocket performance |
Conclusion
HTMx brings the simplicity of traditional server-side rendering to modern web development. Combined with Soli's clean syntax, you get:
- Expressive templates with HTMx helpers
- Server-rendered HTML with partials
- Progressive enhancement by default
- No JavaScript framework complexity
It's not about replacing JavaScript - it's about using the right tool for the right job. Sometimes that's React. Sometimes it's a 14KB library that works with HTML you already know.