SPAs without Javascript

Did you know you can create Single Page Applications (SPAs) without Javascript?

Try SimpleUI for a for a smoother, more productive workflow. SimpleUI is based on Clojure, take a quick look if you have not used Clojure before.

(defcomponent ^:endpoint main [req] [:div#fix-my-id.and-my-class "fix my text"])
Code Output


You say 'Goodbye' and I say 'Hello'

The key idea of SimpleUI is to expose UI components as endpoints. Each one can update on its own, just as if it was a frontend component. Unlike with frontend frameworks there is no js bundle because everything is rendered server side.

Fix the code so that 'Goodbye' becomes 'Hello'.

let greetInput = $('#greet');
greetInput.value = 'Fred';
await click('#submit-greeting'); let responseDiv = $('#response');
assert(responseDiv.innerText === 'Hello Fred')
(defcomponent ^:endpoint greeting [req my-name] [:div#response "Goodbye " my-name]) (defcomponent ^:endpoint main [req] [:form {:hx-post "greeting" :hx-target "#response"} [:label "What is your name?"] [:input#greet {:type "text" :name "my-name"}] [:input#submit-greeting {:type "submit"}] (greeting req "")])
Code Output


HTMX Frontend

Htmx is what gives SimpleUI its superpowers. In particular you can use htmx attributes which start with hx-* to control UI.

By default each defcomponent updates itself. Set hx-target so that it updates the adjacent div instead. Tests will start when you click the button under Code Output.

let subcomponents = $$('.subcomponent');
assert(subcomponents.length === 2)
(defcomponent ^:endpoint subcomponent [req] [:button.subcomponent {:hx-post "subcomponent"} "Click me"]) (defcomponent ^:endpoint main [req] [:div (subcomponent req) [:div#update-me "Update me instead!"]])
Code Output


Casting Arguments

By default SimpleUI submits http parameters with regular url encoding so that they appear as strings on the server. It can be convenient to cast to other forms such as ^:long or ^:boolean when needed.

Update the code so that the button increments the counter instead of decrementing it.

for (let i = 0; i < 5; i++) { await click($('#change-number')); } const finalValue = $('#curr-value').innerText;
assert(finalValue === '10')
(defcomponent ^:endpoint main [req ^:long count] (let [count (or count 5)] ;; default [:form {:hx-post "main"} [:div#curr-value count] [:input {:type "hidden" :name "count" :value (dec count)}] [:input#change-number {:type "submit"}]]))
Code Output


A Journey in the Dark

The Doors of Durin, Lord of Moria. Speak, friend, and enter.

$('#password').value = 'Edro, edro!';
await click('#speak');
assert(src('.door') === '/moria_closed.jpg')
$('#password').value = 'Mellon!' await click('#speak');
assert(src('.door') === '/moria_open.gif')
(def moria-closed [:img.door {:src "/moria_closed.jpg"}]) (def moria-open [:img.door {:src "/moria_open.gif"}]) (defcomponent ^:endpoint main [req password] [:form {:hx-post "main"} moria-closed [:input#password {:type "text" :name "password" :value password}] [:input#speak {:type "submit"}]])
Code Output


Next Steps

Try setting up your own project! The Kit framework provides an excellent starting point, follow the instructions here.

SimpleUI advertises as Javascript free, however this tutorial makes a small exception to safely execute unknown code. Read the details in the source.

Lastly, don't forget the video that started it all (and gave SimpleUI its name). See below.

Simple Made Easy

A great talk.