Multiple instances of a web component compiled by Svelte 3

I have managed to create a web component using Svelte 3 that show a list of products fetched from an API. In this component I have an action that checks if the product-element is inside another (parent) element using intersectionObserver.

All is good as long as I only have one instance of the component on the same page.

The issue I am facing is that the second, third and so on instances of the web componentall check if the product-element is inside the first component’s outerProductContainer-element (parent).

Is it possible to reference the current instance’s outerProductContainer-element? And if that is the case, how?

recomended-product.js

<svelte:options tag="recomended-products" />

<script>
  import { onMount, onDestroy } from "svelte";
  import { intersect } from "./actions/intersectAction";

  let outerProductContainer;
  let productList;
  let productListWidth;

  let products = [];
  let fullWidth = 0;
  let position = 0;
  let currentIndex = 0;
  let productContainers = [];
  let allWidths = [];
  let inside = [];

  /**
   * Do stuff when component is mounted
   */
  onMount(async () => {
    // Get data
    let res;
    let url="https://api.shop.products";
    res = await fetch(url);

    if (res.ok) {
      // Prepare data
      const json = await res.json();
      products = json.product_data;
    }
  });

  /**
   * Slide 
  */
  function slide(direction) {
    if (direction === "left") {
      position = position + allWidths[currentIndex];
      position > 0 ? (position = 0) : currentIndex--;
    } else if (direction === "right") {
      position = position - allWidths[currentIndex];
      position < fullWidth * -1 ? (position = fullWidth * -1) : currentIndex++;
    } else {
      position = position;
    }
    productList.style.transform = `translateX(${position}px)`;
  }

  /**
   * Calculate product container width includ margin and stuff
  */
  function calculateProductContainerWidth() {
    productContainers.forEach((product, index) => {
      let productComputed = window.getComputedStyle(product);
      allWidths[index] =
        parseFloat(productComputed.width) +
        parseFloat(productComputed.marginLeft) +
        parseFloat(productComputed.marginRight);
    });
  }

  /**
   * Handle resize of window
  */
  function handleResize() {
    calculateProductContainerWidth();
  }

  $: productContainers, calculateProductContainerWidth();

  /**
   * Sum width of all product containers
  */
  $: {
    fullWidth = allWidths.reduce((a, b) => a + b, 0) - productListWidth;
  }

  onDestroy(() => {});
</script>

<svelte:window on:resize={handleResize} />

