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:
After splitting at /(\{\{[^}]+\}\})/g
.
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.
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:
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:
- Two-way binds with form elements.
- Dynamic list rendering (like Vue’s
v-for
). - Conditional element rendering (like Vue’s
v-if
).
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.