Saturday, February 8, 2014

Polymers that Accept External CSS


Polymer is a fantastic horizontal code tool. Unlike Angular and its brethren which carve out vertical slices (think URLs) in an application, Polymer carves out brilliant little isolated sections on a page. Wherever Polymers are used, they create new, blank shadow DOMs in which to play—blissfully unaware of any craziness that might be occurring on the rest of the page. Polymers can be used in Angular, Ember, Backbone and plain old pages with equal effectiveness because they only operate in their tiny little shadow DOM.

But, if they are so isolated, how do they end up looking like the rest of the application?

Really, I am wondering how Polymers might solve the window toolkit problem that has plagued so many windowing toolkits over the years: how do you go about creating a window toolkit widget that looks like the application in which it is used, not like the window toolkit. As I found last night, if the Polymer is intended for reuses only with a single website, it is possible for the site-specific Polymer to pull in the CSS from the site. This also applies if the Polymer is intended to be reused with a common CSS, such as Bootstrap.

But what if you want to create a new, custom HTML element with Polymer that is a bit more general? What if you wanted, say, to create the ultimate pizza builder HTML tag that could be used on any pizza related site, regardless of CSS framework, local styles, etc?

I know that things like jQuery UI use themes for this problem. To my eye, these never look quite right. A writing teacher once told me that reading from one paragraph of mine to the next was like riding in a manual transmission car with someone who couldn't drive stick shift. Eventually you get to where you need to be, but the ride along the way is filled with many unexpected, jarring jerks. Maybe I'm just too fussy, but that's how themed widgets look to me.

To avoid the “bad stick shift” visuals, I inevitably wind up customizing the theme. That may be fine with jQuery UI and other “light DOM” toolkits, but how do you style something in the shadow DOM? The cat and hat selectors (if they become widespread) might be one way. Is it possible to include styles directly inside the custom element tags instead? Something like:
    <pricing-plans>
      <link type="text/css" rel="stylesheet" href="css/mine.css">
      <style>
        p { border: 5px solid orange }
      </style>
      <pricing-plan name="Multi-Language"><!-- ... --></pricing-plan>
      <pricing-plan name="I Only Like One"<!-- ... --></pricing-plan>>
      <pricing-plan name="Extras" type="primary"><!-- ... --></pricing-plan>
    </pricing-plans>
Content inside a Polymer tag like that is distributed into the Polymer's <content> tag:
<polymer-element name="pricing-plans">
  <template>
    <div class="container">
      <div class="col-md-12 col-sm-4">
        <content></content>
      </div>
    </div>
  </template>
  <!-- ... -->
</polymer-element>
The problem is the word “distributed.” That content—the CSS and the individual <pricing-plan> elements—are not actually inserted into the Polymer's DOM, just rendered there. Still, if “distributed” means that the styles are applied, then this ought to work.

It does not:



Neither the <link> nor the <style> tags are included in the shadow DOM and, certainly, none of the paragraphs have large orange borers around them. Bother.

I am not quite out of luck, however. I can expose a css attribute on the Polymer:
    <pricing-plans css="/assets/cat_hat/mine.css">
      <pricing-plan name="Multi-Language"><!-- ... --></pricing-plan>
      <pricing-plan name="I Only Like One"<!-- ... --></pricing-plan>>
      <pricing-plan name="Extras" type="primary"><!-- ... --></pricing-plan>
    </pricing-plans>
Then, in the Polymer Dart code, I can add a <style> element that imports that style into the Polymer's shadow DOM:
@CustomTag('pricing-plans')
class PricingPlansElement extends PolymerElement {
  @published String css;
  PricingPlansElement.created() : super.created();
  enteredView() {
    super.enteredView();
    if (css != null) {
      shadowRoot.append(
        new StyleElement()..text = "@import '${css}';"
      );
    }
  }
}
Or the equivalent JavaScript version:
    Polymer('pricing-plans', {
      enteredView: function() {
        if (this.css) {
          var ss = document.createElement("style");
          ss.textContent = "@import '" + this.css + "';";
          this.shadowRoot.appendChild(ss);
        }
      }
    });
With that, I can affect the styles of my Polymer from the outside—even if my particular tastes leave something to be desired:



For completeness sake, I note that the equivalent <link> import of the same CSS was silently ignored.

This approach requires some effort, but, now that I have all the pieces in place, not as much as I feared. This is still more of an edge case. I would imagine that folks creating shareable Polymers across sites would be more than willing to add something like this to their Polymers (along with an example or two). I tend to view sharing Polymers within a site as the 80% case and that already works out of the box.

Day #1,021

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. (edit: fix broken html)

    Hi Chris! Very interesting post. I think the work in shadow-styling spec will make this all a lot easier: http://dev.w3.org/csswg/shadow-styling/ ... in particular /shadow and /shadow-all can be used to apply a theme across the page, and /content can be used if you want to style the distributed nodes at a content tag.

    ReplyDelete
    Replies
    1. Ooh! That'll do exactly what I want, thanks for pointing it out!

      It doesn't appear to work in Chrome canary yet, not even the “Web Platform” chrome://flags setting. Hopefully it'll come along soon so that I can play with it. Thanks!

      Delete