<div class="recomended-container {products.length < 4 ? 'hidden' : ''}">
  <h2>
    {title}
    {#if categoryname} {categoryname} {/if}
  </h2>

  <div bind:this={outerProductContainer}>
    <ul
      id="product-list"
      bind:this={productList}
      bind:offsetWidth={productListWidth}
    >
      {#each products as product, i}
        <li
          class:opacity-25={!inside[i]}
          class:opacity-1={inside[i]}
          use:intersect={{ rootElement: outerProductContainer }}
          on:entered={() => (inside[i] = true)}
          on:exited={() => (inside[i] = false)}
          bind:this={productContainers[i]}
          class="product-container"
        >
          <a href="{product.url}">
            <div class="content">
              <h3>{product.name}</h3>
              <p>{product.description}</p>
              ...
            </div>
          </a>
        </li>
      {/each}
    </ul>

    <button
      on:click={() => slide("left")}
      id="prev"
      class:disabled={position >= 0}
      ><svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        ><path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M15 19l-7-7 7-7"
        /></svg
      ></button
    >

    <button
      on:click={() => slide("right")}
      id="next"
      class:disabled={position <= fullWidth * -1}
      ><svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        ><path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M9 5l7 7-7 7"
        /></svg
      ></button
    >
  </div>
</div>

<style>
  :host {
    display: block;
    background-color: rgba(249, 250, 243, 1);
  }

  *,
  ::before,
  ::after {
    box-sizing: border-box;
    border-width: 0;
    border-style: solid;
    border-color: #e5e7eb;
  }

  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }

  a {
    text-decoration: none;
  }

  .hidden {
    display: none;
  }

  .recomended-container {
    padding-top: 2rem;
    padding-bottom: 5rem;
    padding-left: 1rem;
    padding-right: 1rem;
    margin-left: auto;
    margin-right: auto;
    max-width: 88rem;
  }

  @media (min-width: 640px) {
    .recomended-container {
      padding-left: 1.5rem;
      padding-right: 1.5rem;
    }
  }

  @media (min-width: 1024px) {
    .recomended-container {
      padding-left: 2rem;
      padding-right: 2rem;
    }
  }

  #next,
  #prev {
    -tw-bg-opacity: 1;
    background-color: rgb(253 229 228 / var(--tw-bg-opacity));
    border-radius: 9999px;
    justify-content: center;
    align-items: center;
    transform: translate(var(--tw-translate-x), var(--tw-translate-y))
      rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y))
      scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
    --tw-translate-y: -50%;
    width: 3rem;
    height: 3rem;
    display: flex;
    top: 50%;

    position: absolute;
    padding: 0.5rem 1rem;
    font-size: 1.125rem;
    line-height: 2rem;
    --tw-text-opacity: 1;
    color: rgb(25 93 79 / var(--tw-text-opacity));
    cursor: pointer;
  }

  #next.disabled,
  #prev.disabled {
    opacity: 0.1;
  }

  #prev {
    left: 0px;
  }

  #next {
    right: 0px;
  }

  #next svg,
  #prev svg {
    --tw-text-opacity: 1;
    color: rgb(26 74 63);
    width: 1.5rem;
    height: 1.5rem;
    display: block;
  }

  .recomended-container > h2 {
    margin-top: 1rem;
    margin-bottom: 1.25rem;
    color: rgb(25 93 79);
    font-weight: 900;
    font-size: 1.875rem;
    line-height: 2.25rem;
    letter-spacing: -0.025em;
    font-family: signo, "sans-serif";
  }

  .recomended-container > div {
    padding-left: 4rem;
    padding-right: 4rem;
    position: relative;
  }

  .recomended-container > div > ul {
    flex-wrap: nowrap;
    display: flex;
    align-items: stretch;
    position: relative;
    list-style: none;
    margin: 0;
    padding: 0;

    -webkit-transition: 0.2s ease-in-out;
    -moz-transition: 0.2s ease-in-out;
    -o-transition: 0.2s ease-in-out;
    transition: 0.2s ease-in-out;
  }

  .product-container {
    display: flex;
    opacity: 1;
    justify-content: center;
    position: relative;
    flex-direction: column;
    width: 100%;
    min-width: 100%;
    max-width: 100%;
    transition-duration: 0.2s;
    box-sizing: border-box;
  }

  .product-container.opacity-25 {
    opacity: 0.25;
  }

  .product-container:hover {
    transform: scale(1.02);
  }

  @media (min-width: 560px) {
    .product-container {
      width: 50%;
      min-width: 50%;
      max-width: 50%;
    }
  }

  @media (min-width: 768px) {
    .product-container {
      width: calc(33%);
      min-width: calc(33%);
      max-width: calc(33%);
    }
  }

  @media (min-width: 1024px) {
    .product-container {
      width: 25%;
      min-width: 25%;
      max-width: 25%;
    }
  }

  @media (min-width: 1280px) {
    .product-container {
      width: 20%;
      min-width: 20%;
      max-width: 20%;
    }
  }

  .product-container > a {
    background-color: white;
    border-radius: 0.5rem;
    margin: 0 0.5rem;
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
  }

  .content {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
    text-align: center;
  }

  @media (min-width: 1024px) {
    .content {
      padding-top: 1.5rem;
      padding-bottom: 1.5rem;
    }
  }

  .content h3 {
    margin: 0;
    font-weight: 700;
    font-size: 0.875rem;
    line-height: 1.25rem;
    margin-bottom: 0.5rem;
    letter-spacing: -0.025em;
    font-family: signo, "sans-serif";
    --tw-text-opacity: 1;
    color: rgb(26 74 63);
  }

  .content > div {
    font-size: 0.875rem;
    line-height: 1.25rem;
    margin-top: auto;
  }

  .content > div p {
    margin-bottom: 0;
    font-weight: 100;
  }

  .content > div p s {
    --tw-text-opacity: 1;
    color: rgb(162 188 175);
  }

  .content > div p span {
    -tw-text-opacity: 1;
    color: rgb(26 74 63);
    font-weight: 700;
  }

