Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Polyfilling CAS

Last updated:

In a previous post, I introduced the CAS language, which let you set attributes on elements using CSS-like syntax. No browser seems interested in implementing it right now, though. Luckily, it's a reasonably easy thing to polyfill. I don't have the energy to actually write it right now, but I can at least sketch out how someone else would do it, and hope that one of you will fill it in for me.

First, you'll need a generic CSS parser. My parser will work fine, but you can use whatever you'd like.

Once you've parsed the stylesheet into a JS structure of style rules, then loop through each style rule. For each one, compute the specificity of the selector, and hold onto it. Then, run the selector through document.querySelectorAll() to get the list of matched elements.

Now, you've got a few choices for what to do next. If we assume that JS Maps exist (they do in FF), this is easy. We're going to make a data structure that starts with a Map with elements as keys, and as values another Map that uses property names as keys, and [value, specificity] pairs as values. For each element that the selector matched, check if it's in the Map already, and if it's not, set it up with a default value (empty Map). Then, for each property in the style rule, check if the property already exists in the sub-Map. If it does, compare its specificity with the specificity of your style rule, and replace the value/specificity if your new one is larger. If it doesn't exist yet in the sub-Map, just add it along with its specificity.

Once you've gone through every style rule, adding all the properties to every element, you're left with a Map of elements, each with a list of attributes and values. Iterate through this, calling setAttribute() or removeAttribute() as appropriate. If you want to support the listen() extension, just call addEventListener() at this point too.

If you don't have JS Maps, don't fret, as there's still a reasonable polyfill. Instead of using Maps, we'll just store the information on the element directly, in an expando. You don't even need to set data attributes or anything, just add a new JS property directly to the element that contains an object that'll hold the property names from the style rules. Then, rather than iterating through the element Map (since you don't have one), iterate through the whole document, checking for that property for elements that you need to run attributes for.

Then, set up a MutationObserver or MutationListener, listening for new elements being added to the DOM. When they are, run through all the selectors, seeing if they match the element, and if so doing the same "set the property + specificity if it's higher" thing, then just immediately running the attribute code when you're done with that element.

That's it! That'll give you a fully-functioning CAS polyfill with no missing powers, at a reasonable runtime cost.

(a limited set of Markdown is supported)