Reactive templating: Reactivity

This is the third and final part of a three-part series.

So far I have a lexer that turns expressions into tokens, a parser that normalizes and makes sense of the tokens, and finally some clever code generation that runs the expression and returns a value.

What’s next? How do I stick this value into the template, replacing the expression? The obvious and most simple solution would look something like element.innerText = val, but that’s a one-time deal and therefore not very useful. Not very reactive. And what if there’s more than one template expression per element, such as:

1<div>{{ greeting }}, {{ name }}</div>

It just doesn’t work.

Before we do any sort of text replacements, we can split the Text node into multiple Text nodes:

One single Text node:

A single Text node

After splitting at /(\{\{[^}]+\}\})/g.

After splitting a Text node into multiple Text nodes

Now that each Text node is either entirely a template expression or some other text I don’t care about, I can directly map template expression nodes directly to a function that returns the current values of the expression within. Or I can do the opposite, which is much more interesting and more to the point of this post.

Reactively updating Text nodes

For all of this to be considered reactive, changes to the model should be reflected in the DOM immediately.

My preferred approach to this is a common one, and for good reason. I’ll create a Proxy to, well, proxy a target object in order to augment its default get and set operations. It’s a common approach likely because this is almost certainly the type of thing that Proxy is designed for: getter/setter side effects.

Proxy setter

If you’re unfamiliar with Proxy, you can read up on it here:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

If you read the previous posts, you might remember that the lexer does some post-processing on variable tokens so that they’re always written in bracket notation. This is where I make use of that.

Given an expression, I’m given a set of tokens. Some of those tokens are variable tokens. This means that any changes made to the values of those variable tokens could quite possibly result in a new value to this expression. Since everything in my model is proxied, I want to augment the proxy setters for those model properties (e.g. model["variable"]["from"]["expression"]) so that after their new value is set, the expression is reevaluated and the Text node is updated.

So how do I augment these property setters? What I’ve chosen to do is recursively add a $handlers property to every level of the target object being proxied so that target[$handlers] eventually becomes an array of callbacks, each callback being some side-effect of mutating the target object.

Something like this:

Proxy setter handlers

Again, this happens at every level and every property of the object. In the diagram above, target becomes a new Proxy. So does target.message1, target.message2 and target.message3. Each of these is assigned it’s own set of $handlers.

With this in place, changes to the model properties don’t trigger a re-render of the entire DOM tree (the component in my case). There’s no need for that. Instead, changes to properties only effect parts of the DOM that specifically rely on those properties. Nice!

What next

This pattern can easily be adapted as the base for other features:

The lexer isn’t limited to template expressions.

1<div if="(dog.age * 7) > person.age | Boolean">
2  ..
3</div>

When this expression is run through the lexer, I get both the result of the evaluated expression and a list of tokens (most importantly the variable tokens) that can now be considered as model dependencies of this if expression. Those variables can, as before, be assigned handlers that trigger the re-evaluation of this if attribute.

Everything that worked in the template expression works in this if expression, including filters.

The hard part has already been done, so building out these kinds of features becomes relatively simple.