Dressing Up The Atom
Clothing the imperative shell
In the to-do app, per the Getting Started practices, you created a project folder and a couple modules importing the necessary libraries. The atom was defined in main.js
and the commands imported from todo.js
.
Having completed the core needed to simulate getting things done, you’re now ready to stand up the GUI. Gussy things up.
// ./main.js
import _ from "./libs/atomic_/core.js";
import $ from "./libs/atomic_/shell.js";
import dom from "./libs/atomic_/dom.js";
import * as t from "./todo.js";
import {reg} from "./libs/cmd.js";
const el = document.querySelector("#todo"); //root element
const $state = $.atom(t.init()); //TODO supply init
reg({$state, t});
$.sub($state, function(state){
//TODO render complete content for `el`
});
The GUI is a 2-sided coin, a front and a back, a face and a feed. This correlates to the ports of one-way data flows. You see, on one side you have the atom projecting an image, a face of the app, everything rendered under the root element. And on the other side, the app responds to a feed of events bubbling out of the root element (i.e., the DOM) as the user interacts with it. The face and the feed.
The Face
You’re going to start with the face. This depends on t.init()
returning an appropriate initial state.
The app before work begins:
export function init(){
return {
view: "all",
next: 1,
todo: []
}
}
Rendering
Now project this first frame of your movie reel onto the DOM, your figurative silver screen. Supply the appropriate rendering logic in the handler, taking the state and using it to build the DOM required of the app you envision.
$.sub($state, function(state){
//TODO render complete content for `el`
});
There’s a mapping between what the state models and what you take it to mean. You must account for the look, layout, and the means of interaction. This is design—a sketch, imagined or actual, of what it looks like and how it behaves. It’s a vision for how affordances enable users to get things done.
Affordances: Anything rendered to the GUI which allows the user interact with the app. This includes controls, both basic and custom, things like textboxes, checkboxes, date fields, and buttons. It also include invisibles like events from key presses to gestures.
Your renderer must fill out the root element, its complete face, even though the controls won’t initially do anything since the feed has yet to be activated.
The Atomic dom
library can help. Here’s an example of it rendering one small part:
// ./main.js
const li = dom.tag("li"), //needed dom tags
div = dom.tag("div"),
input = dom.tag("input"),
label = dom.tag("label"),
button = dom.tag("button");
function todo({id, title}){
return li({"data-id": id},
div({"class": "view"},
input({class: "toggle", type: "checkbox"}),
label(title),
button({class: "destroy"})),
input({class: "entry", value: title}));
}
Use the library or the JavaScript you know. The approach cares nothing about how the DOM gets built. You just need a handler which renders a complete GUI for any frame it’s given. You’d probably write functions for distinct parts of the GUI and, building bottom-up, still more for combining/coordinating them.
If the root element is empty, it suggests everything will be rendered from scratch.
<!-- ./index.html -->
<body id='todo-app'>
</body>
While this can work for trivial GUIs, more GUIs are nontrivial. It’s better to use a predefined structure and fixed parts. Given this, break the DOM down into fixtures which can be independently rendered on updates.
Rendering: Fully replacing the contents of the fixture element(s) whenever the data in the atom changes.
Once this is working for the first frame, advance the user story along its timeline. It doesn’t matter there’s no feed. You’ve been trained to see the app as a command line. Keep treating it as such. Issue a command that adds a to-do, marks one complete, or changes the view—all the things your core now handles—from the console, as you’ve been doing all along.
With each issued command a frame of state will be logged to the console for your inspection. Ensure the handler reacts appropriately. Compare the logged state with what’s on the screen. It must replace the contents of the fixtures, rather than append to them, for this to work.
ℹ️ Info: Take care your handler doesn’t drop fixtures from the DOM. Instead, replace and/or update their contents. Their permanent residence in the DOM is what makes them “fixtures.”
Continue throwing commands at it and see it correctly projects the latest snapshot to the screen regardless. A good handler will render any valid state its given. You may have to adjust the handler to get there.
To confirm its resilience—since testing has yet to be discussed—build a timeline for some user story by adding a series of commands temporarily to main.js
, setting breakpoints on each.
$.swap($state, addTodo("Call mom"))
$.swap($state, addTodo("Buy milk"))
$.swap($state, addTodo("Host movie night"))
$.swap($state, toggleStatus(1))
$.swap($state, clearCompleted)
You’ll be able to hit refresh on the browser and see things play out as expected.
The Feed
The affordances you rendered were ultimately meant to facilitate atom updates, something which has been temporarily relegated to the command line. The interface has been poised to take this over.
This is the feed. It translates DOM events into commands.
To this end, you wire up handlers which respond to user interactions with the DOM, which is continuously being rerendered and/or updated. This wiring up happens once using a well-known technique called event delegation. It eliminates the need to continually wire up events as elements are dropped and new ones injected. So, for example, presuming a textbox and a button for adding to-dos, here’s how you might respond to a request to add a to-do.
const el = document.getElementById("todo"); //root element
$.on(el, "click", "#add-todo", function(e){
e.preventDefault();
const title = document.getElementById("title").textContent;
$.swap($state, t.addTodo(title));
});
The key action is the swap
statement. It’s the command you’d have issued from the command line. It may access multiple elements. Here, click
is delegated to the add-todo
button via the root element. The task title is pulled from a textbox.
This is where problems can arise. With a rendering-only approach you fully replace each fixture even when only one small piece gets changed. This can sometimes cause problems. Perhaps an element having focus, loses focus. Or the screen unpredictably jumps in a scrolling area. These wonky behaviors won’t make for a pleasant user experience. Fortunately, these problems are fixable.
Something more than just rendering is needed.
Patching
The stream of states the app renders is built with a handler which looks like this:
$.sub($state, function(state){
//TODO supply rendering logic
});
But, as this approach won’t always suffice, it may be necessary to use another.
Here a history signal is created from the $state
atom:
const $hist = $.hist($state);
A signal is like an atom. It can be subscribed to in the same way, but because it’s derivative, it’s read only. It can’t be updated directly. It’s like a pure function in how it transforms data from one form into another. But it’s also like a cell in a spreadsheet, derived from reference cells, which automatically updates whenever any of them change.
Since signals are read only, continue directing updates to the atom.
ℹ️ Signal: An component which emits a potentially indefinite stream of data. It takes some parameters which configure it to produce a stream from the one or more streams (atoms and/or signals) it consumes.
The stream the history signal provides is similar to the one emitted by the atom. (Yes, atoms emit streams, too.) It receives not only the current frame, but also the prior. That’s both at once.
$.sub($hist, function([curr, prior]){
if (prior == null) {
//TODO supply rendering logic
} else {
//TODO supply patching logic
}
});
So, on the initial callback, the one preceeding any updates to the atom, the prior will be null:
//prior
null
// curr
{
view: "all",
next: 1,
todo: []
}
The current frame will be the same as the frame the $state
atom initially emitted. This is useful. It means, with minimal adaptation, you can take the rendering logic from the prior handler and plug it into the top block of the if
statement in the new handler. None of your original work is lost. Everything is exactly as it was.
Until this command is invoked…
$.swap($state, addTodo("Call mom"))
…and the callback receives these 2 frames:
//curr
{
view: "all",
next: 2,
todo: [
{id: 1, title: "Call mom", status: "active"}
]
}
// prior
{
view: "all",
next: 1,
todo: []
}
Having both frames is especially useful. They can be compared to see what changed. What the handler now needs is logic to amend the DOM which was previously rendered. Thus, instead of rerendering, this logic must reconcile the DOM to the latest state. This makes having the comparable frames especially helpful. This is where patching comes in.
Patching: Updating the previously rendered contents of the fixture element(s) whenever the data in the atom changes. This leaves the portions of the DOM which needn’t change alone and performs a minimal edit to put it into the state it would have reached given a full rendering/replacement.
And while this takes work, it’s not as challenging as one might think. But to understand patching one must first understand the technique it relies on: diffing.
Diffing
You see, after the first callback, all subsequent callbacks receive 2 frames, as demonstrated above. Those frames must be compared to identify what changed. Recall our atom timeline to see how commands transform one frame into another.
Each frame the callback receives corresponds to a graphical representation potentially projected to the DOM. Your job is to programmatically compare the frames and, given the results, patch the DOM. Essentially, you hold the 2 versions of the DOM in your mind’s eye (the prior and the current, with the latter not yet projected) and compare the data in a manner which permits you to reconcile the one DOM with the other.
This offers a useful start:
$.sub($hist, function([curr, prior]){
if (prior == null) {
//rendering logic from original handler
} else {
if (curr.todo.length > prior.todo.length) {
//a to-do has been added
} else (curr.todo.length < prior.todo.length) {
//a to-do has been removed
} else {
//a to-do may have been updated
}
}
});
There are plenty of workable strategies for programmatically diffing 2 frames. It doesn’t matter where you start. With iterative refinement, you’ll find what works.
Here’s a more holistic approach:
$.sub($hist, function([curr, prior]){
if (prior == null) {
//rendering logic from original handler
} else {
//diffing logic
const icurr = _.index(({id}) => id, curr.todo),
iprior = _.index(({id}) => id, prior.todo),
ids = _.union(_.keys(icurr), _.keys(iprior));
for(const id of ids){
const currTodo = icurr[id],
priorTodo = iprior[id];
if (currTodo && priorTodo) {
if (currTodo === priorTodo) {
//nothing happened!
} else {
//to-do was updated
}
} else if (currTodo) {
//to-do was added
} else if (priorTodo) {
//to-do was removed
}
}
}
});
This approach indexes the to-dos in the before and after copies by id
to make finding the before/after instance of any given to-do possible. It gathers a complete set of ids and iterates through them to determine which to-dos were added, which removed, and which updated.
Diffing: Comparing 2 frames of state to determine which values and/or entities were added, which removed, and which updated. Identifying what changed facilitates patching the DOM.
The most handy line of code is the one determining an update with nothing more than a strict equality operator (===
). When JavaScript compares value types (e.g., strings and numbers) its truthiness is based on value equality. But when it compares reference types (e.g., objects) it’s based on identity. The only object which is strictly equal to an object is itself.
This truth is illustrated here:
const same = {id: 1, title: "Call mom", status: "active"}
=== {id: 1, title: "Call mom", status: "active"}; //false
const task = {id: 1, title: "Call mom", status: "active"};
const same = task === task; //true
This is fortunate because our atom anticipates swapping with commands which take state and return new state reuse everything (every object) they can from the original states, precisely for this reason. These functions return every unchanged object they can. Thus is the nature of purity.
See how the original to-dos are all returned:
function addTodo(title){
return function(state){
const {view, next, todo} = state;
const id = next,
status = "active",
task = {id, title, status};
return { //new return object created
view,
next: next + 1, //compute
todo: todo.concat([task]) //combine
}
}
The “combine” line takes the existing list and concatenates one additional task onto the end of it. All but one of the to-dos returned in the new state object are the same to-dos which got passed in via state
. So it took the to-do items, unpackaged them, and repackaged them along with one additional to-do. Several objects simultaneouly exist in both frames. They share an identity because they are one in the same.
Thus, the strategy is to locate what is supposed to be the same entity from both frames using a unique identifier (e.g. id
). Then, having a reference to an identifiable object in either frame, use a cheap strict equality comparison (===
). If they share an identity, the entity was not changed. If they don’t, but neither is null, it was. If one is null, it was either added or removed. If it was changed, compare them property by property in the same way to see which fields, exactly, got changed. The reason this all works is because the kind of functions used to effect updates against the atom. Pure functions!
Diffing permits you to readily identify which entities were added, removed, or updated. Having assessed this from the data, you determine what must be done to patch the DOM. Prefer patching to rendering (e.g. making wholesale replacements) wherever rendering produces a noticeably worse user experience.
With a properly set breakpoint, you will have the prior DOM on screen, and the last 2 frames, prior and current, in your log. Looking at the current state you will know what the DOM should be. Thus, everything you need to contemplate to programmatically solve the problem sits before you frozen in time and is readily repeatable.
There is one thing more you will have had to do to aid reconcilliation. Record entity identifiers directly in the DOM. For example, the 3 to-dos below when rendered to the DOM must include an attribute (e.g., data-id
) with the corresponding id
.
{
view: "all",
next: 4,
todo: [
{id: 1, title: "Call mom", status: "active"},
{id: 2, title: "Buy milk", status: "active"},
{id: 3, title: "Host movie night", status: "active"}
]
}
This enables them to be easily selected from within the root element.
const task = el.querySelector(`li[data-id='${id}']`);
Thus, per the diffing context, you’ll know whether you’re adding, removing, or updating an entity. Then, also having its identifier, you remove or update the entity already rendered to the DOM, or add it. That’s the essence of patching.
Here’s the handler is more fully fleshed out:
const el = document.getElementById("todo-app");
const fixtures = {
todos: el.querySelector("ul.todos")
}
$.sub($hist, function([curr, prior]){
if (prior == null) {
//rendering logic from original handler
} else {
//diffing/patching logic
const icurr = _.index(({id}) => id, curr.todo),
iprior = _.index(({id}) => id, prior.todo),
ids = _.union(_.keys(icurr), _.keys(iprior));
for(const id of ids){
const task = fixtures.todos.querySelector(`li[data-id='${id}']`);
const currTodo = icurr[id],
priorTodo = iprior[id];
if (currTodo && priorTodo) {
if (currTodo !== priorTodo) {
//rendered/replaced todo, but could've patched it instead
task.replaceWith(todo(currTodo));
}
} else if (currTodo) {
fixtures.todos.append(todo(currTodo));
} else if (priorTodo) {
task.remove();
}
}
}
});
This includes the logic for handling the todos themselves. But not the logic for reacting to changes to view
. That’d need another block of logic after the for
.
//diffing/patching logic
if (curr.view !== prior?.view) {
fixtures.todos.setAttribute("data-view", curr.view);
}
It might seem conspicuous the view logic isn’t intermingled with the to-do loop above it. That is, it’s true view changes can affect how and where to-do elements are rendered. But when a simpler GUI is being implemented, view changes need not be a structural concern (i.e., what elements are located where). It can do all that’s necessary based on a single attribute dynamically kept in sync with the select view. So a data-view
attribute (dynamically set to ‘all’, ‘active’ or ‘completed’) on the list (ul
) itself and data-status
attributes on the items (li
).
ul.todos[data-view='completed'] li[data-status='active'],
ul.todos[data-view='active'] li[data-status='completed'] {
display: none;
}
Imagine this requirement. The customer wants the upper right of the screen to display a block with the number of still-active to-dos.
const fixtures = {
todos: el.querySelector("ul.todos"),
remaining: el.querySelector("div.remaining")
}
//diffing/patching logic
fixtures.remaining.textContent =
curr.todo.filter(task => task.status === "active").length;
This demonstrates that the handler must be complete. It must react to any changes in the model which have some bearing on the GUI.
Initially, logic was developed to handle changes to the to-dos, but it also needed logic for handling changes to the view. The changes are detected by comparing before/after instances of the state. Trivial fixtures updates, like the remaining to-do count, can skip the comparison.
That’s the essence of a diffing/patching strategy built around a single handler. It’s a good approach, but there’s another.
Splitting
The $state
atom can be split into fine-grained signals which isolate specific parts of the state. Signals, like atoms, rely on pure functions. The $.map
function transforms the inputs of a stream to new outputs on another stream. (Actually, it can receive any number of input signals and take the latest value of each to produce its output, but that’s not needed here.)
//define signals
const $todo = $.map(({todo}) => todo, $state);
const $view = $.map(({view}) => view, $state);
const $count = $.map(todo => todo.filter(task => task.status === "active").length, $todo);
For the simpler updates to the GUI you can subscribe to the signal directly:
//react to signal updates
$.sub($view, view => fixtures.todos.setAttribute("data-view", view));
$.sub($count, count => fixtures.remaining.textContent = count);
These callbacks receive only the latest state, not the latest 2 frames as with a history signal. For their purposes, that’s sufficient. But when getting the latest 2 frames is useful, feel free to use a history signals, as before, but using a distinct slice of the data model:
const $todoHist = $.hist($todo);
$.sub($todoHist, function([curr, prior]){
//adapt the original diffing/patching logic
});
Don’t forget to register your signals:
reg({$todo, $view, $count, $todoHist});
This faciliates monitoring how the app reacts as commands are issued. Each of these signals, when updated, will be logged to the console.
There’s an added benefit to splitting. Internally, all signals, even ones which unlike hist
only emit the latest frame, remember the prior frame. This way, when some change happens (e.g., the atom gets updated) and it computes its current frame, it performs a strict equality comparison between the prior frame and its current and only emits (calls the callback) if the result changed.
So, for example, if you issue a command which updates the view (e.g. the view
property in the atom) every signal wired up to it does some internal work to determine whether it should fire. So the $todo
signal checks the todo
property of both frames and seeing no change to either array, emits nothing. Work happened, but nothing visible came of it. Likewise, for the same reason, $count
checks things internally and emits nothing. Only $view
actually emits.
Thus, splitting is another good tool.
There is some flexibility to building around a function core. The core itself is particular about how it gets initialized and updated (with pure functions only!), but the imperative shell—which is the messy business of reconciling what happened inside the atom to the DOM—is less particular.
You can create hist
signals from the atom and break down the problem internally in its handler (see example). Or you can split an atom into several finer-grained signals (see example). So you are splitting either from within or from without. You have multiple tools aiding the job: signals, rendering, patching and diffing.
The imperative shell has always been there. It’s the typical stuff you’re used to when writing apps. The exciting piece, the game changer, is the functional core. Its chief cost is the DOM reconcilliation logic. But once you’ve explored and experienced the benefits of a functional core living inside your imperative shell, it’ll seem a bargain when compared to making apps without it.
ML