Posted by Juha Lindstedt on January 4th, 2016
I'm going to show you step by step how I built a fully capable and extremely performant view library, weighting just couple of kilobytes. I will also prove that DOM is actually quite fast, if used properly.
View library we're going to create doesn't care if the data is mutable or immutable - both will work. You can also choose to reorder DOM elements by key or just replace the contents. But the main idea is to understand 100 % how everything work under the hood.
These techniques are based on my FRZR view library (simplified a bit). Check it out if you want to get started immediately, but I still encourage you to follow through these posts – I promise there's something new to learn.
We will begin by focusing on creating, reordering and removing DOM elements in this first post.
Let's start!
Creating elements
Before we're going to build anything, let's study how HTML elements are created in plain JavaScript.
Vanilla basics
Creating HTML elements is quite easy:
// create elements
var h1 = document.createElement('h1');
var p = document.createElement('p');
// add text
h1.textContent = 'Hello world';
p.textContent = 'Vanilla JavaScript rocks!';
// add to DOM
document.body.appendChild(h1);
document.body.appendChild(p);
// result
// <body><h1>Hello world</h1><p>Vanilla JavaScript rocks!</p></body>
Helper
We'll make it even easier with a little helper function:
function el (tagName, attributes) {
var element = document.createElement(tagName);
// go through attributes and set them
for (var attributeName in attributes) {
element[attributeName] = attributes[attributeName];
}
return element;
}
Usage
Couldn't be smoother to use:
// create elements
var h1 = el('h1', { textContent: 'Hello WRLD!' });
var p = el('p', { innerHTML: "Works like the train toilet.<br><i>(Finnish proverb)</i>" });
// add to DOM
document.body.appendChild(h1);
document.body.appendChild(p);
View
Next we will create a View. It will be just a wrapper for HTML elements, with some extra features (we will get back to those in the next part). Think Views as components.
Constructor
Let's get to business and define a View constructor:
function View (options, data) {
for (var key in options) {
if (key === 'el') {
// little trick here to pass the parameters to the el helper
if (typeof options.el === 'string') {
this.el = el(options.el);
} else if (options.el instanceof Array) {
this.el = el(options.el[0], options.el[1]);
} else {
this.el = options.el;
}
} else {
this[key] = options[key];
}
}
// let's get back to this line later
if (this.init) this.init(data);
}
Mounting children
Then add some child mounting methods:
View.prototype.addChild = function (childView) {
this.el.appendChild(childView.el);
childView.parent = this;
};
View.prototype.addBefore = function (childView, before) {
this.el.insertBefore(childView.el, before.el || before);
childView.parent = this;
};
Usage
Let's try it out!
var body = new View({ el: document.body });
var h1 = new View({
el: ['h1', { textContent: 'Hello View!' }]
});
var p = new View({
el: ['p', { textContent: 'Powered by: ' }]
});
// shameless advertisement
var a = new View({
el: ['a', { href: 'https://frzr.js.org', target: '_blank', textContent: 'FRZR' }]
});
p.addChild(a);
body.addChild(h1);
body.addChild(p);
Add/reorder/remove
Time for some magic. Here's one simple method to add/reorder/remove child views:
View.prototype.setChildren = function (views) {
// traverse the DOM starting from the first child element (if present)
var traverse = this.el.firstChild;
// go through given views (if any)
if (views) {
for (var i = 0; i < views.length; i++) {
if (views[i].el === traverse) {
// element already in place, continue to next sibling
traverse = traverse.nextSibling;
continue;
}
// insert/reorder element to the dom
if (traverse) {
this.addBefore(views[i], traverse);
} else {
this.addChild(views[i]);
}
}
}
// remove any DOM nodes left out
while (traverse) {
var next = traverse.nextSibling;
this.el.removeChild(traverse);
traverse = next;
}
}
Example
Let's create a list of Views and start to shuffle them:
var body = new View({ el: document.body });
var ul = new View({ el: 'ul' });
var views = new Array(25);
for (var i = 0; i < views.length; i++) {
views[i] = new View({
el: ['li', { textContent: 'Item ' + i }]
});
}
ul.setChildren(views);
body.addChild(ul);
setInterval(function () {
views.sort(function () { return Math.random() * 2 - 1; });
ul.setChildren(views);
}, 250);
Inheritance helper
This is just for convenience, an inheritance helper function:
View.extend = function (options) {
function ExtendedView (data) {
View.call(this, options, data);
}
ExtendedView.prototype = Object.create(View.prototype);
ExtendedView.prototype.constructor = ExtendedView;
return ExtendedView;
}
Example
Now we can start building components. Let's also measure how performant our tiny view library is by reordering 1000 DOM elements one by one!
// our list element component
var Li = View.extend({
el: 'li',
init: function (data) {
// this gets executed when the View is created
this.el.textContent = data;
}
})
var body = new View({ el: document.body });
var ul = new View({ el: 'ul' });
// let's create list of views
var views = new Array(1000);
for (var i = 0; i < views.length; i++) {
views[i] = new Li('Item ' + i);
}
// add to DOM
ul.setChildren(views);
// let's see how fast we're running
var fps = new View({ el : 'p' });
body.addChild(fps);
body.addChild(ul);
// fps calculation..
var lastFrame = Date.now();
var frameTimeTotal = 0;
var frameTimeCount = 0;
// start running
tick();
function tick () {
// tick on every animationFrame
requestAnimationFrame(tick);
// save frame start time
var now = Date.now();
// take last element and move to first
views.unshift(views.pop());
// update views
ul.setChildren(views);
// fps stuff
frameTimeTotal += (now - lastFrame);
frameTimeCount++;
lastFrame = now;
};
setInterval(function () {
// print fps
fps.el.textContent = 'Running at ' + (1000 / (frameTimeTotal / frameTimeCount)).toFixed(2) + ' fps';
}, 1000);
Next episode
In the next episode we'll learn how to update the DOM elements efficiently. While I write the next post, you can check out some more advanced examples made with FRZR.
Subscribe
Click here to subscribe. I will send you a maximum of one email per blog post and promise not to share your email to anyone.