SimpleJX, a Web Un-framework

Kenny Tilton
13 min readJan 6, 2019

The simplicity of HTML, the power of reactive.

Welcome to SimpleJX, a pure Javascript (or ClojureScript) Web un-framework combining:

  • simple, HTML-like syntax;
  • a declarative/functional paradigm;
  • powerful yet transparent reactive data flow;
  • optimal automatic DOM manipulation;
  • and simple async programming.

The secret? Matrix, a sophisticated reactive library that:

  • codes transparently (no publish/subscribe);
  • propagates change automatically;
  • unifies all concerns, not just the UI; and
  • requires no pre-processing.

Let us look more closely at some of those ideas.

Un-framework

By “un-framework” we mean “introduces no architectural mechanisms of its own”. SimpleJX is a thin ES2015 JS library of tag-generating functions whose syntax mirrors that of HTML and CSS. It inherits all the stability, simplicity, and documentation of the MDN standard. Wrappers for React and ReactNative are also available. With ClojureDart, Flutter comes into play.

A good graphic designer should be comfortable with SimpleJX in hours.

Efficient

Matrix tracks state dependency between individual properties. SimpleJX leverages this fine-grained information to make the minimum of DOM updates necessary at every turn, without building and diffing VDOM.

If some user gesture requires only that a hidden element be revealed, the only DOM manipulation will be to clear the hidden attribute of that element.

Reactive

As with ReactJS, the designer/developer enjoys a declarative coding experience, defining only the what and not the tedious how of manipulating the DOM or propagating state change across the app.

Unlike pure ReactJS, SimpleJX manages state for us, transparently detecting dependencies and automatically updating the DOM of a responsive design.

SimpleJX encourages us to tackle more ambitious interfaces, and so produce a better user experience.

Unifies all concerns

Most UI libraries offer some form of reactive data flow connecting view and model. Matrix, though, is a generic state management library that can connect all concerns of an application.

SimpleJX itself is an example of that, using two thousands lines of code to connect HTML, CSS, and XHR in one seamless state network. Our TodoMVC app connects as well local storage, the system clock, and routing.

All interesting state gets connected by reliable, transparent, automatic state management.

SimpleJX in action

So far, so abstract. We now make things concrete with a simple carousel Web application evolved concept by concept.

We just build HTML, by just writing Javascript

In SimpleJX, we generate HTML tags using JS functions with syntax much like that of HTML tags:

HTML looks like<tag attributes*> content* </tag>.

SimpleJX looks like (tag [attributes*] content*).

Getting specific,h1 creates a JS proxy object for an <h1>DOM element. Attributes are an optional first parameter:

function main() {
return h1({ style: "text-align:center;margin:2em" },
"Hello, world.");
}

For the curious, here is how the proxy object joins the page. In index.html:

<body>
<script>
document.body.innerHTML = tag2html(window.main());
</script>
</body>

main builds a JS proxy for some DOM “tag”, and tag2HTML converts that proxy to actual HTML.

In our next version, we drive home a bit more the un-framework idea:

  • the style attribute as a plain JS object; and
  • children elements following the optional attributes, as in a true HTML tag.
div( {style: {padding: "16px"}},
h1({ class: "fubook doctitle",
style: "font-size:96px" },
"Simple JX"),
p({ class: "tagline" },
"The simplicity of HTML. The magic of reactive."))

The class: property connects to an external CSS file just as does HTML class attribute:

body {
background: #232323;
}
.doctitle {
text-align: center;
font-size: 28px;
color: #000;
}
.doctitle::first-letter {
color: red;
}
.tagline {
font-size: 18px;
color: white;
text-align: center;
line-height: 30px;
padding: 4px;
}

Here is the resulting HTML…

<div id=2 style="padding:9px;">
<h1 id=4 class="fubook doctitle">
Simple JX
</h1>
<p id="6" class="tagline">
The simplicity of HTML.<br>The magic of reactive.
</p>
</div>

…and the resulting app.

For a static page, at least, we have avoided any artificial framework: everything looks and works like HTML and CSS, and without pre-processing of any kind. It is all just JS.

Functions as components

Now that our tags are just JS functions, we can decompose large stretches of interface code into subroutines.

In our first example of this, we break the tag line out into a dedicated function, just to get some code out of the main line.

