New to Dojo 1.2+ is dojox.html.style
. This is a collection of methods which give you the ability to query the stylesheets collection in a document, add and remove rules, and dynamically create new sheets. In this article I’ll explain why and where this is of use, and walk through a split-panel demo that uses dynamic stylesheets to size a 2 column layout.
What is a Dynamic Stylesheet
The concept is simple. Most times CSS is static. You define a set of rules with selectors to match certain elements and declarations to assign style property values to that element. The rules are predefined, in a document (embedded) or external stylesheet which is loaded along with the other static resources for the page. The rules may match existing elements in the page, or during the lifetime of the page you may add new elements that match, or add classes to elements to cause them to match. The CSS itself doesn’t change though.
There are times, however, when the CSS can’t be static. It could be a width value that needs to be calculated at runtime based on viewport width. Or perhaps you need to allow arbitrary color or font-size to be set by the user via an on-screen picker. For example, the page could go through radical changes as user preferences are loaded, or the user interacts with the user interface. If the permutations are numerous enough to preclude bundling up CSS rules for every contingency, you have a few choices:
- Go around the stylesheets, and set style directly on the elements from JavaScript
- Generate the stylesheet on the server and reload from the client-side
- Generate or manipulate the stylesheet with JavaScript
Server-side generated CSS can be simple: CSS is just text, and as long as you send the response with a text/css mime-type, you can use all the templating goodness your server framework allows. But this is not a path most people want to go down, and even in the best case it’s more round trips to the server, albeit in the background. This can be slow, and rules out a whole slew of interesting use cases.
Setting property values directly to the node.style
object is by far the most common approach. All of the major JavaScript libraries provide a utility to make this as painless as it can be. Dojo has dojo.style
. It is simple and effective, but the technique has some repercussions. I want to explore those before discussing dynamic styles, as we’ve lived with these shortcomings so long we tend to forget they exist.
Why Dynamic Stylesheets?
The basic problem is that inline styles don’t play well with external or embedded CSS rules, and reduce the value of the Cascade in CSS down to near zero. Properties set directly on the node’s style object have much higher specificity than stylesheet rules—which means they’ll win out over rules in your stylesheets that specify the same property. This might be what you want, but if it isn’t, your only recourse is to use an !important
flag on those stylesheet rules. It can also be difficult to unset some properties set this way. Having set a property, you want to be able to undo the action to restore the element to its previous or default state. In some cases this is a simple matter: showing and hiding an element typically involves simply toggling between display: block
and display: none
. But what if a stylesheet had set that element to display: inline-block
? When you start looking at properties like margin, padding, background, font
—it becomes more complicated still. Chances are high that these properties have been set in a stylesheet, and even if they haven’t, knowing the right “default” value to set can be tricky. You can store the value before you override it (ugh, that’s a lot of book-keeping) or start string munging in node.style.cssText
.
Remember, we’re talking about cases where adding and removing predefined classes is not an option. An example might help. Imagine a split panel, with a draggable divider. When the page first loads, the panels and the divider are rendered using styles provided by a stylesheet. When the user drags the divider, it moves, and width properties are set in the style attribute of each panel node. We’re now managing style, node-by-node, from JavaScript; the stylesheets don’t get a look-in. It’s not supposed to be that way.
Now, lets try the same scenario using a dynamic stylesheet. When the page loads, a dynamic stylesheet is created, using the id
of the container to prefix all rules. As the divider is dragged, we update the values in this stylesheet. To restore the values, we can either empty out this dynamic stylesheet, or just add or remove a class that will re-engage the original values by specificity. Does that really work? It does. Can it possibly be fast enough to keep up with a mouse move? It can.
How it Works
Let’s look at some code. The dyncssDemo
object gathers together the state information, properties and functions we’ll need. It’s not a Dijit widget, but clearly it could easily become one. Let’s focus on the specific code that gets the work done:
onMove: function(mover, leftTop) {
// summary: handle an onMove event from the divider
// make it a %age, btween 5 and 95%:
var contentWidth = this.containerWidth;
var pcent = Math.min(
this.maxLeft, Math.max(
this.minLeft,
parseInt(leftTop.l / contentWidth * 10000, 10) / 100
));
if(pcent !== this.lastLeft) {
// only update if the left value has actually changed
this.lastLeft = pcent;
this.updateContainer({
leftWidth: pcent+"%",
rightWidth: 100-pcent+"%",
containerId: this.containerId
});
}
}
This handler is connected to the move events that fire when you drag the nubbin. It calculates percent widths for the two columns, within some boundaries, and sends this along to updateContainer
:
function(props) {
var sheet = dojox.html.getDynamicStyleSheet(sheetName);
dojo.forEach(rules, function(rule) {
// sub in the actual values from the provided property bag
var declarationStr = dojo.string.substitute(rule.declarations.join(";\n"),props),
selector = dojo.string.substitute(rule.selector, props);
if(rulesMap[ selector ]) {
// removeCssRule needs to loop over the entire stylesheet
// to find a matching selector/delaration pair
// we minimize that cost by maintaining a hash of the rules we've added
// its then just a lookup to determine if matching rule exists already
// that needs to be removed to be redefined
// we already have rule of this name, remove it first
dojox.html.removeCssRule(selector, rulesMap[selector], sheetName);
}
var rs = dojox.html.insertCssRule(selector, declarationStr, sheetName);
// store it for fast lookup and later removal
rulesMap[selector] = declarationStr;
});
};
The props variable has those percent values. It loops over an array of rules (in scope here by closure—see the full source), and populates the placeholders in the selector and declaration strings with values from the dictionary object its been provided. While CSS obviously allows you to have multiple rules using the same selector, we don’t want that here, so we track and remove duplicate rules (with dojox.html.removeCssRule
) before adding the new ones (with dojox.html.insertCssRule
)
And that’s basically it. If you watch Firebug, you’ll see the new values being applied to each column element:
In the CSS tab, you can see the dynamic stylesheet created (it does not refresh in real time however—a quirk of Firebug—you’ll need to select the other stylesheet, then go back to see the changes):
The rest of the code wires it all up, and shows how you can engage rules in the traditional manner (by adding a class to the container) without jeopardizing your ability to also manage the same style properties from script.
The little + button in top right adds new rows. Interesting huh? Because that is all there is to do here—the stylesheet is already in place to match the new elements, and it is the native CSS engine that applies the styles. You can imagine a lot of ways you might get new rows in there. Click away, then re-try the column-size nubbin. The speed is good—we’re down purely to CSS engine performance to apply those styles, no JavaScript DOM-crawling or function call / property setting overhead.
Full Demo, updated courtesy of Tom Elam
Summary
There’s a lot of mileage in dynamic stylesheets. This is not a technique that will fit everywhere, but as you get comfortable with the idea of manipulating stylesheets instead of nodes, I’m sure you’ll start to find applications everywhere you look.
In the next article in this series I’ll dig more into the performance implications here, and show that not only can dynamic stylesheets be fast enough for day to day use, but can bring meaningful performance improvements to heavy pages.