Making HTML reactive using Signaali

Signaali is a library which provides functions to build reactive systems. In this article, I describe how to use it to change the DOM when an event happens or when a data changes.
The complete source code displayed in this article can be found in Vrac’s repository.
Let’s make an HTML page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Making HTML reactive using Signaali</title>
</head>
<body>
<script src="/js/main.js" defer></script>
<main>
<h1>Reactive HTML without a framework</h1>
<p>This demonstrates how to use Signaali to manipulate the DOM reactively.</p>
<h2>Reactive counter</h2>
<div>
Counter value: <span id="counter-element">n/a</span>
<div>
<button id="inc-counter-button">Increment</button>
<button id="reset-counter-button">Reset</button>
</div>
</div>
<h2>Lazy effects</h2>
<div>
The effects are lazy, the user decides when to re-run them.
Click the button to update the DOM.
<div>
<button id="run-effects-button">Run effects</button>
</div>
</div>
</main>
</body>
</html>
This page has 3 buttons on which we will listen the “click” event. It also has a span element containing a text which we will update from a Signaali effect. Here is the skeleton of the source code. It has no reactivity yet, the callback functions are registered and unregistered, but they do nothing yet.
(ns example.core) ;; TODO: require signaali's namespace
;; Get the references to the DOM elements we want to modify
(def ^js counter-element (js/document.getElementById "counter-element"))
(def ^js inc-counter-button (js/document.getElementById "inc-counter-button"))
(def ^js reset-counter-button (js/document.getElementById "reset-counter-button"))
(def ^js run-effects-button (js/document.getElementById "run-effects-button"))
;; TODO: a state for the counter
;; TODO: an effect that updates the text in the DOM element `counter-element`
(defn on-inc-counter-button-clicked []
;; TODO: increase the counter state
,)
(defn on-reset-counter-button-clicked []
;; TODO: set the counter state to zero
,)
(defn on-run-effects-button-clicked []
;; TODO: run the effect which will update the DOM
,)
(defn setup! []
(.addEventListener inc-counter-button "click" on-inc-counter-button-clicked)
(.addEventListener reset-counter-button "click" on-reset-counter-button-clicked)
(.addEventListener run-effects-button "click" on-run-effects-button-clicked)
;; TODO: set the counter's state to zero
;; TODO: run the effect once, to update the DOM
,)
(defn shutdown! []
(.removeEventListener inc-counter-button "click" on-inc-counter-button-clicked)
(.removeEventListener reset-counter-button "click" on-reset-counter-button-clicked)
(.removeEventListener run-effects-button "click" on-run-effects-button-clicked)
;; TODO: dispose the effect, to avoid memory leaks.
,)
;; Shadow-CLJS hooks: start & reload the app
(defn start-app []
(setup!))
(defn ^:dev/before-load stop-app []
(shutdown!))
(defn ^:dev/after-load restart-app []
(setup!))
Let’s start using Signaali by including the namespace:
(ns example.core
(:require [signaali.reactive :as sr]))
Then we need a state for the counter
(def counter-state
(sr/create-state nil
;; Optional param, useful for debugging
{:metadata {:name "counter state"}}))
Then we need an effect which will update the DOM node with a text version of the counter’s value.
(def counter-text-updater
(sr/create-effect #(set! (.-textContent counter-element) (str @counter-state))
;; Optional param, useful for debugging
{:metadata {:name "counter text updater"}}))
Now let’s make the callbacks interact with Signaali’s state and effect.
(defn on-inc-counter-button-clicked []
(swap! counter-state inc))
(defn on-reset-counter-button-clicked []
(reset! counter-state 0))
(defn on-run-effects-button-clicked []
(sr/re-run-stale-effectful-nodes))
And last, we need to initialize the state and the DOM element’s text on setup, and dispose the effect on shutdown.
(defn setup! []
(.addEventListener inc-counter-button "click" on-inc-counter-button-clicked)
(.addEventListener reset-counter-button "click" on-reset-counter-button-clicked)
(.addEventListener run-effects-button "click" on-run-effects-button-clicked)
(reset! counter-state 0)
(sr/add-on-dispose-callback counter-text-updater #(set! (.-textContent counter-element) "n/a"))
@counter-text-updater)
(defn shutdown! []
(sr/dispose counter-text-updater)
(.removeEventListener inc-counter-button "click" on-inc-counter-button-clicked)
(.removeEventListener reset-counter-button "click" on-reset-counter-button-clicked)
(.removeEventListener run-effects-button "click" on-run-effects-button-clicked))
You now know most of what you need to start your own web framework in Clojure(script).
In the next blog post of this series, I will start writing helpers to avoid most of the boilerplate we had to type above. That’s usually how web frameworks are born.


