web-componentshadow-domcustom-element

How to query Shadow DOM instead of Light DOM for script appended to shadowRoot


I have a template that includes script content, all of which is attached to the shadow DOM of custom element. How to make this script query shadow DOM instead of Light DOM given it is part of the template of shadow root

In another words, i create custom element "pc-coverflow-swiper" that utilizes Swiper JS library. This library is attached to the shadowDOM through template with initialization in script section that is part of the template attached to the shadow DOM

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>Swiper demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />

  <template id="TmplCoverflowSwiper">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
    <style>
      html,
      body {
        position: relative;
        height: 100%;
      }

      body {
        background: #eee;
        font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
        font-size: 14px;
        color: #000;
        margin: 0;
        padding: 0;
      }

      .swiper {
        width: 100%;
        padding-top: 50px;
        padding-bottom: 50px;
      }

      .swiper-slide {
        background-position: center;
        background-size: cover;
        width: 300px;
        height: 300px;
      }

      .swiper-slide img {
        display: block;
        width: 100%;
      }
    </style>
    <div class="swiper mySwiper">
      <div class="swiper-wrapper">
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-1.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-2.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-3.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-4.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-5.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-6.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-7.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-8.jpg" />
        </div>
        <div class="swiper-slide">
          <img src="https://swiperjs.com/demos/images/nature-9.jpg" />
        </div>
      </div>
      <div class="swiper-pagination"></div>
    </div>
    <script>
       (async () => {
          // <!-- Swiper JS -->
          const { Swiper } = await import("https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.mjs");
          console.log('swiper-bundle imported...')
          
          // <!-- Initialize Swiper -->
          var swiper = new Swiper('.swiper', {
          effect: "coverflow",
            grabCursor: true,
            centeredSlides: true,
            slidesPerView: "auto",
            coverflowEffect: {
              rotate: 50,
              stretch: 0,
              depth: 100,
              modifier: 1,
              slideShadows: true,
            },
            pagination: {
              el: ".swiper-pagination",
            },
          });

        }
        )();
    </script>
  </template>

  <script>
    class CoverflowSwiper extends HTMLElement {
      constructor() {
        super(); // always call super() first
      }

      /** Custom Component Reactions **/
      connectedCallback() {
        console.log("Coverflow-Swiper  connected to page");

           //adding shadow DOM
        let shadowRoot = this.attachShadow({ mode: 'open' });
        console.log('created shadow DOM');

        let shadowTmp = document.querySelector("#TmplCoverflowSwiper");;
        shadowRoot.appendChild(
          document.importNode(shadowTmp.content, true)
        );
      
      }
    }

    //link custom element w/ JS class
    customElements.define("pc-coverflow-swiper", CoverflowSwiper);
 
  </script>

</head>

<body>
  <h1>Making Swiper Web Component</h1>
  <pc-coverflow-swiper id="PCCoverflowSwiper">
  </pc-coverflow-swiper>
</body>
</html>

As you can see, the Swiper JS do query for ".swiper" element when initializing via document.querySelector(".swiper") apparently, however. Given it is attached to shadowDOM, this query can't find the said element. I need to add the following hack in the script section of template to provide Swiper JS with the .swiper element of the Shadow DOM:

...
         
           var swiper = new Swiper('.swiper', {
          **init: false**,// need to adjust for shadowRoot first before initialize
          ...
          });

          //hack to make Swiper JS use Shadow DOM instead of Light DOM
          //get ref to shadow DOM
          let shRoot = document.querySelector('#PCCoverflowSwiper').shadowRoot;
          //feed Swiper JS with element from shadow DOM to avoid it look in light DOM
          swiper.pagination.el = shRoot.querySelector(".swiper-pagination");
          //feed Swiper JS with element from shadow DOM to avoid it looking in light DOM and, afterwards, start the swiper  
          swiper.init(shRoot.querySelector(".swiper"));
          
          ...

Is there a way to avoid all this hacking to make Swiper JS query for ".swiper" element located in the shadow DOM instead light DOM that it is currently doing? I mean Swiper JS is part of Shadow DOM to begin with, why is it going out of the scope of shadow DOM. It feels like there needs to be some sort of scope reset (i.e. document = this.shadowRoot or something) before attaching it. Any thoughts, suggestions appriciated.


Solution

  • Your workaround is correct, you need to use shadowRoot and query that instead of the entire document.

    A lot of older libraries (especially ones that rely on jQuery style ready or loader hydration) just aren't compatible inside web components.

    There's no way around this - back when they wrote Swiper they made the assumption that the whole DOM tree could be found through document.querySelector (a reasonable call back when jQuery was standard) and that's at odds with the encapsulation that makes Shadow DOM useful.

    That new Swiper('selector'... constructor is another legacy of that kind of development - when it fires it builds the DOM in the selected element. <template> is a far better way of doing this, especially in any component that's reused.

    Basically: you can use web components in these legacy libraries, but you can't use these legacy libraries in web components.

    This is why there's now a whole different version of Swiper for web components - use swiper-element-bundle instead of swiper-bundle.

    This uses web components instead of the initialisation constructor:

    <script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-element-bundle.min.js"></script>
    
    ...
    
    <swiper-container>
      <swiper-slide>Slide 1</swiper-slide>
      <swiper-slide>Slide 2</swiper-slide>
      <swiper-slide>Slide 3</swiper-slide>
      ...
    </swiper-container>
    

    If you switch to the custom elements version of Swiper that will work with web components better.

    It's also worth looking at other web component alternatives (like Shoelace's carousel) or even just using CSS (for instance you can do a scroll snap carousel with no JS).