DOM Patching from a BackflipHTML Template
In my introduction to BackflipHTML I wrote:
I want to embrace Web Components. If at all possible, I want my templates to serve as the single source of truth for the component in all its states. I do not want to write HTML or CSS inside of JS strings.
That means that in addition to turning templates and data into a string of HTML on the backend, it should have the ability to update the DOM after that HTML is read by the browser.
I took the first small step in that direction. BackflipHTML now has a dom-patch output that can, in some situations, surgically update the DOM based on changes to data.
An Example
Here is a tiny Light DOM web component called “styled-meter”. It wraps a <meter> in a custom element, displays a valname for the meter and changes color when the value is above 80.
Here is the Backflip template:
<styled-meter
b-attr:level
:class="level > 80 ? 'high' : ''"
b-export>
<h3>{{ valname }}</h3>
<meter low="0" high="80" max="100" :value="level"></meter>
</styled-meter>
The CSS class high is set on the element when level is above 80. If we assume this is part of a dashboard we can imagine the page is receiving updates on the values somehow, and levels get updated on the rendered page. This implies there is some part of the template logic that has to function inside the browser.
(It’s getting easier to do this kind of thing with CSS alone, negating the need for JS-based intervention, but in this case selectors of the form [level > 80] are not yet available).
And it works! I added a browser-side JS file that periodically updates the values on the level attribute of each styled-level and the DOM updates as expected. Neat!
How does it work?
New generator: dom-patch
Backflip templates can compile to non-JS languages if a generator is written for the purpose. So far I only have a PHP generator in addition to JS (I’ll add Go later). These turn compiled templates and data into strings of HTML.
dom-patch is just another generator, but it’s a bit different. It generates JavaScript that manipulates the DOM based on changes in data.
b-attr directive implies “live” data
b-attr on a custom element partial tells the compiler that data is being passed via an attribute. In the styled-meter template above, level is a b-attr which means it can be used in the template and it also appears in the generated output as a regular HTML element attribute. I talked about b-attr in my previous post.
The difference is now all b-attr attributes are assumed to be “live” meaning that they are assumed to change after the HTML is rendered, and dom-patch generates JS to support that.
(By the way I know it should be data-level, but the hyphen complicates things and I don’t have it sorted in Backflip yet.)
Code Generation
dom-patch finds the custom element partials that have a b-attr, checks that it can live-update the sites where that b-attr variable is used in the DOM (see more on restrictions below) and generates a class with a update(name) method. That method takes the name of an attribute, and updates the DOM based on its latest value.
Within the generated class there are different sets of functions:
- Element getters. Note that
dom-patchinjects the DOM withdata-bfid="<random>"attributes at the compilation step for elements it needs to reference from the DOM. - Data expression functions: Every site that has an expression like
level > 80 ? 'high' : ''gets a generated function that evaluates that expression. - The
updatefunction connects a data change with a sequence of element changes.
You can see it here. It’s all quite naive and will have to be rethought a few more times as I go forwards but for now it kind of works.
Use the generated code
You have to create the custom element class yourself and call the generated class:
class StyledMeter extends HTMLElement {
static observedAttributes = ["level"];
constructor() {
super();
this.bf = new BackflipStyledMeter(this);
}
attributeChangedCallback(name) {
this.bf.update(name);
}
}
customElements.define("styled-meter", StyledMeter);
I intend to keep BackflipHTML library-like as much as possible. For this reason the frontend doesn’t magically become reactive thanks to dom-patch. You keep complete control over the custom element class and just call on Backflip to patch your DOM.
Try it out
The repo for this demo is on Tangled (I’m trying to get away from GitHub little by little): https://tangled.org/olivierforget.net/backflip-demo-dom-patch. If you’re not yet on Tangled you can clone the repo using the https endpoint.
Some things to notice:
- There is both a PHP server and a JS (Deno) server. Both use the same templates, I just added PHP as an output in
backflip.jsonand had Claude write a PHP server for it. The HTML output is exactly the same. - If you turn JS off in your browser, the meters render as expected: the class and meter value are set correctly. Server-side rendering FTW.
- Turn JS on and the meters change values dynamically via
dom-patch.
Restrictions
This is the simplest thing I put together as a first attempt to tackle templates that have dual backend/frontend functionality. As such it only works under very limited conditions:
Updates attribute values only
Attribute values are the only thing that can be updated DOM-side. Meaning <div b-bind:attrname="js expression"> That’s it. No <span>{{ someText }}</span> and definitely no b-if and b-for directives. That will come later, if I can even get there.
All b-attr are considered “live”
All b-attr on a custom element partial are considered “live”. This means that every site that uses them causes JS to get generated that will be shipped to the browser.
The author of a partial should know what can change DOM-side and what won’t (it’s manifested in the custom element class as static observedAttributes = ["level"] after all). As such explicitly marking the “live” attributes would prevent unnecessary JS generation.
Can’t use with regular template variables
In BackflipHTML you can pass data to a referenced template or partial using <some-partial b-data:somevar="expression">.... As is typical of template languages, that directive is eliminated from the generated output. The only thing that’s left is the actual manifestation of those values, say in an attribute or {{ somevar }}. This works fine for purely server-side generation.
However the total ghosting of variables and values that make up the rendered output handicaps DOM updates on the frontend.
If I try to evaluate the string expression `${level} ${unit}` where level is a b-attr and unit was passed as b-data, I can’t because I don’t know the value of unit.
Scott Jehl wrote about this issue and possibilities for passing template variable data down to the browser.
Light DOM only for now
This only works with Light DOM web components for now. I want BackflipHTML to handle both but I’ll focus on shadow-DOM components later.
Onwards
This is a very restricted capability, yet despite that it was tough getting to this point. I questioned whether it would pan out, but seeing it in action is encouraging. There are many more hurdles ahead, and many interesting things that can be developed.
Next up I’ll look at bringing b-data into the rendered output so it can be used in expressions browser-side, and I may tackle dom-patching {{ someVar }} directives.