DailyJS

Let's Make a Framework: DOM Manipulation Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 2

Posted by Alex R. Young on .

Welcome to part 56 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.

Last week I looked at how to build a cross-browser
innerHTML API. In this week's post I'll continue looking at
DOM manipulation.

Reading HTML

I want to read HTML like this:

'test HTML can be read': function() {
  assert.ok(turing('#dom-html-read-test').html().match(/Example/));
}

Given this HTML:

  Example

This won't work right now. The dom module html
methods need to determine if we're reading or writing, and return a
suitable result when accessed through the chained API. This can be done
based on arguments:

/**
 * Set or get innerHTML.
 *
 * @param {Object} element A DOM element
 * @param {String} html A string containing HTML
 */
dom.html = function(element, html) {
  if (arguments.length === 1)
    return element.innerHTML;

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

// Elsewhere...

turing.domChain = {
  // ...

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

In the case of a chain, it returns the first element's
innerHTML.

The results may be slightly different in each browser (IE will make node
names uppercase), but this should pass:

'test HTML can be read': function() {
  assert.ok(turing('#dom-html-read-test').html().match(/Example/i));
}

Implementing append()

To support appending HTML, I decided to refactor the
dom.replace function to make it generic. Previously it
looked like this:

/**
 * 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',
      insert,
      div;

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

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

The line that uses replaceChild can be replaced with a
callback to make this code reusable:

manipulateDOM = function(element, html, callback) {
  var context = document,
      isTable = element.nodeName === 'TABLE',
      shim,
      div;

  div = context.createElement('div');
  div.innerHTML = '<' + element.nodeName + '>' + html + '';
  shim = isTable ? div.lastChild.lastChild : div.lastChild;
  callback(isTable ? element.lastChild : element, shim);
  div = null;
};

Now append can be implemented using
appendChild in the callback:

dom.append = function(element, html) {
  manipulateDOM(element, html, function(insertTo, shim) {
    insertTo.appendChild(shim.firstChild);
  });
};

I wrote some tests for this during development to see if it actually
worked:

'test HTML can be appended': function() {
  turing('#dom-html-append').append('Example 2');
  assert.ok(turing('#dom-html-append').html().match(/Example[^E]*Example 2/));
},

'test HTML can be appended to tables': function() {
  turing('#dom-table-append').append('X2');
  assert.ok(turing('#dom-table-append').html().match(/X1[^X]*X2/));
}

This is the corresponding HTML:

    X1



  Example

I use case-insensitive regular expressions to test the results in
innerHTML.

Conclusion

This pattern of DOM manipulation is based on jQuery, which I explored
back in part 53. The main
reason manipulateDOM exists is just to turn text into HTML,
but it also has to manage hidden insertion of tbody which
can make IE behave confusingly.

There's a lot of things left to look at in this area, in particular CSS
and attribute manipulation APIs. I've tested what I've done so far in
IE6, just for kicks!