ctmx examples docs source

ctmx is an app development tool for fast product development and even faster page load times. It uses htmx on the frontend.

Get started with the luminus template lein new luminus my-project +ctmx and update project.clj to the latest version of ctmx.

Basic example

(defcomponent ^:endpoint hello [req my-name]
  [:div#hello "Hello " my-name])

(make-routes
  "/demo"
  (fn [req]
    ;; page renders the hiccup and returns a ring response
    (page
      [:div
       [:label "What is your name?"]
       [:input {:name "my-name" :hx-patch "hello" :hx-target "#hello"}]
       (hello req "")])))

Hello

Try inspecting the above text field. You should see something like this.

Now try editing the text. When the input looses focus it submits a request to /hello and updates the contents of #hello.

The core of ctmx is the defcomponent macro which expands to both:

defcomponent enables developers to quickly build rich user interfaces with no javascript. All code is on the server backend and yet it feels the same as frontend code.

Architecture

Javascript Apps
ctmx

Ctmx uses Hypermedia as the Engine of Application State (HATEOAS), the web as it was originally supposed to be. Application state is implicitly stored in the html itself, not in a separate javascript layer. By extending the original html model instead of building a javascript layer over top, we get simplicity and much faster page load times.

Handling data flow



(defcomponent ^:endpoint form [req ^:path first-name ^:path last-name]
  [:form {:id id :hx-post "form"}
    [:input {:type "text" :name (path "first-name") :value first-name}] [:br]
    [:input {:type "text" :name (path "last-name") :value last-name}] [:br]
    (when (= ["Barry" "Crump"] [first-name last-name])
      [:div "A good keen man!"])
    [:input {:type "submit"}]])

(make-routes
  "/data-flow"
  (fn [req]
    (page (form req "Barry" ""))))

ctmx maintains a call stack of nested components. This makes it easy to label data without name clashes. Try submitting the above form and then inspecting the browser network tab.

(path "first-name") and (path "last-name") macroexpand to unique values which are automatically mapped back to the function arguments. We can use the form component multiple times on the page without worrying about a name clash.

Components in array

To expand (path "first-name") and (path "last-name") consistently we must be careful with components inside arrays. Use ctmx.rt/map-indexed to map values across an array.

MatthewMolloy
ChadThomson
(def data
  [{:first-name "Matthew" :last-name "Molloy"}
   {:first-name "Chad" :last-name "Thomson"}])

(defcomponent table-row [req i person]
  [:tr
    [:td (:first-name person)] [:td (:last-name person)]])

(defcomponent table [req]
  [:table
    (ctmx.rt/map-indexed table-row req data)])

(make-routes
  "/nesting-components"
  (fn [req]
    (page (table req))))

Transforming parameters to JSON

The UI provides a natural structure to nest our data. This corresponds closely to the database schema and provides a natural connection between the two. Try adding customers using the form below.

({})


(defn add-customer [{:keys [first-name last-name customer]}]
  {:customer
   (conj (or customer []) {:first-name first-name :last-name last-name})})

(defn- text [name value]
  [:input {:type "text" :name name :value value :required true}])

(defcomponent customer [req i {:keys [first-name last-name]}]
  [:div
    ;; other display here.
    [:input {:type "hidden" :name (path "first-name") :value first-name}]
    [:input {:type "hidden" :name (path "last-name") :value last-name}]])

(defcomponent ^:endpoint ^{:params-stack add-customer} customer-list
  [req first-name last-name ^:json-stack customer]
  [:form {:id id :hx-post "customer-list"}
    ;; display the nested params
    [:pre (-> req :params ctmx.form/json-params util/pprint)]
    [:br]

    (ctmx.rt/map-indexed serverless.functions.core/customer req customer)
    (text (path "first-name") first-name)
    (text (path "last-name") last-name)
    [:input {:type "submit" :value "Add Customer"}]])

(make-routes
  "/transforming"
  (fn [req]
    (page (customer-list req "Joe" "Stewart" []))))

As we add customers the JSON builds up to match the UI. We would lightly transform the data before persisting it, however it is often already close to what we want it to be.

This example uses the add-customer middleware to transform parameters before they are displayed.

Casting parameters

You have clicked me 0 times!

(defcomponent ^:endpoint click-div [req ^:long num-clicks]
  [:form {:id id :hx-get "click-div" :hx-trigger "click"}
    [:input {:type "hidden" :name "num-clicks" :value (inc num-clicks)}]
    "You have clicked me " num-clicks " times!"])

(make-routes
  "/parameter-casting"
  (fn [req]
    (page (click-div req 0))))

Ctmx uses native html forms, so data is submitted as strings. We can cast it as necessary. Supported casts include ^:long, ^:boolean and ^:double. See documentation for details.

We may also cast within the body of defcomponent.

[:div
  (if ^:boolean (value "grumpy")
    "Cheer up!"
    "How are you?")]

Further reading

Please see the examples.