Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to separate web components to individual files and load them?

I have a web component x-counter, which is in a single file.

const template = document.createElement('template');
template.innerHTML = `
  <style>
    button, p {
      display: inline-block;
    }
  </style>
  <button aria-label="decrement">-</button>
    <p>0</p>
  <button aria-label="increment">+</button>
`;

class XCounter extends HTMLElement {
  set value(value) {
    this._value = value;
    this.valueElement.innerText = this._value;
  }

  get value() {
    return this._value;
  }

  constructor() {
    super();
    this._value = 0;

    this.root = this.attachShadow({ mode: 'open' });
    this.root.appendChild(template.content.cloneNode(true));

    this.valueElement = this.root.querySelector('p');
    this.incrementButton = this.root.querySelectorAll('button')[1];
    this.decrementButton = this.root.querySelectorAll('button')[0];

    this.incrementButton
      .addEventListener('click', (e) => this.value++);

    this.decrementButton
      .addEventListener('click', (e) => this.value--);
  }
}

customElements.define('x-counter', XCounter);

Here the template is defined as using JavaScript and html contents are added as inline string. Is there a way to separate template to an x-counter.html file, css to say, x-counter.css and corresponding JavaScript code to xcounter.js and load them in index.html?

Every example I lookup has web components mixed. I would like to have separation of concerns, but I am not sure how to do that with components. Could you provide a sample code? Thanks.

like image 944
johndoe Avatar asked Mar 09 '19 17:03

johndoe


2 Answers

In the main file, use <script> to load the Javascript file x-counter.js.

In the Javascript file, use fetch() to load the HTML code x-counter.html.

In the HTML file, use <link rel="stylesheet"> to load the CSS file x-counter.css.

CSS file : x-counter.css

button, p {
    display: inline-block;
    color: dodgerblue;
}

HTML file : x-counter.html

<link rel="stylesheet" href="x-counter.css">
<button aria-label="decrement">-</button>
    <p>0</p>
<button aria-label="increment">+</button>

Javascript file : x-counter.js

fetch("x-counter.html")
    .then(stream => stream.text())
    .then(text => define(text));

function define(html) {
    class XCounter extends HTMLElement {
        set value(value) {
            this._value = value;
            this.valueElement.innerText = this._value;
        }

        get value() {
            return this._value;
        }

        constructor() {
            super();
            this._value = 0;

            var shadow = this.attachShadow({mode: 'open'});
            shadow.innerHTML = html;

            this.valueElement = shadow.querySelector('p');
            var incrementButton = shadow.querySelectorAll('button')[1];
            var decrementButton = shadow.querySelectorAll('button')[0];

            incrementButton.onclick = () => this.value++;
            decrementButton.onclick = () => this.value--;
        }
    }

    customElements.define('x-counter', XCounter);
}

Main file : index.html

<html>
<head>
    <!-- ... -->
    <script src="x-counter.js"></script>
</head>
<body>
    <x-counter></x-counter>
</body>
</html>
like image 160
Supersharp Avatar answered Nov 10 '22 04:11

Supersharp


A generic pattern using top level await without side-effects:

my-component/
  element.js
  template.html
  styles.css

template.html (be sure to link to styles.css)

<template>
  <link rel="stylesheet" href="./styles.css" />

  <!-- other HTML/Slots/Etc. -->
  <slot></slot>
</template>

styles.css (regular CSS file)

:host {
  border: 1px solid red;
}

element.js (uses top level await in export)

const setup = async () => {
  const parser = new DOMParser()
  const resp = await fetch('./template.html')
  const html = await resp.text()
  const template = parser.parseFromString(html, 'text/html').querySelector('template')

  return class MyComponent extends HTMLElement {
    constructor() {
      super()

      this.attachShadow({ mode: 'open'}).appendChild(template.content.cloneNode(true))
    }

    // Rest of element implementation...
  }
}

export default await setup()

index.html (loading and defining the element)

<!doctype html>
<html>
  <head>
    <title>Custom Element Separate Files</title>
    <script type="module">
      import MyComponent from './element.js'

      if (!customElements.get('my-component')) {
        customElements.define('my-component', MyComponent)
      }
    </script>
  </head>
  <body>
    <my-component>hello world</my-component>
  </body>
</html>

You can and should make side-effects (like registering a custom element in the global scope) explicit. Aside from creating some init function to call on your element, you can also provide a distinct import path, for example:

defined.js (sibling to element.js)

import MyComponent from './element.js'

const define = async () => {
  let ctor = null

  customElements.define('my-component', MyComponent)
  ctor = await customElements.whenDefined('my-component')

  return ctor
}

export default await define()

index.html (side-effect made explicit via import path)

<!doctype html>
<html>
  <head>
    <title>Custom Element Separate Files</title>
    <script type="module" src="./defined.js"></script>
  </head>
  <body>
    <my-component>hello world</my-component>
  </body>
</html>

This approach can also support arbitrary names when defining the custom element by including something like this inside define:

new URL(import.meta.url).searchParams.get('name')

and then passing the name query param in the import specifier:

<script type="module" src="./defined.js?name=custom-name"></script>
<custom-name>hello</custom-name>
like image 3
morganney Avatar answered Nov 10 '22 04:11

morganney