</style>

intersectAction.js

let intersectionObserver;

export function intersect(element, options) {
  if (!intersectionObserver) initializeIntersectionObserver(options.rootElement);

  intersectionObserver.observe(element);

  return {
    destroy() {
      observer.unobserve(element);
    },
  };
}

function initializeIntersectionObserver(rootElement) {
  if (intersectionObserver) return;

  const options = {
    root: rootElement,
    rootMargin: `0px 0px 0px 0px`,
    threshold: 0.8
  }

  intersectionObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        const eventName = entry.isIntersecting ? 'entered' : 'exited';
        entry.target.dispatchEvent(new CustomEvent(eventName))
      });
    }, options
  )
}

Here is my rollup config in case that is of essence.

rollup.config.js

import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';

const production = !process.env.ROLLUP_WATCH;

function serve() {
    let server;

    function toExit() {
        if (server) server.kill(0);
    }

    return {
        writeBundle() {
            if (server) return;
            server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
                stdio: ['ignore', 'inherit', 'inherit'],
                shell: true
            });

            process.on('SIGTERM', toExit);
            process.on('exit', toExit);
        }
    };
}

export default [{
    //input: 'src/main.js',
    input: ["./src/RecomendedProducts.svelte"],
    output: {
        //sourcemap: true,
        format: 'iife',
        dir: "../assets/"
        //name: 'svelte',
        //file: '../assets/svelte.js'
    },
    plugins: [
        svelte({
            compilerOptions: {
                // enable run-time checks when not in production
                dev: !production,
                customElement: true,
            }
        }),
        // we'll extract any component CSS out into
        // a separate file - better for performance
        css({ output: 'bundle.css' }),

        // If you have external dependencies installed from
        // npm, you'll most likely need these plugins. In
        // some cases you'll need additional configuration -
        // consult the documentation for details:
        // https://github.com/rollup/plugins/tree/master/packages/commonjs
        resolve({
            browser: true,
            dedupe: ['svelte']
        }),
        commonjs(),

        // In dev mode, call `npm run start` once
        // the bundle has been generated
        !production && serve(),

        // Watch the `public` directory and refresh the
        // browser on changes when not in production
        !production && livereload('public'),

        // If we're building for production (npm run build
        // instead of npm run dev), minify
        production && terser()
    ],
    watch: {
        clearScreen: false
    }
},{
    //input: 'src/main.js',
    input: ["./src/SiteSearch.svelte"],
    output: {
        //sourcemap: true,
        format: 'iife',
        dir: "../assets/"
        //name: 'svelte',
        //file: '../assets/svelte.js'
    },
    plugins: [
        svelte({
            compilerOptions: {
                // enable run-time checks when not in production
                dev: !production,
                customElement: true,
            }
        }),
        // we'll extract any component CSS out into
        // a separate file - better for performance
        css({ output: 'bundle.css' }),

        // If you have external dependencies installed from
        // npm, you'll most likely need these plugins. In
        // some cases you'll need additional configuration -
        // consult the documentation for details:
        // https://github.com/rollup/plugins/tree/master/packages/commonjs
        resolve({
            browser: true,
            dedupe: ['svelte']
        }),
        commonjs(),

        // In dev mode, call `npm run start` once
        // the bundle has been generated
        !production && serve(),

        // Watch the `public` directory and refresh the
        // browser on changes when not in production
        !production && livereload('public'),

        // If we're building for production (npm run build
        // instead of npm run dev), minify
        production && terser()
    ],
    watch: {
        clearScreen: false
    }
}];

Leave a Comment