Simpleui
JS Free Single Page Applications
Install / Use
/learn @whamtet/SimpleuiREADME
SimpleUI
Clojure backend for htmx and datastar. Previously known as ctmx.
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->- Rationale
- Getting started
- Usage
- Datastar!
- Advanced Usage
- Pros and Cons of SimpleUI
- Testing
- Contributing
- License
Rationale
htmx enables web developers to create powerful webapps without writing any Javascript. Whenever hx-* attributes are included in html the library will update the dom in response to user events. The architecture is simpler and pages load more quickly than in Javascript-oriented webapps.
SimpleUI is a backend accompaniment which makes htmx even easier to use. It works in conjunction with hiccup for rendering and reitit for routing.
<!-- TOC --><a name="getting-started"></a>Getting started
Add the following dependency to your deps.edn file:
io.simpleui/simpleui {:mvn/version "1.6.0"}
Or to your Leiningen project.clj file:
[io.simpleui/simpleui "1.6.0"]
Getting started is easy with clojure tools and the excellent kit framework.
clojure -Ttools install com.github.seancorfield/clj-new '{:git/tag "v1.2.404"}' :as new
clojure -Tnew create :template io.github.kit-clj :name yourname/guestbook
cd guestbook
make repl
(kit/sync-modules)
(kit/install-module :kit/simpleui)
Quit the process, make repl then
(go)
Visit localhost:3000. To reload changes
(reset)
<!-- TOC --><a name="usage"></a>
Usage
First require the library
(require '[simpleui.core :refer :all])
The core of SimpleUI is the defcomponent macro.
(defcomponent ^:endpoint hello [req my-name]
[:div#hello "Hello " my-name])
This defines an ordinary function which also expands to an endpoint /hello.
To use our endpoint we call make-routes
;; make-routes generates a reitit handler with the root page at /demo
;; and all subcomponents on their own routes
(make-routes
"/demo"
(fn [req]
(page ;; page renders the rest of the page, htmx script etc
[:div
[:label "What is your name?"]
[:input {:name "my-name" :hx-patch "hello" :hx-target "#hello"}]
(hello req "")])))

Here the only active element is the text input. On the input's default action (blur) it will request to /hello and replace #hello with the server response. We are using hello both as a function and an endpoint. When called as an endpoint arguments are set based on the http parameter my-name.
The first argument to defcomponent is always the req object
<!-- TOC --><a name="authentication-iam"></a>Authentication, IAM
You may check a user's permissions inside the component, however for page level checks remember that make-routes is just generating reitit vectors
(make-routes
"/demo"
(fn [req] ...))
;; returns
;; ["/demo"
;; ["/my-component1" my-component1]
;; ["/my-component2" my-component2]
;; ...]
You can attach page level checks using standard Reitit techniques.
<!-- TOC --><a name="parameter-casting"></a>Parameter Casting
htmx submits all parameters as strings. It can be convenient to cast parameters to the required type
(defcomponent my-component [req ^:long int-argument ^:boolean boolean-argument] ...)
Casts available include the following
- ^:long Casts to long
- ^:long-option Casts to long (ignores empty string)
- ^:double Casts to double
- ^:double-option Casts to double (ignores empty string)
- ^:longs Casts to array of longs
- ^:doubles Casts to array of doubles
- ^:array Puts into an array
- ^:set Puts into a set
- ^:boolean True when
(contains? #{"true" "on"} argument). Useful with checkboxes. - ^:boolean-true True when
(not= argument "false") - ^:edn Reads string into edn
- ^:keyword Casts to keyword
- ^:nullable Ensures the strings "", "nil" and "null" are parsed as nil
- ^:trim Trims string and sets it to nil when empty
- ^:json Parses json
- ^:prompt Takes value from
hx-promptheader - ^:date Parses dates of the form "2026-03-03T17:10:00.000-00:00"
Additional Parameters
In most cases htmx will supply all required parameters. If you need to include extra ones, set the hx-vals attribute. To serialize the map as json on initial render walk the body with simpleui.render/walk-attrs (example).
[:button.delete
{:hx-delete "trash-can"
:hx-vals {:hard-delete true}}
"Delete"]
<!-- TOC --><a name="commands"></a>
Commands
Commands provide a shorthand to indicate custom actions.
(defcomponent ^:endpoint component [req command]
(case command
"print" (print req)
"save" (save req)
nil)
[:div
[:button {:hx-post "component:print"} "Print"]
[:button {:hx-post "component:save"} "Save"]])
command will be bound to the value after the colon in any endpoints.
top-level?
SimpleUI sets top-level? true when a component is being invoked as an endpoint.
(defcomponent ^:endpoint my-component [req]
(if top-level?
[:div "This is an update"]
[:div "This is the original render"]))
<!-- TOC --><a name="updating-multiple-components"></a>
Updating multiple components
When you return multiple components as a list, SimpleUI will set hx-swap-oob on all but the last. Those elements will be swapped in by id at various points on the page.
(defcomponent my-component [req]
(list
;; update these as well
[:div#title ...]
[:div#sidebar ...]
;; main element
[:div.main-element {:id id} ...]))
Be careful to only include hx-swap-oob elements when top-level? is true.
Responses
By default SimpleUI expects components to return hiccup vectors which are rendered into html.
nil returns http 204 - No Content and htmx will not update the dom.
:refresh returns 200 - OK with the HX-Refresh header set to true to refresh the page.
{:hx-redirect "/ok"} converts to HX-Redirect with value "/ok".
You may also return an explicit ring map if you wish. A common use case is to refresh the page after an operation is complete
(defcomponent ^:endpoint my-component [req]
(case (:request-method req)
:post
(do
(save-to-db ...)
simpleui.response/hx-refresh)
:get ...))
simpleui.response/hx-refresh sets the "HX-Refresh" header to "true" and htmx will refresh the page.
Updating Session
When a component returns a response map without a body key SimpleUI assumes it is a session update and wraps the response in 204 - No Content.
(defcomponent ^:endpoint my-component [req shopping-item]
(update session :cart conj shopping-item))
The response won't update anything on the page, but the session will be updated.
<!-- TOC --><a name="script-responses"></a>Script Responses
htmx will execute any script tags you include.
[:script "alert('Application successful')"]
You can also mix scripts with visual content. Once you're inside Javascript you can invoke SimpleUI with the HTMX commands ajax and trigger.
<!-- TOC --><a name="unsafe-html"></a>Unsafe HTML
The default hiccup rendering mode blocks HTML strings from being inserted into the DOM. If you need this disable render-safe
(simpleui.config/set-render-safe false)
<!-- TOC --><a name="hanging-components"></a>
Hanging Components
If you don't include components in an initial render, reference them as symbols so they are still available as endpoints.
(defcomponent ^:endpoint next-month [req] [:p "next-month"])
(defcomponent ^:endpoint previous-month [req] [:p "previous-month"])
(defcomponent ^:endpoint calendar [req]
next-month
previous-month
[:div#calendar ...])
<!-- TOC --><a name="si-set-si-clear"></a>
si-set, si-clear
SimpleUI contains complex state in forms. On wizards and multistep forms some elements may disappear while we still wish to retain the state. To handle this situation create a 'stack' of hidden elements on initial page render
[:input#first-name {:type "hidden"}]
[:input#second-name {:type "hidden"}]
...
When you proceed from one form to the next you may push onto the stack
[:button {:hx-post "next-step"
:si-set [:first-name :second-name]
:si-set-class "my-stack"}]
si-set will oob-swap `fir
Related Skills
node-connect
338.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
338.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.4kCommit, push, and open a PR
