My Relationship With the DOM
I personally have a weird secret obsession with creating alternative ways to make reactive application drivers using the DOM (you know, like React, Vue, Angular, Svelte, etc.). I have written about it a few times, in fact. There are bunches of approaches to creating a DOM from code:
- The Virtual DOM — pioneered by React, virtual DOMs are fast because all the updates are done in tiny objects that virtually represent the DOM, hence Virtual DOM. In an update, both the previous and next virtual DOM objects are compared through a method known as diffing, and at the end of the process the new HTML is finally patched in. The thing that VDOMs tend to bring into play, however, are larger frameworks and therefore slower initial loads.
- Procedural DOM — many frameworks that rely on template languages/markup are actually processing that markup and building a procedural DOM, which simply builds up the DOM by parsing the given template into a set of procedural instructions. Template-language-based frameworks also tend to be large and thus have slower initial loads.
- Compiled DOM — frameworks like Svelte compile their template language in to very fast vanilla JS. This is an incredibly novel approach, it’s fast, and it works really well. However, you’d need to parse a whole language in order to create a DOM-compiling program. This will likely have both smaller file sizes and, therefore, faster loads. But, because of the complexities of designing and implementing a domain-specific language, for our project this solution is out of the question.
- Native DOM — this is the true vanilla way.
document.createElement(’sometag’)is how we instantiate DOM elements, creating some fairly hefty objects. But, with all the advances of modern JS engines a lot of unused code actually gets dropped as ‘dead code’, so this is actually likely to be fairly to very fast. And at the very least, if you’re not relying on external packages and frameworks, there will be very fast initial loads. Let’s also consider — all of those frameworks pile code on top of operations that ultimately just call these functions anyway. You’re nearly guaranteed the most efficient results with vanilla JS, plain and simple.
All of these approaches are valid and have been used to power modern web applications’ DOMs effectively. For this project we’ll be taking the vanilla-est route possible.
What We Will Be Building
My personal attitude is to avoid anything too complicated for this project. I want it to be built off of vanilla JS and have zero dependencies. So template languages are out of the question. Although usually accessible for users, we’d have to parse them, adding annoying overhead and complexity. Plus, we can’t rely strictly on JS to do the job. We need to learn/develop a domain specific language (the template/markup language) to actually use the framework.
Virtual DOM’s are melted into our brains as fast thanks to React, but the reality is they’re just calling into vanilla with a bunch of overhead. They trade great performance for reliability and consistency, which has generally been OK for the state of the web. But Virtual DOMs do things like comparisons on the whole DOM tree on every update, re-rendering components unnecessarily, and adding and removing event listeners for no better reason than simplicity in declaration. So that’s not my solution either.
I like simplicity. The most beautiful and expressive code is also typically simple. So I’ll be using vanilla JS to make a miniature framework that has functionality close to that of React. It’s going to be like jQuery and React had a declarative lovechild with a fluent API. We’ll be writing codes like this:
Looks sort of like a React function component, but also like some jQuery stuff going on in there, too.
- I want to use a fluent API — meaning I want to allow the user to chain dot operator functions to carry on with the declaration of a component.
- I want to be able to declare a portion of a component as being reactive and only run updates on those specific DOM elements.
- I want to be able to compose my components into higher order components.
Those are all the bare necessity features I want to include for this introductory article. My thinking is to get us to minimum viable product features and we’ll extend them another time.
Now, without further ado, let’s finally write some code.
Wrap It Up
My first thought is to encapsulate our DOM instructions in an object. We will just be wrapping up vanilla DOM objects so this should be easy enough.
First off, let’s talk about those Symbols. I like to hide stuff I don’t want the end user to play with behind Symbols. I think this approach is superior to the classic
_ prefixing that you’ll see for ‘private’ variables in legacy (and not legacy) code bases. Symbol keyed fields also aren’t included in the iteration when you call
In terms of the object, I’ve opted for the classic constructor function style, meaning we can create an
Html instance by calling it with the
new operator. We expect our parameter
parent to be a DOM object. This will represent the parent object in the tree. If it is null we make a new document fragment and use that as the parent instead. This will be useful for a few reasons:
- The document fragment will render its child contents all at once, so there won’t be lots of redraws called. In fact, when we collapse our tree we will append one final document fragment, so any render will be one draw call.
- When we go to compose our components, the document fragments will magically disappear when we call the native
appendChild. The trees will collapse as we concatenate them, leaving that one final document fragment I just mentioned.
We also give our HTML wrapper object a target field using our
TARGET Symbol. That will be important soon. The takeaway here is that we can now call
new Html() to start the base of our DOM tree.
One more quick note. I dislike using the new keyword (spaces, more verbose), even though I love the constructor function pattern. So typically, I abstract away new by just wrapping it in a regular function:
And one other function that we ought to have is a way to turn an existing DOM element into one of our wrapped
Html objects, having that element as the target:
This way, when we need to get a past target element, we still can pull it into
Html wrapper context
Since we have a constructor function, we can easily extend its functionality by adding functions to its prototype. We’ll be doing that a lot. I used to not advocate this approach, but that was because I didn’t understand prototype as well as I should’ve. By extending prototype we can add extremely low cost functionality. The code you write lives in only one place — the prototype. The functions are simply borrowed from that prototype by the instance objects, meaning they don’t get copied into those instances. This way the functions don’t have to live on the actual instances. Plus, if you just write a function with the same name and put it on an instance object, you’ve essentially ‘overridden’ the prototypical functionality, making it really flexible when you need to change your base behavior.
But that’s enough of an aside about prototype, let’s write out a way to give our
Html objects children:
As you can see, we’ve merely wrapped our native DOM construction into this function,
e. We assign our new target value so that we track the object intuitively. Notice that the function also returns itself. That means the
Html object can keep chaining dot operator functions. You may be wondering, ‘what’s with the sh***y nondescript name, buddy?’. Well, I never intend to call
e directly. Its a backing function for our fluent element-named functions. Now how are we going to write out all those functions for the multitude of HTML elements that exist?
We can just make a list of the names of the elements we want, and add each function procedurally within a
We can do the exact same thing for attributes, too:
Empty Nest Syndrome
Let’s take a moment to appreciate that one of our primary goals is near accomplished already. We can fluently create dot operator chains of arbitrary length to declare our DOM. Sort of. Except we still can’t nest into children of our parent element. We can only construct a flat list of elements. Let’s extend
close functions let us control the nesting structure of our DOM.
open creates a new instance of
Html with the current target as the parent (so future targets will be children), and the
close function takes the parent, which if we’re nested equally is the previous target element, and asks for that parent element’s parent. Phew. Let’s make sure that holds up.
That’s some highly readable code there. (Not really). It would be with some proper white-space, alas the console can only handle so much and I’m too lazy to figure out how multi-line editing works just for this test snippet. Anyway, it does appear to be working just fine. We
open and it nests, we
close and it de-nests. Perfect, this is the behavior that we want. We’ve officially accomplished step 1 — make a fluent declarative DOM builder.
Putting It Together
Now we can move onto another task. I’ve decided we should go for composition first since designing reactivity is going to be a fair bit more complicated. Luckily, it will be easy to compose our components with another really simple pair of extensions to our
root function recursively looks for the top-most element in the current tree referred to by this
Html wrapper object. We reassign the
mtop value in the while loop — the simple logic is that
mtop.parentElement will return false when it is null, meaning that we’re at the top of the tree. Then
concat is as simple as appending another trees
.root() to this tree’s parent. Now we can write codes like this:
And with that simple extension, we have composition-friendly components. There’s another tick off of our MVP checklist.
But wait, we need to also be able to mount our application to the DOM. Let’s write a tiny function to do that:
Now we can replace a root div element with our custom SPA application.
Finally, if we want to compare our functionality to modern SPA frameworks, we need a way to update our
Html object’s raw HTML on the fly. But because I want to focus on simplicity, our components are non-reactive by default — they simply generate static HTML. One of the first ways that we can make our mini SPA framework more reactive is by adding the ability to listen to events. Let’s write an
on function to handle adding event listeners to our target HTML.
Admittedly there is a ton going on here. Let’s break it down part by part.
class— this is a function that can take any number of string parameters and add them to the target DOM object’s class list. It returns
thisso that we can continue to utilize it in fluent-API fashion.
on— this function registers an event listener to the target. Because I want to preserve a uniform API across our framework, I’ve opted to inject a wrapped DOM object. This way when we are defining the callback, we can manipulate the sender DOM object using our same fluent-API.
myButton— this is the example. Like I was saying before, because of the injected
Htmlobject, we can use its reference (here,
hx) to manipulate the DOM element using our regular fluent syntax.
Now we can have any element intercept whatever event we like, and we can access that element with our fluent API.
But there are limitations to this approach. What about when you need to make changes to the DOM that touch more than simply the target of an event? And what happens when we want to hold onto some application state? For this, we will need a messaging system, and more extensions to our
A Trip To The Store
What we need to do to get reactive functionality is register components as interceptors of custom events that aren’t reliant on the target from which they originate. When we use DOM events, we can’t really send an event from a parent to a child. Not without a bunch of overhead. Instead, we will build a tiny redux-like store, and register elements that will respond to state changes.
Let’s begin by defining the store:
tinyStore really is pretty tiny. It’s a simple function which takes a
model and a parameter called
model is the default value for the application state.
rdfn is a special function called a reducer. A reducer, in the redux-sense, is a function that takes a
model and an object that we will know as an Action. It uses these ingredients to produce a new state, which we will observe from registered elements. That observation will allow us to react to changes in the state on registered component elements.
Notice we actually store a copy of the default model, and we only produce copies of the store when we call
state() is called.
As a quick aside, for more complex state we must use a less naive function to copy our state values. Using the spread operator *will not* deep copy an object. So if you have nested objects in your state, make sure you write a deep copy function. It’s easy using recursion, but I didn’t include it here for simplicity. I will include a link to the source for this project at the end of the article, and in the release code you can find the deep copy function that I use.
Here’s a little example of how that works:
Just like I explained before, to create a Store, we first must create a default model —
myAppModel here. You can see our
reducer shown here takes a
model and a destructured array
[k, data]. That destructured array is the format that we expect our Actions to be in.
k is the kind of Action we’re sending.
data is the payload of our Action. That means whatever extra data we want to send along with our action. We can
dispatch an Action to change the state. Finally, we can get the state using
state() which we see here has changed due to our call to
dispatch. Since we can only change the state via
dispatch, and we can only get a copy of the state, That pretty much covers all the functionality of the store. What, though, do we use it for?
With a Symbol keyed property on
window and another extension to our
Html object, we can easily register a store to use with our application.
My intention is to call this
use function one time at the root of the application. Calling it more than once would overwrite the value, which isn’t really my intention. I could guard against this better, but for the sake of simplicity this will suffice. This will allow us to access our registered store from within the application. Now it’s time for another nice Symbol-keyed-
There are a few things happening here:
- We create the Symbol-keyed property
windowand assign it an empty object. When I write the
subscribefunction (in just a moment!) we will populate the
UPDATESobject with arrays of callbacks to be triggered when the
- We add another extension method to the
Htmlobject. It takes an array in Pair format, destructured to the fields
data. Remember, this is the format that we are using to describe Actions in our messaging system.
Html.prototype.dispatchwe call the actual
tinyStore.dispatchfunction. This ensures that our state will be accurate when we pass it on to…
- …the function located at
window[UPDATES][k]. These are the actual registered subscribers to the state changes. If there is an object at
[UPDATES][k]then it will be ensured by our
subscribefunction to be an array that we can
forEachover. Each value
fin this array will be the function callback for a registered element.
As always we return
this so that it fits into our fluent API. Now about that
subscribe function — we haven’t written it yet!
Don’t Forget to Subscribe
Lucky for us, its not that hard to build a way to subscribe to our messaging system.
See? It’s not that complicated. We take both a key
k and a callback
cb. We save our current target in the
cur variable. In our definition of
listen, we inject our
Html.from(cur) object into the callback
cb (along with
data) so that we can again preserve our consistent API. If our
window[UPDATES] object isn’t assigned, we assign it to an empty array using the
||= operator (or-equals assignment). Using
||= for this is nice, because it will only execute the expression if
window[UPDATES][k] is unassigned, and it ensures that it is assigned for the next step. Next we
listener function into our ensured-array
window[UPDATES][k] value. And finally, we return
this to continue our fluent API.
Note: I will be covering memory leaks and dropping subscriptions properly in the next episode of this series, when I talk about a vanilla routing extension for this project.
We can use such a mechanism like this:
Now we need to test it all together to prove it works. Here’s a pen with all the pieces working together:
It was a fairly lengthy and involved process, but we managed to make something pretty impressive. It has features on par with major DOM rendering frameworks like React, but we made it in under 200 source lines. We created something with no dependencies; we imported nothing to build this.
Furthermore, I imagine that this solution is fairly fast. Remember that the React Virtual DOM is diffing and rebuilding potentially huge trees of objects on every update. We’re doing significantly less. We are non-reactive by default, and otherwise require a subscription to the application store to become reactive. So we re-render only tiny portions of our DOM with no diffing algorithm, just closures and the
Html wrapper object interface.
That’s all I’m going to cover in this article, but stay tuned for a Write Your Own Vanilla SPA Framework Pt 2! I hope you have enjoyed this wild excursion into creating a higher level DOM manipulation API all your own.