Widget Theming

After Raj's discovery of Shadow DOM triggered company-wide conversion to properly incapsulated widgets, Howard—not to be outdone—gleans another great opportunity in this change. He realizes that with the help of scoped stylesheets and shadow DOM, he can separate the general framing of the widgets from the theming code. For years now, it's been bugging him that whenever the widget's author wanted to tweak the general layout of their widgets, they would have edit the /src/ui/layout/widgets.css stylesheet and muck with their gubbins of code there, creating a waterfall of lines that looks like this:



/* General Widget properties */

.widget { … }

/* Contacts Widget layout. All rules must start with .contacts.widget  */

.contacts.widget ul {
    margin: 0;
    list-style: none;
    …
}

.contacts.widget ul li { … }

.contacts.widget ul li img.photo { … }

.contacts.widget ul li span.n { … }

…

/* Activity Stream Widget layout. All rules must start with .activity.widget */

.activity.widget ul { … }

.activity.widget li { … }

.activity.widget li.shared { … }

…

Then, a separate stylesheet in the /src/ui/themes/ directory is used to apply colors, backgrounds, and all those wonderful barnacles that typically comprise a theme. Despite strictly enforced naming conventions, the structure is somewhat brittle and, on occasion, one rule clobbers another, subtly breaking the beautiful house of cards, causing users grief—and throwing Howard into panic mode. Panic mode gets old quickly.

Moving widget layout styles to scoped stylesheets next to respective widgets goes a long way to make layout less brittle. Further, putting these scoped styles inside each widget's shadow DOM subtree ensures that the these styles are contained to the widgets. Sure, it's a bit of a refactoring, but that beats arguing with mom. Howard goes to work. An hour later, the /src/ui/layout/widgets.css stylesheet is drained and all widgets look like this (/src/ui/widgets/contacts.js, for example):


var ui = ui || {};
ui.widgets = ui.widgets || {};

(function() {
    var MAX_USERS = 8;

    function Contacts()
    {
        this.element = document.createElement('div');
        this.element.className = 'contacts widget';
        var shadow = new ShadowRoot(this.element);
        shadow.applyAuthorStyles = true;
        var style = document.createElement('style');
        style.scoped = true;
        style.textContent = this.scopedStyleText;
        shadow.appendChild(style);
        var users = shadow.appendChild(document.createElement('ul'));
        this.loadUsers(MAX_USERS, function(user) {
            var li = document.createElement('li');
            // Populate [li] with data from the [user] object.
            // …
        });
    }

    // …

    // Styles moved from /src/ui/layout/widgets.css, minus ".contacts.widget" selector.
    // Quasi-literals are awesome! (http://wiki.ecmascript.org/doku.php?id=harmony:quasis)
    Contacts.prototype.scopedStyleText = `ul {
        margin: 0;
        list-style: none;
        
        }
        ul li { … }
        ul li img.photo { … }
        ul li span.n { … }
        …`;

    // …

    ui.widgets.Contacts = Contacts;

})();

Whoa!—Howard sees another way to improve robustness of the layout system. The applyAuthorStyles flag (which is false by default) controls whether document stylesheets apply in the shadow DOM subtrees. Thus removing a line of code ensures that the widget's styles are never messed with:


this.element.className = 'contacts widget';
var shadow = new ShadowRoot(this.element);
shadow.applyAuthorStyles = true;
var users = shadow.appendChild(document.createElement('ul'));

The celebration is cut short as Howard notices that the stylesheets from /src/ui/themes/ directory were also part of the document stylesheet. As it stands, there is no way to apply theming styles to the widgets…

Or maybe there is. CSS Variables to the rescue! Upon reading the spec, Howard does a little dance. Not only they allow widget theming, the method is such that you can specify precise and named points where theming is allowed. Digging into the Contacts widget, Howard documents the following theming points:

Howard mixes the variables into the scopedStyleText string:


// Styles moved from /src/ui/layout/widgets.css, minus ".contacts.widget" selector.
// Quasi-literals are awesome! (http://wiki.ecmascript.org/doku.php?id=harmony:quasis)
Contacts.prototype.scopedStyleText = `ul {
    margin: 0;
    list-style: none;
    font: data(font);
    color: data(color);
    background: data(background);
    …
    }
    ul li {  … }
    ul li img.photo {
    border: 1px solid data(color);
    …';

Now to tweak the actual themes, starting with /src/ui/themes/purple-and-black.css:



…

/* Contacts Widget Theme  */

.contacts.widget ul {
    color: black;
    background: purple;
}

.contacts.widget ul li img.photo {
    border-color: black;
}

.contacts.widget {
    data-color: black;
    data-background: purple;
}

Whoohoo! No more reaching into widgets to style them! No more breakages when widget authors change their widgets and forget to update the theme! After all themes are converted, the Socia1eet looks and works same as before. But on the inside, the brittle layout and theming system has been replaced with a flexible, elegant, and robust solution.