function SimpleJXTagLine () {
span({ class: "tagline" },
"The simplicity of HTML. The power of reactive.");

Next we create a truly reusable, parameterized component for page Credits:

function Credits(attrs, ...content) {
return footer( Object.assign({}, { class: "info" }, attrs),
content.map(s => p({}, s)))}

Our main line now looks a bit simpler:

function main() {
return [
div(
{ class: "app" },
h1({ class: "doctitle" }, "SimpleJX&trade;"),
SimpleJXTagLine(),
Credits(
{ style: "font-size:14px;color:black;margin-top:0px" },
"Created by <a href='...'>Kenny Tilton</a>",
"Maker of <a href='...'>Tilton's Algebra</a>"))]
}

The app.

SimpleJX implements Web components, in a sense.

Reactive Web Interfaces

So far, so static. But the hard part of UI/UX is making elements work together, controlling or reflecting each other in combination.

“Simple things should be simple, complex things should be possible.” — Alan Kay

We begin with a simple carousel of images, with arrow controls to navigate forward or back through the images. Below is a live, embedded, working app. Try clicking “<” or “>”.

For that functionality we needed:

  • a bit of state to keep track of the current image index;
  • an element to display the current image; and
  • two arrows to increment or decrement the image index.

First, the state itself, along with a confession: we did not tell you about the second map parameter.

QuoteCarousel = () =>
div(
{ class: "simplicityQCarousel" },
{
name: "qCarousel",
quoteNo: cI(0)
},
QCarouselControlBar(),
QImage()
);

That optional second map defines custom properties such as quoteNo. The wrapper cI (short for “cell Input”) makes quoteNo reactive, and initializes the value to zero.

Reactive? Please read on.

Next comes the image renderer, and a problem. The renderer needs to know the current image index, but that state lives in the top QuoteCarousel container. How can the renderer get to that value in a way that lets it “react” to changes?

Think “CSS selector”.

Selectors (in effect)

Think “CSS selector”. The other bit of custom state we added above is the name “qCarousel”. For Matrix lifecycle reasons, search via true CSS selectors will not do, so the Matrix library provides utilities for searching what we call the family tree (hence fm prefixes).

Using the utility fmUp and the name “qCarousel”, any given element can find the QuoteCarousel and read or write its quoteNo. Here is the read operation, highlighted in bold:

QImage = () =>
div({ class: "captionImage" },
{ currentQ: cF( c =>
qIdx = wrapAt( c.md.fmUp("qCarousel").quoteNo,
simplicityQ.length)
return simplicityQ[ qIdx])},
c =>
img( { class: "fazer",
style: "width:300px",
src: S3Root + c.md.currentQ.url }))

The write operation works the same: the arrow click handler navigates via fmUp to the carousel element so it can increment its quote index. Here is the write, again in bold:

QCarouselControlBar = () =>
div({ class: "qControlBar" },
QStepper(-1),
QStepper(1))
QStepper = increment =>
i( { class: "material-icons stepper",
onclick: md => {
md.fmUp("qCarousel").quoteNo += increment
}
},
increment === -1 ?
"navigate_before"
: "navigate_next")

But how does incrementing quoteNo cause the QImage to pick a new graphic?

Transparent reactive programming

We mentioned transparency at the outset, and just now that the quoteNoproperty was “reactive”. Most likely, the reader did not notice those qualities sail by. Let us now highlight them both.

In the QImage component we see the current image defined as a reactive function of the value quoteNo, made reactive simply by reading quoteNowith conventional JS syntax from within the cF formula.

In the QStepper widget, we see the onclick handler simply incrementing quoteNo, again with conventional JS +=syntax.

And we are done. Thanks to Matrix internals (and the magic of JS define-property), when quoteNo changes the selected image will change, and SimpleJX internals will alter actual DOM imgelement’s src attribute.

There is no need to subscribe, no need to publish, and no need to use special pre-processed forms to make the reactive magic happen. We code transparently and declaratively and SimpleJX/Matrix brings it to life.

One bit of state, many controls, many views

We begin with our first real quote carousel, our minimum viable product. It shows the author name and a quotation as well as their image, and offers reasonable UI controls.

To control the quotation displayed: click the author name; use the graphical arrows; or click in app window (to focus the keyboard there) and press the left or right keyboard arrow keys.

Now look for three manifestations of the selected quotation: the image shown, the quotation shown, and the highlighting of the author’s name.

Success!

The user loves our carousel so much they want to leave it running by itself in a dedicated window! Here’s the spec on the new automatic carousel:

  • add a play/pause button that defaults to a “play” icon;
  • when they click the button, have the carousel start moving through the quotations, changing every two seconds and…
  • …show a “pause” icon instead of “play”;
  • hide the “next” and “previous” arrows;
  • when they click an author’s name, stop play and show that; and
  • have the space bar also start/stop play.

Below we have the enhanced carousel, but with a U/X problem we will fix shortly. Reminder: to test the space bar, click inside the pen to give it the keyboard focus.

The U/X problem we have in mind is the delay between pressing “play” and the first new image appearing. The user thinks, “Hunh? Nothing happened. Did I mis-click?” For two long seconds.

We will fix that in the next iteration. First, let us see what makes the carousel run.

How it works, or Callback Heaven!

Here is how the carousel works. First, we added a new playing property to the carousel widget, making it a cI “input Cell” so we can change it from normal code. We also code up a new cell observer property:

QuoteCarousel = () =>
div(
{ class: "simplicityQCarousel" },
{
name: "qCarousel",
quoteNo: cI(0),
playing: cI(false, { observer: obsPlaying })
},
QCarouselControlBar(),
QImage()
);

Observers

Observers are just “on change” callbacks for cell properties. They get to see the property being managed by the cell, the model (object) owning the property, the new and prior values, and the cell itself.

obsPlaying = (propertyName, md, newValue, priorValue, cell) => {
if (newValue) {
md.playInterval = setInterval(() => {
++md.quoteNo;
}, 2000);
} else {
window.clearInterval(md.playInterval);
}
};

Observers let the dataflow between properties do something other than cause other properties to change. In this case, we implement the playing behavior by incrementing quoteNo every two seconds using a JS interval.

This leaves the Play/Pause button with a simple job, toggle playing:

QPlayButton = () =>
i({
class: "material-icons qPlayer", x
onclick: md => {
qc = md.fmUp("qCarousel")
qc.playing = !qc.playing
},
content: cF(c => {
return c.md.fmUp("qCarousel").playing ?
"pause" : "play_arrow"
})
});

Note also how the icon changes from play to pause: because it reads the playing property, the formula runs again when the same widget toggles that property (and a SimpleJX observer conveys the new content to the DOM).

Great, but where is “Callback Heaven”?

Where is Callback Heaven? Callback Hell is coping with the asynchronous arrival of data in an imperative world, usually in the form of XHR responses. Here, it is setInterval that calls our increment function asynchronously, but the principle and problems are the same.

Matrix was born in the crucible of the unpredictable event loop. Interval expiration is no different from a user deciding to click the mouse or press a key, or the unpredictable arrival of XHR responses: the new data gets absorbed predictably and with integrity.

Change within change: solving our UX delay

Now let us fix that awkward delay after we click play. We decide we should change the quotation as soon as they click “play”, then let the interval handle subsequent changes. Again we can leverage observers.

If you watch closely in the demo above, you will see that, when you click play, the play icon immediately becomes a pause icon, and the stepping arrows disappear at once. But the image does not change! This is because the interval we created will not call our function for the first time until the interval has elapsed the first time.

What we would like to do, at the same time we create the interval, is move at once to the next image so the users gets immediate reward. But this means triggering data flow by incrementing the data flow cell quoteNo, and when an observer runs we are smack in the middle of processing the flow from the playing state! The data integrity we mentioned relies on processing only one state change at a time. What to do?

Matrix lets us enqueue a deferred change while processing another change. Here is the new working example, with the image changing as soon as we click “play”:

Here we enhance the playing observer to force the carousel to the next quotation as soon as playing changes to true:

obsPlaying = (propertyName, md, newValue, priorValue, c) => {
if (newValue) {
withChange(() => ++md.quoteNo)
md.playInterval = setInterval(() => {
++md.quoteNo;
}, 4000);
} else {
window.clearInterval(md.playInterval);
}
};

Now when playing goes to true, this observer enqueues a quoteNo bump and creates an interval to do so repeatedly, saving it in a normal property of the carousel for later clearing. Propagation of the change to playing then propagates further, altering or hiding other controls.

Scaling

We now know almost everything there is to know about SimpleJX. We will see a couple of new reactive bells and whistles below, but nothing that compromises the simplicity of SimpleJX.

So why aren’t we done? Because we have yet to illustrate the most important SimpleJX qualities of all:

  • the simplicity remains as the interface grows, and
  • the simplicity makes refactoring a breeze.

Most libraries offer beautifully simple introductory demos. But do they scale? From here on we will simply elaborate our carousel based on the kind of requests for enhancement one can expect if our products are well-received.

Feature creep!

Our user loves the app even more, and has more requests! They want play to be slower to have more time to appreciate each slide, or super slow so it is not distracting. We decide to let them control the speed themselves, and come up with this spec:

  • while the carousel is playing, show two new icons on either side of the now “pause” icon, one like “<<” to slow down, and a “>>” to speed up;
  • the keyboard up/down arrows will be alternatives to the speed buttons;
  • when speed up is requested, immediately advance to the next quote (since they seem to be impatient).

And here it is. Press “play” to try the speed controls and, again, click the carousel to try keyboard input.

The implementation introduces nothing new. The point here is that complexity increases only linearly as our UIs grow. If you have been following the source, you will see we have now divided it up sensibly, by functionality.

Now let us look at how we implemented speed control. The approach might be becoming familiar. First, we need a new reactive input, the delay between frames, with an observer to replace the interval when the delay changes:

QuoteCarousel = () =>
div(
{ class: "simplicityQCarousel" },
{
name: "qCarousel",
quoteNo: cI(0),
playing: cI(false, { observer: obsPlaying }),
delay: cI(2000, { observer: obsDelay })
},
QCarouselControlBar(),
QImage()
);

The observer:

obsDelay = (propertyName, md, newValue, priorValue, c) => {
if (!md.playInterval) return;
window.clearInterval(md.playInterval);
md.playInterval = setInterval(() => {
++md.quoteNo;
}, md.delay);
if (newValue < priorValue)
// they want to go faster, so we show a new quote immediately
withChange(++md.quoteNo);
};

Note that again we suspect user impatience and enqueue an immediate next quotation if they are accelerating.

[This is a WIP, with one more chapter to come: demonstrating the ease of refactoring by adding a second carousel. Thinking quotations on happiness would be great, lots of good ones there.]

The Programmer Experience

Let us step back from the trees to appreciate the reactive forest. What is the programmer experience when using SimpleJX? As soon as we programmers had our toggle switches and stored programs, we began the meta-work of building better ways to build applications. For those of you only reading about reactive software, here are the big wins this practitioner sees.

Transparent, Declarative, Automatic Program State Management

We build applications the way we build a spreadsheet model, providing functional descriptions of how some property should be derived, and then letting an internal engine worry about when to recompute that property to keep it current. And it is great fun, as many first experienced with ReactJS.

This one “win” is composed of several nested wins. The transparency means we do not have to write code to publish and subscribe explicitly: dependencies are detected automatically during property reads. The automatic change propagation means we never worry about what else to change when changing something; the engine knows. And, finally, in so-called glitch-free reactive engines such as SimpleJX and MobX, we need not worry about recalculating things in the right order, which is a problem in complex dependency graphs in which a single derived node can have multiple dependency paths leading to some source property.

Omniscience

Because SimpleJX state is organized as a tree of objects in which every object knows its parent as well as its children, any property of any object has unlimited, global visibility into any property of any other object. SimpleJX objects are omniscient. Gasp. What happened to “pure”?!

In SimpleJX, the productivity bet is on programmer expressiveness. Now when programming a UI/UX, everything on the screen is connected. Should the trash can icon way up in a toolbar be disabled if no to-do item is selected? Just code it that way: make the trash can disabled property a function of the current selection of the to-do list container. The reactive engine does the rest.

This global reach is not as risky as it may sound. Because we search outwards from the object owning the property we are computing, we enjoy a naturally limited context provided by our position in the tree.

Omnipotence

By the same token, any given event handler is free to alter any “input” property anywhere, by navigating from the object handling the event to any other object to procedurally alter that input property.

The trash can widget in a toolbar anywhere in the widget hierarchy can navigate to the to-do list container and remove an item from the list directly.

In this case, the risk of global reach is mitigated again by the context offered by the position of the UI control in the larger application tree. Context locality comes also from the UI event indicating which object was the target of a mouse gesture, or by the application keyboard focus.

Speed, for Free

Because solid reactive engines transparently detect exactly which properties depend on which other properties, they also know which properties do not need to be recomputed. A developer using such an engine, through no additional effort, is guaranteed optimal runtime state propagation, including the logical minimum of DOM manipulation.

Summary

The programmer, when coding up any widget, can call on any other application state to decide what to render. Without coding explicit publish/subscribe, their widget will stay current at run time with other state, and can act on any other application state knowing all necessary consequent change will happen automatically. And everything will be handled with optimal efficiency.

And, yes, it is exactly as powerful (and fun) as it sounds.

--

--

Kenny Tilton

Developer and student of reactive systems. Lisper. Aging lion. Some assembly required.