Let's Make a Framework: CSS Manipulation

17 Mar 2011 | By Alex Young | Tags frameworks tutorials lmaf documentation dom css

Welcome to part 54 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 took a look at how jQuery manipulates the DOM using $.html(). This week I’m going to discuss how jQuery lets us get and set CSS properties with .css.

jQuery’s .css() Method

The .css() method can both get and set CSS values on attributes. The reason why this is useful is — you’ve guessed it — browser incompatibilities. Standards-compliant browsers provide the getComputedStyle method, while Internet Explorer uses currentStyle and runtimeStyle.

jQuery also returns the correct value for both CSS and DOM naming schemes: .css('background-color') will return the same value as .css('backgroundColor').

Even though accessing an element’s style property, jQuery’s API provides a more consistent and less error-prone way of programming.

Internals

jQuery groups this functionality into css.js. Getting values uses jQuery.css(elem, name), while setting uses jQuery.style(elem, name, value).

jQuery.css

The first thing to note is the camelCase method, which is used quite early on:

css: function( elem, name, extra ) {
  // Make sure that we're working with the right name
  var ret, origName = jQuery.camelCase( name ),
    hooks = jQuery.cssHooks[ origName ];

This is a very simple method that uses a regular expression to transform dashed strings to DOM properties:

// rdashAlpha = /-([a-z])/ig
// fcamelCase = function( all, letter ) {
//   return letter.toUpperCase();
// };

camelCase: function( string ) {
  return string.replace( rdashAlpha, fcamelCase );
}

Which works like this:

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

>> 'backgroundColor'

This value is kept and referred to as originName. This is then used with cssProps which helps deal with special cases. The “hooks” refer to CSS hooks, which is jQuery’s way of making CSS getting and setting more extensible.

// cssProps: {
//  "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
// },

// Back in the CSS function:
name = jQuery.cssProps[ origName ] || origName;

// If a hook was provided get the computed value from there
if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
  return ret;

// Otherwise, if a way to get the computed value exists, use that
} else if ( curCSS ) {
  return curCSS( elem, name, origName );
}

If none of the hooks match, curCSS is called. This is actually a reference to the getComputedStyle or currentStyle method used to extract CSS values. jQuery wraps these methods with its own functions to make them behave more consistently. For example, the currentStyle function converts sizes to pixels based on a hack written by Dean Edwards in 2007.

Both of the calculated names are passed to getComputedStyle and used to extracted the value from a query against document.defaultView.getComputedStyle (or IE’s equivalent).

jQuery.style

The .style method sets style properties, and works with various signatures:

$('selector').css({ 'background-color': 'blue' });
$('selector').css('background-color', 'blue');

The first thing it does is checks the element is valid for this type of operation:

// Don't set styles on text and comment nodes
if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
  return;
}

We’ve seen nodeType used like this frequently in this series. The next part looks a lot like .css():

// Make sure that we're working with the right name
var ret, origName = jQuery.camelCase( name ),
  style = elem.style, hooks = jQuery.cssHooks[ origName ];

name = jQuery.cssProps[ origName ] || origName;

When a value to set is present, some interesting checks are performed:

// Make sure that NaN and null values aren't set. See: #7116
if ( typeof value === "number" && isNaN( value ) || value == null ) {
  return;
}

// If a number was passed in, add 'px' to the (except for certain CSS properties)
if ( typeof value === "number" && !jQuery.cssNumber[ origName ] ) {
  value += "px";
}

// If a hook was provided, use that value, otherwise just set the specified value
if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {
  // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
  // Fixes bug #5509
  try {
    style[ name ] = value;
  } catch(e) {}
}

Other than the weird bugs worked around with the if statements, writing styles simply boils down to mapping to a camel case name then writing the element’s property.

Conclusion

jQuery, and frameworks like it, give us a seamless way of getting and setting style properties. It would be a fairly simple task if it wasn’t for those pesky browser bugs and incompatibilities.


blog comments powered by Disqus