SkillAgentSearch skills...

Simpleui

JS Free Single Page Applications

Install / Use

/learn @whamtet/Simpleui
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

SimpleUI

Clojure backend for htmx and datastar. Previously known as ctmx.

<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) --> <!-- TOC end --> <!-- TOC --><a name="rationale"></a>

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-prompt header
  • ^:date Parses dates of the form "2026-03-03T17:10:00.000-00:00"
<!-- TOC --><a name="additional-parameters"></a>

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.

<!-- TOC --><a name="top-level"></a>

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.

<!-- TOC --><a name="responses"></a>

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.

<!-- TOC --><a name="updating-session"></a>

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

View on GitHub
GitHub Stars274
CategoryDevelopment
Updated24d ago
Forks15

Languages

Clojure

Security Score

100/100

Audited on Mar 3, 2026

No findings