Let's Make a Framework: DOM Manipulation

2011-03-24 00:00:00 +0000 by Alex R. Young

Welcome to part 55 of Let's Make a Framework, the ongoing series about
building a JavaScript framework.

If you haven't been following along, these articles are tagged with
lmaf. The project we're creating is called Turing. Documentation is
available at turingjs.com.

The previous tutorials have looked at how jQuery implements
.html() and .css() for cross-browser HTML and
CSS manipulation. In this week's post I'll start implementing
.html() for our framework.

API and Tests

The method turing.dom.html(element, html) should be
available, along with the chained version,

A test like this should pass with the new method:

'test HTML can be written': function() {
  var element = turing.dom.get('#dom-html-tests')[0];
  turing.dom.html(element, 'This is a link');
  assert.equal(turing.dom.get('#dom-html-tests p').length, 1);
  assert.equal(turing.dom.get('#dom-html-tests a').length, 1);

And this should pass, too:

'test chained HTML works on multiple elements': function() {
  turing('#dom-html-chain-test p').html('Link');
  assert.equal(turing.dom.get('#dom-html-chain-test p a').length, 4);

The quickest route to implementing this is to use

 * DOM manipulation
 * @param {Object} element A DOM element
 * @param {String} html A string containing HTML
dom.html = function(element, html) {
  element.innerHTML = html;

// ...

turing.domChain = {
  init: function(selector) {

// ...

   * Chained DOM manipulation.  Applied to every element.
   * @param {String} html A string containing HTML
   * @returns {Object} `this`
  html: function(html) {
    for (var i = 0; i < this.elements.length; i++) {
      dom.html(this[i], html);
    return this;

Browser Support

This will work with simple tests in IE, Firefox, Chrome, etc. It'll fail
on the edge cases supported by jQuery. In particular, this won't work:

// Given a simple table (in test/dom_test.html):
'test manipulating table rows': function() {
  assert.equal(turing.dom.get('#dom-html-table-test tr').length, 2);

To make this work we need to trick IE into writing to what it considers
"read only" elements. Microsoft's innerHTML documentation
says the following:

The property is read/write for all objects except the following, for which it is read-only: COL, COLGROUP, FRAMESET, HEAD, HTML, STYLE, TABLE, TBODY, TFOOT, THEAD, TITLE, TR.

I've written a basic way around this. It works by creating a temporary
div which is populated with a table, and then the lastChild
is extracted, and the contents of the original table replaced.

   * Replaces the content of an element.
   * @param {Object} element A DOM element
   * @param {String} html A string containing HTML
  dom.replace = function(element, html) {
    var context = document,
        isTable = element.nodeName === 'TABLE',

    div = context.createElement('div');
    div.innerHTML = '<' + element.nodeName + '>' + html + '';
    insert = isTable ? div.lastChild.lastChild : div.lastChild;

    element.replaceChild(insert, element.firstChild);
    div = null;

IE automatically inserts tbody tags so I've tried to deal
with that. Dealing with this is actually quite complicated, and I'll
expand on this more next week.

This gets triggered based on an exception handler:

dom.html = function(element, html) {
  try {
    element.innerHTML = html;
  } catch (e) {
    dom.replace(element, html);

The latest commit was