Friday, June 13, 2014

Dynamically Adding External Styles to Polymer Elements


I am still trying to dynamically apply page styles inside a Polymer element. Without this capability, things like native elements (buttons, drop downs, etc.) will default to native browser styles as in the pizza toppings choosers at the bottom of my <x-pizza> custom element:



Styling Polymer elements is easy—just put some CSS in the <template> part of the Polymer element definition. All the usual ways to style work, including @import, <link>, and <style>. How they work is the question to which I still lack a good answer. The hope is that, once I have that answer, I will be able to dynamically pull in external CSS—like that from the containing page.

I believe that last night's Platform.ShadowCSS was useful, but likely not the right path. Tonight, I take a closer look at what Polymer and its associated polyfills are doing. Specifically, how are they applying CSS from external sources? To answer that, I start with a <link> stylesheet tag inside my Polymer <template> definition:
<link rel="import"
      href="../bower_components/polymer/polymer.html">
<polymer-element name="x-pizza">
  <template>
    <link type="text/css" rel="stylesheet" href="/assets/bootstrap.min.css">
    <!-- Actual Polymer template definition here... -->
  </template>
  <script src="x_pizza.js"></script>
</polymer-element>
As expected, that has the desired behavior. Manually defining CSS works fine with Polymer, but is of limited use in this case. I might think my <x-pizza> element looks good with Bootstrap styles:



But potential designers might have other ideas. If I want my element to gain widespread support, it cannot rigidly adhere to one look. Note: this used to be the purview of the since deprecated applyAuthorStyles.

Anyhow, if I inspect the <link> tag in my custom element's shadow DOM, I find… that it is gone. In its place is an inline <style> tag that contains the contents of the external stylesheet:



Interesting. So something in Polymer is loading that stylesheet and using it to replace the original <link> tag. That sounds like exactly what I need—only I need to be able to run whatever that is after the normal Polymer initialization.

Taking a closer look at the JavaScript console, I see:
...
XHR finished loading: GET "http://localhost:8000/bower_components/polymer/polymer.html". Loader.js:171
XHR finished loading: GET "http://localhost:8000/bower_components/polymer/layout.html". Loader.js:171
XHR finished loading: GET "http://localhost:8000/assets/bootstrap.min.css". loader.js:87
...
(I have enabled “Log XMLHttpRequests” in Chrome's console settings)

Furthermore, if I expand the details of the XHR request, I find:
XHR finished loading: GET "http://localhost:8000/assets/bootstrap.min.css". loader.js:87
b.xhr loader.js:87
b.fetch loader.js:66
b.process loader.js:40
b.resolve styleloader.js:25
b.resolveNode styleloader.js:34
b.loadStyles styleloader.js:61
n.loadStyles styles.js:36
d.loadResources polymer-element.js:113
d.init polymer-element.js:36
d.createdCallback polymer-element.js:27
k CustomElements.js:294
h CustomElements.js:247
s CustomElements.js:397
h Observer.js:85
...
y Observer.js:314
d.parse Parser.js:34
d.parseImport Parser.js:47
d.parseLink Parser.js:42
(anonymous function) Parser.js:30
d.parse Parser.js:29
b boot.js:11
(anonymous function) boot.js:33
c HTMLImports.js:220
d HTMLImports.js:225
k.parseImport Parser.js:104
k.parseLink Parser.js:123
k.parse Parser.js:55
k.parseNext Parser.js:44
d Parser.js:149
That is quite a stacktrace (and I even cut a bunch of observable stacks out). But I focus my efforts on the loadResources() and loadStyles() portions of the stack. Although the latter seems the most promising (view the method on GitHub), it does not work for me because the fetchTemplate() method always returns undefined. Perhaps this is because the polyfill code has already fetched the template by this time.

But as luck would have it, I happen upon convertSheetsToStyles() (view the method on GitHub). This, like loadStyles(), is a method on PolymerElement, not the Polymer in my backing definition:
Polymer('x-pizza', {
  // ...
  attached: function() {
    this.addExternalCss();
    // ...
  },

  addExternalCss: function() {
    var link = document.createElement('link');
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.href = '/assets/bootstrap.min.css';
    this.shadowRoot.appendChild(link);
  },
  // ...
});
The PolymerElement is available in the element property. So I add a call to its convertSheetsToStyles() method after I have dynamically added the stylesheet <link> to the element's shadowRoot:
  // ...
  addExternalCss: function() {
    var link = document.createElement('link');
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.href = '/assets/bootstrap.min.css';
    this.shadowRoot.appendChild(link);

    this.element.convertSheetsToStyles(this.shadowRoot);
  },
  // ...
That does not result in an XHR request to load the contents of the stylesheet. But it does replace the <link> with an @import equivalent:



And that actually works. All of the native elements in <x-pizza> now have Bootstrap styles applied to them. Even better, this works in Firefox and even Internet Explorer:



That is pretty cool.

I would imagine that the @import is converted by Polymer into a full-blown inline <style> to better prevent FOUC in Polymer elements (something that Polymer is very good about). But I do not really have to worry about FOUC in this case—the stylesheet would have already been loaded in the <head> of the original document. This approach pulls the same stylesheet into the shadow DOM via @import, but no second request is needed—it is already in browser cache.

I feel a little bad about reaching under the PolymerElement covers like that. But not too bad!



Day #92


No comments:

Post a Comment