Let's Make a Framework: Writing CSS Properties
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:
- Check the element has the right
nodeType - Get the CSS property name, correcting case
- Make sure that
NaNandnullvalues aren’t set - If a number was passed in, add
pxto the value (except for certain CSS properties) - Write the value using the element’s
styleproperty
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.