A Thunderbolt for a developer! Do you know any good developers based in The Netherlands? Tell them about working at Springest. If we hire them, you will get a 27" Apple Thunderbolt Display. :)

Setting CSS in JavaScript Widgets

At Springest we have two widgets that can be embedded. Partners can use our search widget, which lets people search in our database. We also have a review widget, that institutes can configure in our admin and embed into their site.

There are basically two ways of embedding a widget: via a generated iframe (like Facebook does), or with generated DOM elements.

There are a few pros and cons for both methods. With the iframe version you don’t have to worry about conflicting styles, and requests to the server can be done without having to worry about cross domain Ajax requests. However, you only get a small portion of the viewport, with a fixed width and height. If the content is bigger, you will get scrollbars.

Generated DOM elements do interfere with the stylesheets used on the webpage, but in return you get full control over the web page, and can open tabs, lightboxes et cetera. A big disadvantage of this method is that you will need to use JSONP (it calls a function you specify, with the returned data as the argument), in order to make your widget work in all browsers including IE6.

We use the latter method, which brings us to the subject of this blogpost:

Initially setting CSS with Javascript in all browsers

My original idea was to set all possible basic styles like color and background-color with JavaScript upon initialization. But what if the page’s stylesheet contains lines that have !important appended to them? The JS-set styles, treated by the browser like inline styles, would be overridden due to the higher priority set with the !important. I solved that issue by simply appending !important to all of the styles I set to the elements using JS.

So far, so good. I tested it in Firefox and Chrome, and it worked like a charm. It also worked in IE7 and 8, because they implement !important quite correctly. Since IE6 is a total bitch, however, I had to filter out all the !importants for IE6 clients.

This works something like the following in the JS (the styles string is rendered dynamically by PHP upon every request):

/** browser detection; set var to true if client doesn't properly support !important **/
var isIE6 = /*@cc_on @_jscript_version <= 5.6 ? true : false || @*/ false;

/** takes a string and strips any occurences of !important if isIE6 == true **/ 
function filterImportant(styles) {
    return isIE6 ? styles.replace(/\!important/g, '') : styles;
};

/** we do this for each element we create **/
var elm = document.createElement('div');
    elm.style.cssText = filterImportant("WidgetCss->defineAll(array('background-color'=>'#FFF', 'color'=> '#000')) ?>");

Our WidgetCss CakePHP Helper can be found in this gist. It basically returns a string with all CSS defaults, except for those set with the array you pass as the argument. It also does some nifty tricks for border-radius and margin that enable the use of shorthands in the array (i.e. feeding ‘margin’=>‘5px’ to the function creates margin-top:5px;margin-right:5px etc. in the output). For each element you use in the widget, you can set the style.cssText property by calling the filterImportant function in order to make it work in IE6. (isIE6 is a boolean set to true only in Internet Explorer up to and including version 6.)

This works like a charm. However, I want to be able to overwrite certain styles later on. For example, when the width of the widget is 100%, and the user resizes their window, I want to modify the position of the buttons and text. Or, in our review widget, I want reviews to fade in and out in a repeating loop.

Overwriting the styles with JavaScript

First I tried overwriting the styles this way:

elm.style.color = 'red !important';

This works fine in Firefox and IE9. IE up to 8 does not like this, so I changed the isIE6 variable to replace all !importants in the css function for IE8 and below (setting that JScript version from 5.6 to 5.8). So in the final code for our widget, we changed isIE6 to isIE8, and check for JScript version 5.8 or lower. The only caveat of this: the layout might break in IE6-8 if the webpage uses !important on a style for a very generic selector that matches elements inside the widget.

The code was live for a couple of months, but a few weeks ago Chrome 18 went into beta. Chrome 18+, for some reasons, does not set the new style. After a little research I found there is a JavaScript method, part of the style object of an element: setProperty. This method accepts three arguments: the property, the desired value, and the string ‘important’ or null if you either want to set the style with !important or not.

This is how that looks:

elm.style.setProperty('color', 'red', 'important');

Only minor disadvantage is: the property must be of the following format: background-color, and not backgroundColor or WebkitBorderRadius. So I changed all my code where I set the styles. It only broke when I tested in IE6-8. IE6-8 actually wants me to set all styles like this: elm.style[‘backgroundColor’] = ‘red’.

In Firefox it doesn’t matter, both methods work.

This all is just frigging annoying.

Luckily it’s just one simple regular expression to transform background-color to backgroundColor. So here is the complete JavaScript function I use to set a style:

var isIE8 = /*@cc_on @_jscript_version <= 5.8 ? true : false || @*/ false;

function setCss() {
    if (arguments.length == 3) {
        var obj = {};
            obj[arguments[1]] = arguments[2];
    } else {
        var obj = arguments[1];
    }
    
    var elm = arguments[0],
        val;
    
    for (var prop in obj) if (obj.hasOwnProperty(prop)) {
        val = obj[prop];
        
        if (prop == 'float') {
            elm.style['cssFloat'] = val;
            elm.style['styleFloat'] = val;
            prop = 'cssFloat';

            if (!isIE8 && typeof elm.style.setProperty != 'undefined') {
                elm.style.setProperty('float', val, 'important');
            }
        }

        if (isIE8 && prop.indexOf('-') != -1) {
            prop = prop.replace(/(\-[a-z])/g, function(match) {
                return match.substring(1).toUpperCase();
            })
        }

        elm.style[prop] = val;

        if (!isIE8 && typeof elm.style.setProperty != 'undefined') {
            elm.style.setProperty(prop, val, 'important');
        }
    }
};

It accepts two or three arguments. In case of two, the second argument should be an object with properties as keys and values. If there are three arguments, the second is a single property and the third its value.

It loops over all property-value pairs. There is a small trick to setting floats with JavaScript: use cssFloat for normal browsers, and styleFloat for Internet Explorer. The setProperty method just wants float as the property. Fuck yeah, browser differences!

Then it checks if it’s IE6-8, and if there is a dash in the property. With a regular expression replace it transforms the dashed property into a camelCased one. Then it checks for good browsers (IE9, Firefox, Chrome, etc) and uses the setProperty method to make it work.

The complete function took me a while to write, since there are so many different ways the browsers work. I think it’s quite bulletproof, so go ahead and use this if you write your own widgets without using a library like Mootools or jQuery. (Our complete widgets are about 25kB in size, so including a library for shorter code is not worth the overhead of downloading the library itself. Also: jQuery and Prototype can interfere with libraries on the page.)

Quite some words you’ve just read, but you’ve read them! Thanks and good luck coding awesome widgets!

Comments