DailyJS

Let's Make a Framework: DOM Manipulation Part 4

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 4

Posted by Alex R. Young on .
Featured

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: DOM Manipulation Part 4

Posted by Alex R. Young on .

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