Let's Make a Framework: Writing CSS Properties

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

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

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

I had some feedback on last week’s commit from John-David Dalton which can be viewed here: commit 9dc312. The changes he suggested help keep behaviour relative to the element’s document rather than the global one. I tested both suggestions in IE/Firefox/WebKit.

Writing CSS

Last week we were doing this sort of thing:

turing('#dom-test').css('background-color');

Now CSS properties can be read, how about writing them? I discussed how jQuery implements writing styles back in part 54. The algorithm works like this:

  1. Check the element has the right nodeType
  2. Get the CSS property name, correcting case
  3. Make sure that NaN and null values aren’t set
  4. If a number was passed in, add px to the value (except for certain CSS properties)
  5. Write the value using the element’s style property

This process is actually fairly simple. Even the cases where px shouldn’t be added are properties that aren’t usually manipulated:

// Exclude the following css properties to add px
cssNumber: {
  "zIndex": true,
  "fontWeight": true,
  "opacity": true,
  "zoom": true,
  "lineHeight": true
}

It’s easier to pass numbers rather than remembering to use ‘10px’. The reason this is an object with properties set to true is to make it easy to use: jQuery.cssNumber[property] is nice and succinct.

Tests

These are the tests I want to pass:

'test writing style properties': function() {
  var element = turing.dom.get('#dom-test')[0],
      expected = element.currentStyle ? '#f5f5f5' : 'rgb(245, 245, 245)';

  turing.dom.css(element, { 'background-color': expected, 'width': 1000 });

  assert.equal(turing.dom.css(element, 'background-color'), expected);
  assert.equal(turing.dom.css(element, 'backgroundColor'), expected);
  assert.equal(turing.dom.css(element, 'width'), '1000px');
},

'test chained writing style properties': function() {
  var element = turing.dom.get('#dom-test')[0],
      expected = element.currentStyle ? '#f1f1f1' : 'rgb(241, 241, 241)';

  turing('#dom-test').css({ 'background-color': expected });

  assert.equal(turing('#dom-test').css('background-color'), expected);
  assert.equal(turing('#dom-test').css('backgroundColor'), expected);
}

As usual I’m testing both the “modular” API and chained API. The first test includes a numerical value that should automatically get px set. I haven’t tested all the edge cases because I don’t think it would help you learn anything about DOM programming.

Implementation

The original dom.css method I defined already detected cases where styles should be written, so I’ve added a loop to iterate over an object with a list of styles:

/**
 * 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);
  } else {
    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        setStyle(element, property, options[property]);
      }
    }
  }
};

Next, setStyle’s implementation is dependent on browser:

if (document.documentElement.currentStyle) {
  getStyle = function(element, property) {
    return element.currentStyle[camelCase(property)];
  };

  setStyle = function(element, property, value) {
    return setStyleProperty(element, camelCase(property), value);
  };
} else if (document.defaultView.getComputedStyle) {
  getStyle = function(element, property) {
    return element.ownerDocument.defaultView.getComputedStyle(element, null).getPropertyValue(uncamel(property));
  };

  setStyle = function(element, property, value) {
    return setStyleProperty(element, uncamel(property), value);
  };
}

The currentStyle detection lets us determine if we should always camelCase properties or not. Next, setStyleProperty does the real work:

function setStyleProperty(element, property, value) {
  if (invalidCSSNode(element)) {
    return;
  }

  if (typeof value === 'number' && !cssNumericalProperty[property]) {
    value += 'px';
  }

  element.style[property] = value;
}

This uses cssNumericalProperty which is the same as jQuery’s cssNumber object. I also created invalidCSSNode to detect if the element’s style can be written to:

function invalidCSSNode(element) {
  return !element || element.nodeType === nodeTypes.TEXT_NODE || element.nodeType === nodeTypes.COMMENT_NODE || !element.style;
}

I’ve used the nodeTypes object again to make this more readable.

Conclusion

The tests pass in IE, Firefox, and Chrome/Safari, so I’m happy. It would be possible to take reading and writing CSS properties a lot further than I have here — colour values could be unified across browsers, and maybe even the document’s stylesheets could be manipulated.

This week’s code is commit c6a2ee8.


blog comments powered by Disqus