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.
Subscribe to my newsletter
Read articles from Vincent Cantin (green-coder) directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
