Search code examples
javascripthtmlcssweb-componentshadow-dom

webcomponents.js Shadow DOM :host selector does not work on Safari and Firefox


I tried making my own web component using the polyfill from https://github.com/webcomponents/webcomponentsjs

Here's my code:

im-list.html

<template id="im-list-temp">
  <style>
    :host {
      list-style-type: none;
      margin: 0;
      padding: 0;
    }
  </style>
  <content> </content>
</template>

<script>
  var currentScript = document._currentScript || document.currentScript;
  var proto = Object.create(HTMLUListElement.prototype, {
    createdCallback: {
      value: function() {
        var doc = currentScript.ownerDocument;
        var t = doc.querySelector("#im-list-temp");
        var clone = doc.importNode(t.content, true);
        var root = this.createShadowRoot();
        root.appendChild(clone);
      }
    }
  });
  document.registerElement('im-list', {
    prototype: proto,
    extends: 'ul'
  });
</script>

index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="bower_components/webcomponentsjs/webcomponents.js"></script>
    <link rel="import" href="./components/im-list.html" />
    <title>List Test</title>
  </head>
  <body>
    <ul is="im-list">
      <li>Blubb</li>
      <li>Blubb Blubb</li>
    </ul>
  </body>
</html>

This code works fine in Chrome (43.0.2357.81) but it doesn't work in Firefox (38.0.1 and 40.0a2) and Safari (8.0.6). In FF and Safari the <style> is just added to the normal DOM.

enter image description here


Solution

  • GitHub Repository

    octocat I put this and other snippets on github. Feel free to fork and improve.


    The problem is that webcomponets.js doesn't "fix" the style to be used on browsers that lack native ShadowDOM support. That is, it does not make the browser able to understand selectors like :host.

    The way Polymer solve it, is by rewriting the style.

    So, under Polymer, this:

    :host ::content div
    

    Becomes this:

    x-foo div
    

    Therefore, to have that under a VanillaJS component, is necessary to manually do that.

    Here is an snippet I use to create the Shadow Root and rewrite the styles only on browsers that use webcomponents.js instead of native Shadow DOM:

    var addShadowRoot = (function () {
      'use strict';
      var importDoc, shimStyle;
    
      importDoc = (document._currentScript || document.currentScript).ownerDocument;
    
      if (window.ShadowDOMPolyfill) {
        shimStyle = document.createElement('style');
        document.head.insertBefore(shimStyle, document.head.firstChild);
      }
    
      return function (obj, idTemplate, tagName) {
        var template, list;
    
        obj.root = obj.createShadowRoot();
        template = importDoc.getElementById(idTemplate);
        obj.root.appendChild(template.content.cloneNode(true));
    
        if (window.ShadowDOMPolyfill) {
          list = obj.root.getElementsByTagName('style');
          Array.prototype.forEach.call(list, function (style) {
            if (!template.shimmed) {
              shimStyle.innerHTML += style.innerHTML
                .replace(/:host\b/gm, tagName || idTemplate)
                .replace(/::shadow\b/gm, ' ')
                .replace(/::content\b/gm, ' ');
            }
            style.parentNode.removeChild(style);
          });
          template.shimmed = true;
        }
      };
    }());
    

    Copy-paste that on your component.

    Next, you need to call this function inside your createdCallback, like:

    addShadowRoot(this, "im-list-temp", "ul[is=im-list]");
    

    Here is an example with your code