Minimum viable view library, part I

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.