DailyJS

Let's Make a Framework: Writing CSS Properties

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: Writing CSS Properties

Posted by Alex R. Young on .
Featured

tutorials frameworks css lmaf dom documentation

Let's Make a Framework: Writing CSS Properties

Posted by Alex R. Young on .

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
.