Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using a non-shadow DOM custom element both inside and outside the shadow DOM

I have a custom element (without shadow DOM) that I'd like to be able to use anywhere, even inside another custom element that might use shadow DOM. However, I'm not sure how to get the styles working in both places.

For example, lets say I create a simple fancy-button element:

class fancyButton extends HTMLElement {
  constructor() {
    super();

    this.innerHTML = `
      <style>
      fancy-button button {
        padding: 10px 15px;
        background: rgb(62,118,194);
        color: white;
        border: none;
        border-radius: 4px
      }
      </style>
      <button>Click Me</button>`;
  }
}

customElements.define('fancy-button', fancyButton);
<fancy-button></fancy-button>

Inside a shadow DOM element, the inserted style tag will allow the fancy-button styles to work. However, if this component gets used outside of a shadow DOM element, the style tag will be duplicated every time the element is used.

If instead I add the style tag as part of the html import file, then the styles only work outside of the shadow DOM but at least they are only declared once.

<!-- fancy-button.html -->
<style>
fancy-button button {
  padding: 10px 15px;
  background: rgb(62,118,194);
  color: white;
  border: none;
  border-radius: 4px
}
</style>

<script>
class fancyButton extends HTMLElement {
  constructor() {
    super();

    this.innerHTML = `<button>Click Me</button>`;
  }
}

customElements.define('fancy-button', fancyButton);
</script>

What's the best way to add custom element styles that handles both being used inside and outside the shadow DOM?

like image 829
Steven Lambert Avatar asked Jul 12 '17 22:07

Steven Lambert


1 Answers

So I was able to find a solution thanks to Supersharp suggestions about checking if we're in the shadow DOM.

First you add the styles as part of the import file so that the styles apply outside of the shadow DOM by default. Then when element is added to the DOM, we check getRootNode() to see if it's been added to a ShadowRoot node. If it has, and the styles haven't already been injected into the root, then we can inject the styles manually.

var div = document.createElement('div');
var shadow = div.attachShadow({mode: 'open'});
shadow.innerHTML = '<fancy-button></fancy-button>';

document.body.appendChild(div);
<style data-fs-dialog>
  fancy-button button {
    padding: 10px 15px;
    background: rgb(62,118,194);
    color: white;
    border: none;
    border-radius: 4px
  }
</style>

<script>
class fancyButton extends HTMLElement {
  constructor() {
    super();
  }
  
  connectedCallback() {
    this.innerHTML = `<button>Click Me</button>`;
    
    var root = this.getRootNode();

    // In polyfilled browsers there is no shadow DOM so global styles still style
    // the "fake" shadow DOM. We need to test for truly native support so we know
    // when to inject styles into the shadow dom. The best way I've found to do that
    // is to test the toString output of a shadowroot since `instanceof ShadowRoot`
    // returns true when it's just a document-fragment in polyfilled browsers
    if (root.toString() === '[object ShadowRoot]' && !root.querySelector('style[data-fs-dialog]')) {
      var styles = document.querySelector('style[data-fs-dialog]').cloneNode(true);
      root.appendChild(styles);
    }
  }
}

customElements.define('fancy-button', fancyButton);
</script>

<fancy-button></fancy-button>

When all browsers support <link rel=stylesheet> in the shadow DOM, then the inline script can turn into an external stylesheet as robdodson suggested, and the code is a bit cleaner.

like image 193
Steven Lambert Avatar answered Oct 06 '22 01:10

Steven Lambert