DailyJS

Let's Make a Framework: DOM Manipulation Part 3

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 3

Posted by Alex R. Young on .
Featured

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 3

Posted by Alex R. Young on .

Welcome to part 57 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 implemented some DOM methods, like append and
html for reading innerHTML. There are still
yet more DOM manipulation methods that we can build on the work we've
done so far.

text

The text method should work like html, in that
it either returns the text contents of an element or sets them. Given
this test:

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

And this HTML:

  Example

Then it should be fairly easy to write something that extracts the text
nodes.

jQuery's Implementation

Let's look at how jQuery does it:

Sizzle.getText = function( elems ) {
    var ret = "", elem;

    for ( var i = 0; elems[i]; i++ ) {
        elem = elems[i];

        // Get the text from text nodes and CDATA nodes
        if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
            ret += elem.nodeValue;

        // Traverse everything else, except comment nodes
        } else if ( elem.nodeType !== 8 ) {
            ret += Sizzle.getText( elem.childNodes );
        }
    }

    return ret;
};

This is from Sizzle. It will get the text node of all the descendants
recursively.

The comments help follow what happens based on each type of node (else
the nodeType integer values would be confusing). This could
actually be expressed using Node:

  if (elem.nodeType === Node.TEXT_NODE
      || elem.nodeType === Node.CDATA_SECTION_NODE) {
};

Unfortunately, not all browsers support this. I thought it might be more
friendly for readers if we set up an object to store node types:

nodeTypes = {
  ELEMENT_NODE:                  1,
  ATTRIBUTE_NODE:                2,
  TEXT_NODE:                     3,
  CDATA_SECTION_NODE:            4,
  ENTITY_REFERENCE_NODE:         5,
  ENTITY_NODE:                   6,
  PROCESSING_INSTRUCTION_NODE:   7,
  COMMENT_NODE:                  8,
  DOCUMENT_NODE:                 9,
  DOCUMENT_TYPE_NODE:            10,
  DOCUMENT_FRAGMENT_NODE:        11,
  NOTATION_NODE:                 12
};

So I ended up implementing a similar function to jQuery:

function getText(elements) {
  var results = '', element, i;

  for (i = 0; elements[i]; i++) {
    element = elements[i];
    if (element.nodeType === nodeTypes.TEXT_NODE 
        || element.nodeType === nodeTypes.CDATA_SECTION_NODE) {
      results += element.nodeValue;
    } else if (element.nodeType !== nodeTypes.COMMENT_NODE) {
      results += getText(element.childNodes);
    }
  }

  return results;
};

I also set up some short functions for turing.dom and the
chained API:

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

// ...

turing.domChain = {
  // ...

  /**
   * Get or set text nodes.  Applied to every element.
   *
   * @param {String} text A string containing text to set
   * @returns {Object} `this` or the text content
   */
  text: function(text) {
    if (arguments.length === 0) {
      return this.elements.length === 0 ? null : getText(this.elements);
    } else {
      for (var i = 0; i < this.elements.length; i++) {
      }
    }
    return this;
  }

Like reading innerHTML, working with text nodes is fairly
straightforward.

Writing Text

jQuery writes to text nodes like this:

return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );

The key thing here is it uses empty to clear the node
first.

jQuery has a lot of features like caching that it has to handle, which
makes empty more complex than our simple case. Given this
test:

'test nodes can be emptied': function() {
  turing.dom.empty(turing.dom.get('#dom-html-empty-test')[0]);
  assert.equal(turing.dom.get('#dom-html-empty-test')[0].innerHTML, '');
}

The bare minimum required to implement the equivalent functionality is
quite simple:

/**
 * Empty nodes.
 *
 * @param {Object} element A DOM element
 */
dom.empty = function(element) {
  while (element.firstChild) {
    element.removeChild(element.firstChild);
  }
};

A test for writing text might look like this:

'test chained text nodes can be written': function() {
  turing('#dom-text-write-test p').text('Written again');
  assert.ok(turing.dom.get('#dom-text-write-test p')[0].innerHTML.match(/Written again/));
}

And an implementation is fairly basic DOM stuff, with
appendChild and createTextNode, combined with
dom.empty:

/**
 * Set or get text nodes.
 *
 * @param {Object} element A DOM element
 * @param {String} text A string containing text
 */
dom.text = function(element, text) {
  if (arguments.length === 1) {
    return getText(element);
  } else {
    dom.empty(element);
    element.appendChild(document.createTextNode(text));
  }
};

// Chained:

/**
 * Get or set text nodes.  Applied to every element.
 *
 * @param {String} text A string containing text to set
 * @returns {Object} `this` or the text content
 */
text: function(text) {
  if (arguments.length === 0) {
    return this.elements.length === 0 ? null : getText(this.elements);
  } else {
    for (var i = 0; i < this.elements.length; i++) {
      dom.text(this.elements[i], text);
    }
  }
  return this;
}

I tested this in WebKit, IE 6, and Firefox.

Conclusion

Dealing with text nodes is simpler than HTML, but it requires a little
bit of care to make it feel as straightforward as it should be. jQuery's
implementation is more complicated than my tutorial because it does a
lot of housekeeping in the background, but the basic principles are
outlined here.

This week's code is in commit
2c7ca
.