Clojure backend for for htmx. Previously known as ctmx.
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.
Getting started is easy with clojure tools and the excellent kit framework.
clojure -Ttools install com.github.seancorfield/clj-new '{:git/tag "v1.2.381"}' :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)
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
SimpleUI retains a call stack of nested components. This is used to set ids and values in the sections below.
In the above example we use a fixed id #hello
. If a component exists multiple times you may set id
automatically.
[:div.my-component {:id id} ...]
SimpleUI also provides optional path
and value
functions.
[:input {:type "text" :name (path "first-name") :value (value "first-name")}]
[:input {:type "text" :name (path "last-name") :value (value "last-name")}]
These are unique for each instance of a component and make it easy to retain state over stateless http requests.
Note: path
and value
only work when id
is set at the top level of the component. SimpleUI uses id
to record the position of the component in the component stack.
If you are using the component stack on a page, you must invoke simpleui.rt/map-indexed
instead of clojure.core/map
.
This is because the index of the array forms part of the component stack.
(def data [{:first-name "Fred" :last-name "Smith"}
{:first-name "Ocean" :last-name "Leader"}])
(defcomponent table-row [req index first-name last-name]
[:tr ...])
...
[:table
(rt/map-indexed table-row req data)]
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] ...)
You may also cast within the body of defcomponent
[:div
(if ^:boolean (value "grumpy")
"Cheer up!"
"How are you?")]
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
- ^: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
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 in initial page renders, you should call simpleui.render/walk-attrs
on your returned html body (example).
[:button.delete
{:hx-delete "trash-can"
:hx-vals {:hard-delete true}}
"Delete"]
Commands provide a shorthand to indicate custom actions.
(defcomponent ^:endpoint component [req command]
(case command
"print" (print req)
"save" (save req))
[: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.
Best to avoid, but sometimes too convenient to resist. htmx provides the hx-swap-oob
attribute for updating multiple dom elements within a single response. In SimpleUI we must only provide the additional elements when htmx is updating, not in the initial render
(defcomponent my-component [req]
(list
(when top-level?
[:div.side-element
{:id (path "path/to/side-element")
:hx-swap-oob "true"}
...])
[:div.main-element {:id id} ...]))
Be very careful to only include hx-swap-oob
elements when top-level?
is true.
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.
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 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.
htmx will execute any script tags you include.
[:script "alert('Application successful')"]
You can also mix scripts with visual content.
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 ...])
htmx does not include disabled fields when submitting requests. If you wish to retain state in this case use the following pattern.
[:input {:type "text" :name (path "input") :value (value "input") :disabled disabled?}]
(when disabled?
[:input {:type "hidden" :name (path "input") :value (value "input")}])
SimpleUI makes it possible to build dynamic forms, for details please see advanced usage.
lein auto test
Integration tests are run with puppeteer against the demo subproject.
cd demo
clj -M:run
In a separate tab
cd test-integration
npm i
node index.js
Copyright © 2023 Matthew Molloy
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.