Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which parts should go in the shadow DOM and the light DOM?

I'm learning web components. When designing a custom element, I have to decide what is going to be hidden in the the shadow DOM. The remainder will then be exposed in the light DOM.

As far as I understand, the APIs allow two extreme use cases with different tradeoffs:

  • hide almost nothing in the shadow DOM, most of the element's content is in the light DOM and in the element's attributes:
    • this allows an HTML author to provide anything for the component to display without writing JS;
    • this is close to the status quo regarding searchability and accessibility
    • but there is little reward for the work involved; I add complexity with components but they don't encapsulate anything (everything is exposed).
  • hide almost everything in the shadow DOM, the element's innerHTML is empty:
    • this requires the element to be instantiated from JS;
    • this locks usage a lot more because instantiating from JS is more strict (type-wise) than using HTML slots and attributes;
    • this may be less searchable and accessible (I'm not sure whether this is the case);

I currently lean toward hiding everything in the shadow DOM for the following reasons:

  • I intend to instantiate everything from JS. I'm not going to author pages in HTML manually. It would be more work to code both an HTML API and a JS API.
  • It's less cognitive work to hide everything. I don't need to find a right balance about which information is visible in the light DOM.
  • It's closer to most JS frameworks I'm familiar with.

Am I missing something?


Edit

Thank you, I am answered that it depends on the use case which partially answers my question. But I'm still missing an answer regarding the case I'm in: I'd rather not support slots for some of my components.

I'll add an example for each extreme of the spectrum:

  • Light-DOM-heavy component: the component user has to insert elements into slots
    <template id=light-email-view>
      <div>
        <div><slot name=from></slot></div>
        <ul><slot name=to></slot></ul>
        <h1><slot name=subject></slot></h1>
        <div><slot name=content></slot></div>
        <ul><slot name=attachements></slot></ul>
        <div class=zero-attachment-fallback>no attachments</div>
      </div>
    </template>
    

  • Shadow-DOM-heavy component: the component user has to use the JS API
    <template id=shadow-email-view>
      <div></div>
    </template>
    <script>
    ...
    let view = document.createElement('shadow-email-view');
    // this method renders the email in the shadow DOM entirely
    view.renderFromOject(email);
    container.appendChild(view);
    </script>
    

    In the first example, the component author has more work to do since they need to "parse" the DOM: they have to count attachments to toggle the fallback; basically, any transformation of input that isn't the browser copying an element from the light DOM into the matching shadow DOM slot. Then they need to listen for attribute changes and whatnot. The component user also has more work, they have to insert the right elements into the right slots, some of them non-trivial (the email content may have to be linkified).

    In the second example, the component author doesn't need to implement support for instantiating from HTML with slots. But the component user has to instantiate from JS. All the rendering is done in the .renderFromObject method by the component author. Some additional methods provide hooks to update the view if needed.

    One may advocate for a middle ground by having the component offer both slots and JS helpers to fill those. But I don't see the point if the component isn't to be used by HTML authors and that's still more work.

    So, is putting everything with the shadow DOM viable or should I provide slots because not doing so isn't standard compliant and my code is going to break on some user agent expecting them (ignoring older UAs that are not at all aware of custom elements)?

  • like image 279
    guillaume Avatar asked Jan 22 '19 02:01

    guillaume


    Video Answer


    2 Answers

    @supersharp has nailed it.

    One thing I see with Web Components is that people tend to have their component do way too much instead of breaking into smaller components.

    Let's consider some native elements:

    <form> there is no shadow DOM and the only thing it does is read values out of its children form elements to be able to do an HTTP GET, POST, etc.

    <video> 100% shadowDOM and the only thing it uses the app supplied children for is to define what video will be playing. The user can not adjust any CSS for the shadow children of the <video> tag. Nor should they be allowed to. The only thing the <video> tag allows is the ability to hide or show those shadow children. The <audio> tag does the same thing.

    <h1> to <h6> No shadow. All this does is set a default font-size and display the children.

    The <img> tag uses shadow children to display the image and the Alt-Text.

    Like @supersharp has said the use of shadowDOM is based on the element. I would go further to say that shadowDOM should be a well thought out choice. I will add that you need to remember that these are supposed to be components and not apps.

    Yes, you can encapsulate your entire app into one component, but the browsers didn't attempt to do that with Native components. The more specialized you can make your components to more reusable they become.

    Avoid adding anything into your Web Components that is not vanilla JS, in other words, do not add any framework code into your components unless you never want to share them with someone that does not use that framework. The components I write are 100% Vanilla JS and no CSS frameworks. And they are used in Angular, React and vue with no changes to the code.

    But chose the use of shadowDOM for each component written. And, if you must work in a browser that does not natively support Web Components that you may not want to use shadowDOM at all.

    One last thing. If you write a component that does not use shadowDOM but it has CSS then you have to be careful where you place the CSS since your component might be placed into someone else's shadowDOM. If your CSS was placed in the <head> tag then it will fail inside the other shadowDOM. I use this code to prevent that problem:

    function setCss(el, styleEl) {
      let comp = (styleEl instanceof DocumentFragment ? styleEl.querySelector('style') : styleEl).getAttribute('component');
      if (!comp) {
        throw new Error('Your `<style>` tag must set the attribute `component` to the component name. (Like: `<style component="my-element">`)');
      }
    
      let doc = document.head; // If shadow DOM isn't supported place the CSS in `<head>`
      // istanbul ignore else
      if (el.getRootNode) {
        doc = el.getRootNode();
        // istanbul ignore else
        if (doc === document) {
          doc = document.head;
        }
      }
    
      // istanbul ignore else
      if (!doc.querySelector(`style[component="${comp}"]`)) {
        doc.appendChild(styleEl.cloneNode(true));
      }
    }
    
    export default setCss;
    
    like image 158
    Intervalia Avatar answered Sep 27 '22 23:09

    Intervalia


    The choice is 100% dependent on the use case.

    Also:

    • if you want the user to be able to format your custom element with global CSS style attributes, you may opt for the normal, light DOM.

    • you're right: in the Shadow DOM, "this may be less searchable": the document.querySelector() method won't inspect the Shadow DOM content.

    • as a consequence, some third-pary JS library may fail to integrate easily with Shadow DOM

    • if you intend to use a Custom Element polyfill for legacy browsers, you may avoid Shadow DOM because some of its features cannot be really polyfilled.

    • in many cases, the answer is to provide a mix of Light DOM and Shadow DOM. As suggested by @JaredSmith:

      • Shadow DOM for the Web Component author,
      • Light DOM for the Web Compoent user, intergrated in the Shadow DOM with <slot>.

    As a conclusion, you should consider the context in which your Web Component will be used to decide whether Shadow DOM is required or not.


    Answer to the Edit

    Considering your use case, I would create a custom element and:

    • let the user populate the light DOM with atomic value(s): type element <div class="mail-to"> or custom sub-components <mail-to> as suggested by @Intervalia,
    • use a Shadow DOM to mask the light DOM,
    • use Javascript: this.querySelectorAll('.mail-to') or this.querySelectorAll('mail-to') instead of <slot> to extract data from the light DOM and copy (or move) them to the Shadow DOM.

    This way users won't have to learn the <slot> working, and the developer will be able to format the web component rendering with more freedom.

    <email-view>
      <mail-to>[email protected]</mail-to>
      <mail-to>[email protected]</mail-to>
      <mail-from>[email protected]</mail-from>
      <mail-body>hello world!</mail-body>
    <email-view>
    
    like image 43
    Supersharp Avatar answered Sep 28 '22 00:09

    Supersharp