Let's Make a Framework: DOM Manipulation Part 4

14 Apr 2011 | By Alex Young | Tags frameworks tutorials lmaf documentation dom css

Welcome to part 58 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 read and write functionality for text nodes. Along the way I had to build an empty method to clean out nodes before changing them.

I don’t know about you, but I find researching the implementations of these DOM methods fascinating. It might not be as glamorous as animations, but it underpins everything we do in client-side JavaScript.

This week I want to look at reading style values.

Reading Style Values

A few weeks ago I researched how jQuery implements its css() method, in Part 54, CSS Manipulation. Most frameworks implement something similar in some form, Prototype for example has setStyle and getStyle.

The essence of reading styles is to use currentStyle or getComputedStyle based on browser support. The getComputedStyle method was introduced in DOM level 2, and is intended to provide read access to computed style values. jQuery makes this a bit more friendly by allowing for both camelcase names and the hyphenated ones most of us are used to.

camelCase

The easiest way to do this in JavaScript is:

'backgroundColor'.replace(/-([a-z])/ig, function(all, letter) { return letter.toUpperCase(); })

… which is what jQuery does.

/**
 * Converts property names with hyphens to camelCase.
 *
 * @param {String} text A property name
 * @returns {String} text A camelCase property name
 */
function camelCase(text) {
  if (typeof text !== 'string') return;
  return text.replace(/-([a-z])/ig, function(all, letter) { return letter.toUpperCase(); });
};

We also need to go the other way. IE’s properties are not in camelCase!

/**
 * Converts property names in camelCase to ones with hyphens.
 *
 * @param {String} text A property name
 * @returns {String} text A camelCase property name
 */
function uncamel(text) {
  if (typeof text !== 'string') return;
  return text.replace(/([A-Z])/g, '-$1').toLowerCase();
};

Getting Style Values

I want this test to pass:

'test reading style properties': function() {
  var element = turing.dom.get('#dom-test')[0];
  assert.equal(turing.dom.css(element, 'background-color'), 'rgb(240, 240, 240)');
}

The turing.dom.css(element, propertyName) signature will be used to read CSS properties directly. I’ll also add a chained version with the more succinct syntax:

'test chained reading style properties': function() {
  assert.equal(turing('#dom-test').css('background-color'), 'rgb(240, 240, 240)');
}

But there’s a catch. IE will return #f0f0f0 instead, so the revised tests look like this (with camelCase assertions added as well):

'test reading style properties': function() {
  var element = turing.dom.get('#dom-test')[0],
      expected = element.currentStyle ? '#f0f0f0' : 'rgb(240, 240, 240)';
  assert.equal(turing.dom.css(element, 'background-color'), expected);
  assert.equal(turing.dom.css(element, 'backgroundColor'), expected);
},

'test chained reading style properties': function() {
  var element = turing.dom.get('#dom-test')[0],
      expected = element.currentStyle ? '#f0f0f0' : 'rgb(240, 240, 240)';
  assert.equal(turing('#dom-test').css('background-color'), expected);
  assert.equal(turing('#dom-test').css('backgroundColor'), expected);
}

Ideally our CSS API would return the same values. Maybe this could be something for a future iteration?

The way I’ve implemented this is to set up a getStyle functional internal to the turing.dom module. One is IE-friendly and the other is for W3C browsers:

if (document.documentElement.currentStyle) {
  getStyle = function(element, property) {
    return element.currentStyle[camelCase(property)];
  };
} else if (window.getComputedStyle) {
  getStyle = function(element, property) {
    return document.defaultView.getComputedStyle(element, null).getPropertyValue(uncamel(property));
  };
}

/**
 * Gets or sets style values.
 *
 * @param {Object} element A DOM element 
 * @returns {Object} The style value
 */
dom.css = function(element, options) {
  if (typeof options === 'string') {
    return getStyle(element, options);
  }
};

Set isn’t implemented yet of course. The chained hooks look like this:

turing.domChain = {
  /**
   * Get or set styles.
   *
   * @param {Objects} options Either options for a style to set or a property name
   * @returns {Object} `this` or the style property
   */
  css: function(options) {
    if (typeof options === 'string') {
      return this.elements.length > 0 ? getStyle(this.elements[0], options) : null;
    } else {
      for (var i = 0; i < this.elements.length; i++) {
        dom.css(this[i], options);
      }
    }
    return this;
  },

Conclusion

On the surface reading CSS properties looks easy, but it takes some effort to create a consistent API across browsers. A production framework would go deeper than this implementation — take a look at jQuery’s CSS module for more details.

This week’s code was commit 9dc312f.

References


blog comments powered by Disqus