Find me on GitHub

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.