🌈 Try out the features of Web Components one by one
Tags: javascript, webdev, webcomponents, html
Published: 2020-01-03 16:29:48

---
title: 🌈 Try out the features of Web Components one by one
published: true
description: In order to learn web components from scratch, I implemented  features of web components one by one
tags: #javascript #webdev #webcomponents #html
---

<img src="https://user-images.githubusercontent.com/3895795/71714960-194f1d80-2e53-11ea-9fda-330a363474cb.gif" width="100%"></img>

# 📖 Table Of Content
  * [🌍 1. HelloWorld with simple CustomElements](#chapter-1)
  * [🚴 2. Try the life cycle (other than Adapted Callback)](#chapter-2)
  * [🎣 3. Adapted Callback](#chapter-3)
  * [🔧 4. Extends HtmlXXXElement](#chapter-4)
  * [🛤️ 5. Three main ways to generate CustomElements](#chapter-5)
  * [🌓 6. CustomElements with shadow DOM and without](#chapter-6)
  * [🚪 7. Open and Close mode of ShadowRoot](#chapter-7)
  * [🐾 8. Template](#chapter-8)
  * [🎰 9. Slot](#chapter-9)
  * [🌸 10. Adopted Stylesheets](#chapter-10)
  * [🍺 11. (Extra) Todo List Example](#chapter-11)

# 💻 Source Code

[Demo](https://muukojima.github.io/feature-of-web-components/src/)
[Github](https://github.com/MuuKojima/feature-of-web-components)

# 🛠️ Preparation before starting for my codes

`feature-of-web-components/src/index.html`

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link rel="stylesheet" href="./index.css" />
    <script type="module" src="./index.js"></script>
  </head>
  <body>
    <div class="box">
      <button data-tag-name="x-helloworld">show</button>
      <span>Hello World</span>
    </div>
    <div class="box">
      <button data-tag-name="x-lifecycle">show</button>
      <span>LifeCycle</span>
    </div>
    <div class="box">
      <button data-tag-name="x-adapted-callback">show</button>
      <span>Adapted Callback</span>
    </div>
    <div class="box">
      <button data-tag-name="x-extends">show</button>
      <span>Extends</span>
    </div>
    <div class="box">
      <button data-tag-name="x-3ways">show</button>
      <span>Initialize 3 ways</span>
    </div>
    <div class="box">
      <button data-tag-name="x-shadow-noshadow">show</button>
      <span>Enable or Disable of ShadowRoot</span>
    </div>
    <div class="box">
      <button data-tag-name="x-open-close">show</button>
      <span>Open or Close of ShadowRoot</span>
    </div>
    <div class="box">
      <button data-tag-name="x-template">show</button>
      <span>Template</span>
    </div>
    <div class="box">
      <button data-tag-name="x-slot">show</button>
      <span>Slot</span>
    </div>
    <div class="box">
      <button data-tag-name="x-adopted-stylesheets">show</button>
      <span>Adopted Stylesheets</span>
    </div>
    <div class="box">
      <button data-tag-name="x-todo-list">show</button>
      <span>Todo List</span>
    </div>
    <main>
      <!-- Attach component -->
    </main>
  </body>
</html>
```

`feature-of-web-components/src/index.css`

```css
* {
  box-sizing: border-box;
}

body {
  color: dimgray;
  font-family: Helvetica, Arial, sans-serif;
}

main {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 30px;
}

x-shadow-noshadow {
  width: 700px;
}

x-todo-list {
  width: 500px;
}

.box {
  border: 1px solid lightgray;
  padding: 10px 20px;
}

.box + .box {
  margin-top: 10px;
}

button {
  margin-right: 10px;
}

h1 {
  font-weight: bold;
  font-size: 50px;
  text-align: center;
}
```

`feature-of-web-components/src/index.js`

```js
import './adoptedStyleSheets/index.js';
import './adaptedCallback/index.js';
import './extends/index.js';
import './helloworld/index.js';
import './lifecycle/index.js';
import './openClose/index.js';
import './shadowdom/index.js';
import './slot/index.js';
import './todoList/index.js';
import './template/index.js';
import './3ways/index.js';

// Get DOM
const _mainElm = document.querySelector('main');
const _buttonElms = document.querySelectorAll('button');

/**
 * Remove DOM
 * @private
 */
const _clearMain = () => {
  while (_mainElm.firstChild) {
    _mainElm.removeChild(_mainElm.firstChild);
  }
};

/**
 * Click each item
 * @private
 * @param {CustomEvent} e
 */
const handleItemClick = e => {
  // Initialize
  _clearMain();
  // Attach the tag to main
  _mainElm.appendChild(document.createElement(e.currentTarget.dataset.tagName));
};

// Set an event for each item
[..._buttonElms].forEach(item => item.addEventListener('click', handleItemClick));
```


# 🌍 1. HelloWorld with simple CustomElements <a name="chapter-1"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621737-79a44c00-2c14-11ea-864f-b5670c5cc7c4.gif" width="300"></img>

Just put `this.innerHTML` in your `connectedCallback`.

`feature-of-web-components/src/helloworld/index.js`

```js
/**
 * Hello Wold class
 */
export default class HelloWorld extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
  }
  /**
   * Attach
   */
  connectedCallback () {
    this.innerHTML = '<h1>Hello World</h1>';
  }
}

// Register custom element
window.customElements.define('x-helloworld', HelloWorld);
```

NOTE: Don't put `this.innerHTML` in your `constructor`. Because you can't add children or manipulate `attributes` in the `constructor` yet. If do that, you'll get error like this.

```
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
```

# 🚴 2. Try the life cycle (other than Adapted Callback) in CustomElements <a name="chapter-2">

<img src="https://user-images.githubusercontent.com/3895795/71621738-79a44c00-2c14-11ea-8ab7-b8acfd8dfa71.gif" width="300"></img>

### (1) observedAttributes

When a custom element is registered in `window.customElements.define`, `observedAttributes` is called and returns an array to listen for attribute changes. The reason for choosing an attribute to listen on here is that if you listen to all properties, there is overhead.

### (2) constructor

Next, `constructor` is called and initialization processing is performed.
The responsibilities of the `constructor` are to set up event listeners and to create a `shadowRoot`. When you try to use it as a `constructor`, note that it is a pattern that receives arguments from outside and sets it to attribute. At this point, it is still an error to set a value for attribute, so be careful. Also, an operation that attempts to add a child element such `as this.innerHTML` in the `constructor` will result in an error. The solution is to manipulate `attributes` and `render` with `connectedCallback`.

### (3) attributeChangedCallback

After the `constructor` is called, it can receive attribute changes, and `attributeChangedCallback` is called.

### (4) connectedCallback

`connectedCallback` is called when the tag is attached. The responsibility of `connectedCallback` is fetching resources and rendering.

### (5) disconnectedCallback

When the tag is detached, `disconnectedCallback` is called. The responsibility of `disconnectedCallback` is to clean up memory, such as remove event listeners.

`feature-of-web-components/src/lifecycle/index.js`

```js
import LifecycleItem from './item.js';

/**
 * Lifecycle testing class
 */
export default class Lifecycle extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = '<x-lifecycle-item label="LifeCycle"></x-lifecycle-item>';
    
    // Register `LifecycleItem` from the component on this side
    // If you define it in `LifecycleItem`, `window.customElements.define` is called
    // Then `observedAttributes` is also called
    // So it is not suitable for life cycle testing
    !window.customElements.get('x-lifecycle-item') && window.customElements.define('x-lifecycle-item', LifecycleItem);
  }
}

// Register custom element
window.customElements.define('x-lifecycle', Lifecycle);
```

`feature-of-web-components/src/lifecycle/item.js`

```js
/**
 * Lifecycle implementation class
 */
export default class LifecycleItem extends HTMLElement {
  /**
   * Create a whitelist to subscribe to attribute changes
   */
  static get observedAttributes() {
    alert('①: observedAttributes');
    return ['label'];
  }

  /**
   * Subscribe to attribute changes
   * @param {string} name 
   * @param {string} oldValue 
   * @param {string} newValue 
   */
  attributeChangedCallback(name, oldValue, newValue) {
    alert('③: attributeChangedCallback');
    switch(name){
      case 'label':
        this._label = newValue;
        break;
      default:
        break;
    }
  }

  /**
   * Constructor
   */
  constructor() {
    super();
    alert('②: constructor');
  }

  /**
   * Render
   */
  _render() {
    this.innerHTML = '<h1 class="label"></h1>';
    this.querySelector('.label').textContent = this._label;
  }

  /**
   * Attach
   */
  connectedCallback() {
    alert('④: connectedCallback');
    this._render();
  }

  /**
    * Detach
    */
  disconnectedCallback() {
    alert('⑤: disconnectedCallback');
  }

  /**
   * When the owner document moves
   */
  adoptedCallback() {
    // This is difficult to call, so just cut out and explain
  }
}
```

# 🎣 3. Adapted Callback <a name="chapter-3">

<img src="https://user-images.githubusercontent.com/3895795/71621739-7a3ce280-2c14-11ea-9262-671620e05c3a.gif" width="300"></img>

It is not usually called, but it is one of the life cycles. A callback that fires when the parent document moves. In the example, the custom element is moved to another html in the iframe and fired.

`feature-of-web-components/src/adaptedCallback/index.js`

```js
  
import './item.js';

/**
 * AdaptedCallback testing class
 */
export default class AdaptedCallback extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = `
      <style>
        .container {
          width: 500px;
          margin: 0 auto;
        }
        iframe {
          width:100%;
          height: 200px;
        }
        .button {
          display: block;
          width: 100%;
        }
      </style>
      <x-adapted-item></x-adapted-item>
      <div class="container">
        <div>Outer html</div>
        <button class="button">move</button>
        <iframe src="./adaptedCallback/inner.html">
      </div>
    `;
    this.clickLisnner = this.handleClick.bind(this);
    const buttonElm = this.querySelector('.button');
    buttonElm.addEventListener('click', this.clickLisnner);
  }

  /**
    * Detach
    */
  disconnectedCallback() {
    const buttonElm = this.querySelector('.button');
    buttonElm.removeEventListener('click', this.clickLisnner);
  }

  /**
   * Click button
   */
  handleClick() {
    const item = this.querySelector('x-adapted-item');
    const iframElm = this.querySelector('iframe');
    iframElm.contentDocument.body.appendChild(item);
  }
}

// Register custom element
window.customElements.define('x-adapted-callback', AdaptedCallback);
```

`feature-of-web-components/src/adaptedCallback/item.js`

```js
/**
 * AdaptedCallback implementation class
 */
export default class AdaptedItem extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = '<h1>Adapted Callback</h1>';
  }

  /**
   * When the owner document moves
   * In other words, fire when the parent html is changed to another html
   */
  adoptedCallback() {
    alert('adoptedCallback');
  }
}

// Register custom element
window.customElements.define('x-adapted-item', AdaptedItem);
```

`feature-of-web-components/src/adaptedCallback/inner.html`

```html
<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        color: dimgray;
        font-family: Helvetica, Arial, sans-serif;
      }
      h1 {
        font-weight: bold;
        font-size: 50px;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div>Inner html</div>
    <!-- Put components from outside here -->
  </body>
</html>
```

# 🔧 4. Extends HtmlXXXElement <a name="chapter-4"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621740-7a3ce280-2c14-11ea-97ea-6e8674534214.gif" width="300"></img>

Extend existing `HTMLAnchorElement` and create link tags that require approval for transition.

`feature-of-web-components/src/extends/index.js`

```js
  
import './anchor.js';

/**
 * Extends testing class
 */
export default class XExtends extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = `
      <style>
        a {
          display: block;
          text-align: center;
        }
      </style>
      <h1>Extend existing UI parts</h1>
      <!-- 
        Apply custom elements using the 'is' attribute
        Alternatively, it can also be created with the new operator, DOM API
      -->
      <a href="https://www.google.com/" is="x-confirm-anchor">https://www.google.com/</a>
    `;
  }
}

// Register custom element
window.customElements.define('x-extends', XExtends);
```

`feature-of-web-components/src/extends/anchor.js`

```js
/**
 * ConfirmAnchor class that extends Anchor
 */
export default class ConfirmAnchor extends HTMLAnchorElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.clickLisnner = e => {
      const result = confirm(`Jump to link? : '${this.href}'`);
      if (!result) {
        // Disable Anchor action
        e.preventDefault();
      }
    }
    this.addEventListener('click', this.clickLisnner);
  }

  /**
   * Detach
   */
  disconnectedCallback() {
    this.removeEventListener('click', this.clickLisnner);
  }
}

// Register custom element
// Add third argument when extends existing browser element
customElements.define('x-confirm-anchor', ConfirmAnchor, { extends: 'a' });
```

# 🛤️ 5. Three main ways to generate CustomElements <a name="chapter-5"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621741-7a3ce280-2c14-11ea-8f2e-6b77e5e1b197.gif" width="300"></img>

### (1) Tag writing

```html
<body>
  <x-foo label="hello"><x-foo>
</body>
```

### (2) DOM API

```js
const elm = document.createElement('x-foo');
elm.label = 'hello';
document.body.appendChild(elm);
```

### (3) new operator

```js
const Foo = window.customElements.get('x-foo');
document.body.appendChild(new Foo('hello'));
```

`feature-of-web-components/src/3ways/index.js`

```js
import './label.js'

/**
 * Testing custom element generation patterns
 */
export default class X3ways extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this._render();
  }

  /**
   * Render
   * @private
   */
  _render() {
    this.innerHTML = `
      <h1>Three ways to generate CustomElements</h1>
      <!-- ① Declarative tag writing pattern -->
      <x-label label="① hello"></x-label>
    `;

    // ② Generated by new operator
    const Label = window.customElements.get('x-label');
    const labelElmA = new Label('② hello');
    this.appendChild(labelElmA);
    // ③ Generated by DOM API
    const labelElmB = document.createElement('x-label');
    labelElmB.label = '③ hello';
    this.appendChild(labelElmB);
  }
}

// Register custom element
window.customElements.define('x-3ways', X3ways);
```

`feature-of-web-components/src/3ways/label.js`

```js
/**
 * Label class
 */
export default class Label extends HTMLElement {
  /**
   * Create a whitelist to subscribe to attribute changes
   */
  static get observedAttributes() {
    return ['label'];
  }

  /**
   * Subscribe to attribute changes
   * @param {string} name 
   * @param {string} oldValue 
   * @param {string} newValue 
   */
  attributeChangedCallback(name, oldValue, newValue) {
    switch(name){
      case 'label':
        this._label = newValue;
        this._render();
        break;
      default:
        break;
    }
  }

  /**
   * Constructor
   */
  constructor(label) {
    super();
    this._label = label || '';
  }

  /**
   * Attach
   */
  connectedCallback() {
    // If initialized by the new operator, an error will occur if the attribute is not manipulated after the constructor
    this.label = this._label;
  }

  /**
   * Render
   * @private
   */
  _render() {
    this.innerHTML = `<h1>${this._label}</h1>`;
  }

  set label(val) {
    if (val) {
      this.setAttribute('label', val);
    } else {
      this.removeAttribute('label');
    }
  }
}

// Register custom element
window.customElements.define('x-label', Label);
```

# 🌓 6. Customelements with shadow DOM and without <a name="chapter-6"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621742-7a3ce280-2c14-11ea-8c27-348ed87c6584.gif" width="300"></img>

Here, let's compare the normal custom element and the custom element with ShadowDOM. For non-ShadowDOM custom elements, the style wins, and the parent `h1` element becomes the style of the child `h1` element, it is smaller in size, and the color is blue. In the case of `ShadowDOM`, there is no influence of the style from the parent, and the child does not affect the parent.

`feature-of-web-components/src/shadowdom/index.js`

```js
import './noshadow.js';
import './shadow.js';

/**
 * ShadowRoot testing class
 */
export default class ShadowNoShadow extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = `
      <style>
        .container {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          padding: 50px 0;
        }
        x-noshadow,
        x-shadow {
          width: 600px;
        }
      </style>
      <h1>Enable or Disable of ShadowRoot</h1>
      <div class="container">
        <x-noshadow></x-noshadow>
        <x-shadow><x-shadow>
      </div>
    `
  }
}

// Register custom element
window.customElements.define('x-shadow-noshadow', ShadowNoShadow);
```

`feature-of-web-components/src/shadowdom/noshadow.js`

```js
/**
 * ShadowRoot disabled class
 */
class NOShadow extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = `
      <style>
        h1 {
          font-size: 30px;
          color: #3F51B5;
        }
        .box-noshadow {
          border: 1px solid lightgray;
          padding: 20px;
        }
      </style>
      <div class="box-noshadow">
        <h1>Disable shadowRoot</h1>
      <div> 
    `;
  }
}

// Register custom element
window.customElements.define('x-noshadow', NOShadow);
```

`feature-of-web-components/src/shadowdom/shadow.js`

```js
/**
 * ShadowRoot enabled class
 */
export default class Shadow extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    // Constructor is recommended for forming shadowRoot
    this.attachShadow({mode: 'open'});
  }

  /**
   * Attach
   */
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
        }
        h1 {
          font-size: 30px;
          color: #d51b5a;
          text-align: center;
        }
        .box-shadow {
          border: 1px solid lightgray;
          padding: 20px;
        }
      </style>
      <div class="box-shadow">
        <h1>Enable shadowRoot</h1>
      <div>
    `;
  }
}

// Register custom element
window.customElements.define('x-shadow', Shadow);
```

# 🚪 7. Open mode and Close mode of ShadowRoot <a name="chapter-7"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621743-7ad57900-2c14-11ea-8e8b-eb8266fc23e0.gif" width="300"></img>

ShadowRoot can be set to `open` or `closed` mode open can take shadowRoot but closed returns null when accessing shadowRoot.

According to goole, closed is ["should avoid"](https://github.com/chromium/chromium/blob/6f13c35b976901ac184b2d731a7b5e722b8cb2b0/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/index.mjs#L129), but it is interesting `chromium` uses [closed](https://github.com/chromium/chromium/blob/6f13c35b976901ac184b2d731a7b5e722b8cb2b0/third_party/blink/renderer/core/script/resources/layered_api/elements/switch/index.mjs#L129)👀

`feature-of-web-components/src/openClose/index.js`

```js
import './close.js';
import './open.js';

/**
 * Open and Close testing class
 */
export default class OpenClose extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    this.innerHTML = `
      <style>
        .button {
          width: 100px;
          height: 20px;
          display: block;
          margin: 0 auto;
        }
      </style>
      <h1>Mode of ShadowRoot</h1>
      <button class="button">click</button>
      <x-open></x-open>
      <x-close></x-close>
    `;
    this.clickLisnner = this.handleClick.bind(this);
    const buttonElm = this.querySelector('button');
    buttonElm.addEventListener('click', this.clickLisnner);
  }

  /**
    * Detach
    */
  disconnectedCallback() {
    const buttonElm = this.querySelector('button');
    buttonElm.removeEventListener('click', this.clickLisnner);
  }

  /**
   * Ckick button
   */
  handleClick() {
    const openElm = this.querySelector('x-open');
    const closeElm = this.querySelector('x-close');
    if (openElm.shadowRoot) {
      // Come here
      alert('Get Open ShadowDOM', openElm.shadowRoot);
    }
    if (closeElm.shadowRoot) {
      // Don't come here
      alert('Get Closed ShadowDOM', openElm.shadowRoot);
    }
    console.log(openElm.shadowRoot);
    // shadowRoot returns null
    console.log(closeElm.shadowRoot);
  }
}

// Register custom element
window.customElements.define('x-open-close', OpenClose);
```

`feature-of-web-components/src/openClose/open.js`

```js
/**
 * Open implementation class
 */
class Open extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
        }
        h1 {
          font-weigh: bold;
          font-size: 50px;
          text-align: center;
        }
      </style>
      <h1>Open</h1>
    `;
  }
}

// Register custom element
window.customElements.define('x-open', Open);
```

`feature-of-web-components/src/openClose/close.js`

```js
/**
 * Close implementation class
 */
class Close extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    const root = this.attachShadow({mode: 'closed'});
    root.innerHTML = `
      <style>
        h1 {
          font-weigh: bold;
          font-size: 50px;
          text-align: center;
        }
      </style>
      <h1>Close</h1>
    `;
  }
}

// Register custom element
window.customElements.define('x-close', Close);
```

# 🐾 8. Template <a name="chapter-8"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621744-7ad57900-2c14-11ea-9500-4a16b82d8aa2.gif" width="300"></img>

Even if template tag is rendered on the screen, it does not exist on the screen. Until activated, the content inside will not be rendered, and Do not load resources inside.

`feature-of-web-components/src/template/index.js`

```js
// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <h1>Template</h1>
`;

/**
 * Template implementation class
 */
export default class Template extends HTMLElement {
  /**
   * Attach
   */
  connectedCallback() {
    // Activate template with cloneNode
    this.appendChild(template.content.cloneNode(true));
  }
}

// Register custom element
window.customElements.define('x-template', Template);
```

# 🎰 9. Slot <a name="chapter-9"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621745-7ad57900-2c14-11ea-990f-9dbfa1b1ca04.gif" width="300"></img>

Creating a modal that can insert content from outside using slot tag.
NOTE: Slot does not work without shadowRoot.

`feature-of-web-components/src/slot/index.js`

```js
import './modal.js';

// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host {
      display: block;
    }
    h1 {
      font-weigh: bold;
      font-size: 50px;
      text-align: center;
    }
    .container {
      margin-top: 20px;
    }
  </style>
  <h1>Slot</h1>
  <button class="signin-button">signin</button>
  <button class="signup-button">signup</button>
  <div class="container"></div>
`;

/**
 * Slot testing class
 */
export default class XSlot extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._signinBtnElm =  this.shadowRoot.querySelector(".signin-button");
    this._signupBtnElm =  this.shadowRoot.querySelector(".signup-button");
    this._onSigninClickLisnner = this.handleSigninClick.bind(this);
    this._onSignupClickLisnner = this.handleSginupClick.bind(this);
  }

  /**
   * Attach
   */
  connectedCallback() {
    this._signinBtnElm.addEventListener('click', this._onSigninClickLisnner);
    this._signupBtnElm.addEventListener('click', this._onSignupClickLisnner);
  }

  /**
   * Detach
   */
  disconnectedCallback() {
    this._signinBtnElm.removeEventListener('click', this._onSigninClickLisnner);
    this._signupBtnElm.removeEventListener('click', this._onSignupClickLisnner);
  }

  /**
   * Click signin
   */
  handleSigninClick() {
    const containerElm = this.shadowRoot.querySelector('.container');
    containerElm.innerHTML = `
      <x-modal>
        <div slot="content">
          <h1>Signin</h1>
        </div>
      </x-modal>
    `;
  }

  /**
   * Click signup
   */
  handleSginupClick() {
    const containerElm = this.shadowRoot.querySelector('.container');
    containerElm.innerHTML = `
      <x-modal>
        <div slot="content">
          <h1>Signup</h1>
        </div>
      </x-modal>
    `;
  }
}

// Register custom element
window.customElements.define('x-slot', XSlot);
```

`feature-of-web-components/src/slot/modal.js`

```js
// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host {
      display: block;
    }
  </style>
  <dialog open>
    <button class="close">✗</button>
    <slot name="content"></slot>
  </dialog>
`;

/**
 * Slot implementation class
 */
export default class XSlot extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._closeElm =  this.shadowRoot.querySelector(".close");
    this._onCloseLisner = () => this.handleCloseClick();
  }

  /**
   * Attach
   */
  connectedCallback() {
    this._closeElm.addEventListener('click', this._onCloseLisner);
  }

  /**
   * Detach
   */
  disconnectedCallback() {
    this._closeElm.removeEventListener('click', this._onCloseLisner);
  }

  /**
   * Click close
   */
  handleCloseClick() {
    const dialogElm = this.shadowRoot.querySelector("dialog");
    dialogElm.close();
  }
}

// Register custom element
window.customElements.define('x-modal', XSlot);
```

# 🌸 10. Adopted Stylesheets <a name="chapter-10"></a>

<img src="https://user-images.githubusercontent.com/3895795/71621747-7b6e0f80-2c14-11ea-9cef-bbef27220f05.gif" width="300"></img>

Import css from external file using `adoptedStyleSheets`. `@imports` is asynchronous, so we need handling with `promise`.

`feature-of-web-components/src/adoptedStyleSheets/index.js`

```js
// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <h1 class="box">Adopted Stylesheets</h1>
`;

/**
 * AdoptedStyleSheets implementation class
 */
export default class AdoptedStyleSheets extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    this._initializeDOM();
  }

  /**
   * Initialize
   */
  _initializeDOM = async () => {
    const commonStylePath = '../index.css';
    const componentStylePath = './index.css';
    const tasks = [
      this._createStyleSheet(commonStylePath), 
      this._createStyleSheet(componentStylePath)
    ];
    try {
      this.attachShadow({mode: 'open'});
      // Combine existing sheets with our new one
      // e.g. this.shadowRoot.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
      this.shadowRoot.adoptedStyleSheets = await Promise.all(tasks);
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    } catch(err) {
      console.error(err);
    }
  }

  /**
   * Create stylesheet
   * @param {string}
   * @returns {Promise}
   */
  _createStyleSheet = async path => {
    const url = new URL(path, import.meta.url);
    const style = await new CSSStyleSheet().replace(`@import url(${url})`);
    return style;
  }
}

// Register custom element
window.customElements.define('x-adopted-stylesheets', AdoptedStyleSheets);
```

`feature-of-web-components/src/adoptedStyleSheets/index.css`

```css
:host {
  display: block;
}

h1 {
  font-style: italic;
}
```



# 🍺 11.(Extra) Todo List Example <a name="chapter-11"></a>

<img src="https://user-images.githubusercontent.com/3895795/71689537-9ba0f880-2de5-11ea-8e35-1699a4ef1ff8.gif" width="300"></img>

`feature-of-web-components/src/todoList/index.js`

```js
import './item.js';

// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host {
      display: block;
    }
    h1 {
      text-align: center;
      font-weight: bold;
      font-size: 50px;
    }
    .container {
      padding: 20px 0;
    }
    form {
      display: flex;
      align-items: center;
      justify-content: center;
      border: 1px solid lightgray;
      padding: 10px; 0;
      background-color: whitesmoke;
    }
    input {
      flex-grow: 1;
      margin: 0 10px;
      height: 20px;
    }
    x-todo-item + x-todo-item {
      margin-top: 20px;
    }
  </style>
  <h1>Todo List</h1>
  <form>
    <input type="text"></input>
    <button type="submit">add</button>
  </form>
  <div class="container"></div>
`;

/**
 * TodoList class
 */
export default class TodoList extends HTMLElement {
  /**
   * Constructor
   */
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._containerElm = this.shadowRoot.querySelector('.container');
    this._submitElm = this.shadowRoot.querySelector('form');
    this._inputElm = this.shadowRoot.querySelector('input');
    this._clickSubmitListener =  this._tryAddItem.bind(this);
  }

  /**
   * Attach
   */
  connectedCallback() {
    this._submitElm.addEventListener('submit', this._clickSubmitListener);
    this._render();
  }

  /**
   * Detach
   */
  disconnectedCallback() {
    this._submitElm.removeEventListener('submit', this._clickSubmitListener)
    const todoElms = this.shadowRoot.querySelectorAll('x-todo-item');
    [...todoElms].forEach(item => item.clearListeners())
  }

  /**
   * Render
   * @private
   */
  _render() {
    // Insert test data
    this._addItem('TaskC', false);
    this._addItem('TaskB', true);
    this._addItem('TaskA', false);
  }

  /**
   * Find Todo item from ID
   * @private
   * @param {string} id
   * @returns {Element | undefined}
   */
  _findItemById(id) {
    const todoElms = this.shadowRoot.querySelectorAll('x-todo-item');
    const target = [...todoElms].find(item => item.id === id);
    return target;
  }

  /**
   * Try add todoItem
   * @private
   * @param {CustomEvent} e 
   */
  _tryAddItem(e) {
    e.preventDefault();
    const val = this._inputElm.value;
    if (!val) {
      return;
    }
    // Initialize input
    this._inputElm.value = '';
    this._addItem(val, false);
  }

  /**
   * Add todo item
   * @private
   * @param {label} label
   * @param {boolean} checked
   */
  _addItem(label, checked) {
    const todoElm = document.createElement('x-todo-item');
    todoElm.label = label;
    todoElm.checked = checked;
    const onToggleListener = this._toggleItem.bind(this);
    const onRemoveListener = this._removeItem.bind(this);
    todoElm.addEventListener('onToggle', onToggleListener);
    todoElm.addEventListener('onRemove', onRemoveListener);
    todoElm.clearListeners = () => {
      todoElm.removeEventListener('onToggle', onToggleListener);
      todoElm.removeEventListener('onRemove', onRemoveListener);
    };
    // Add Todo items to the top of the container
    this._containerElm.insertBefore(todoElm, this._containerElm.firstChild);
  }

  /**
   * Toggle todo item for check mark
   * @private
   * @param {CustomEvent} e 
   */
  _toggleItem(e) {
    const item = this._findItemById(e.detail.id);
    if (!item) {
      return;
    }
    // Toggle check mark
    item.checked = !item.checked;
  }

  /**
   * Remove todo item form todolist
   * @private
   * @param {CustomEvent} e 
   */
  _removeItem(e) {
    const item = this._findItemById(e.detail.id);
    if (!item) {
      return;
    }
    // Remove target todo item 
    this._containerElm.removeChild(item);
  }
}

// Register custom element
window.customElements.define('x-todo-list', TodoList);
```

`feature-of-web-components/src/todoList/item.js`

```js
// Create template tag
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host {
      display: block;
    }
    .container {
      display: flex;
      align-items: center;
      justify-content: center;
      border: 1px solid lightgray;
      padding: 10px; 0;
    }
    .label {
      flex-grow: 1;
      margin: 0 10px;
    }
    :host .label {
      text-decoration: none;
    }
    :host([checked]) .label {
      text-decoration: line-through;
      opacity: 0.5;
    }
  </style>
  <div class="container">
    <input class="checkbox" type="checkbox">
    <label class="label"></label>
    <button class="remove" type="button">remove</button>
  </div>
`;

/**
 * TodoItem class
 */
export default class Todo extends HTMLElement {
  /**
   * Create a whitelist to subscribe to attribute changes
   */
  static get observedAttributes() {
    return ['label', 'checked'];
  }

  /**
   * Subscribe to attribute changes
   * @param {string} name 
   * @param {string} oldValue 
   * @param {string} newValue 
   */
  attributeChangedCallback(name, oldValue, newValue) {
    switch(name){
      case 'label':
        this._label = newValue;
        break;
      case 'checked':
        this._checked = this.hasAttribute('checked');
        break;
      default:
        break;
    }
    this._render();
  }

  /**
   * Constructor
   */
  constructor() {
    super();
    this.attachShadow({ 'mode': 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._id = this._createRandomId();
    this._label =  '';
    this._checked =  false;
    this._checkBoxElm = this.shadowRoot.querySelector('.checkbox');
    this._removeElm = this.shadowRoot.querySelector('.remove');
    this._labelElm = this.shadowRoot.querySelector('.label');
    this._toggleListener = this._dispatchToggle.bind(this);
    this._removeListener = this._dispatchRemove.bind(this);
  }

  /**
   * Attach
   */
  connectedCallback() {
    this._checkBoxElm.addEventListener('click', this._toggleListener);
    this._removeElm.addEventListener('click', this._removeListener);
    this._render();
  }

  /**
   * Detach
   */
  disconnectedCallback() {
    this._checkBoxElm.removeEventListener('click', this._toggleListener)
    this._removeElm.removeEventListener('click', this._removeListener);
  }

  /**
   * Render
   * @private
   */
  _render() {
    this._labelElm.textContent = this._label;
    this._checkBoxElm.checked = this._checked;
    this._checked ? this._labelElm.classList.add('label--selected') : this._labelElm.classList.remove('label--selected');
  }

  /**
   * Create Random ID
   * If you try to create a UUID, the code will be long, so I will omit it here
   * @private
   * @returns {string}
   */
  _createRandomId() {
    return Math.random().toString(32).substring(2);
  }

  /**
   * Dispatch that item check has been toggled
   * @private
   */
  _dispatchToggle() {
    this.dispatchEvent(new CustomEvent('onToggle',
      {
        detail: { id: this._id },
        bubbles: true,
        composed: true
      }
    ));
  }

  /**
   * Dispatch that item has been removed
   * @private
   */
  _dispatchRemove() {
    this.dispatchEvent(new CustomEvent('onRemove',
      {
        detail: { id: this._id },
        bubbles: true,
        composed: true
      }
    ));
  }

  /**
   * Get id
   * @returns {string} id
   */
  get id() {
    return this._id;
  }

  /**
   * Set label
   * @param {string} val
   */
  set label(val) {
    if (val) {
      this.setAttribute('label', val);
    } else {
      this.removeAttribute('label');
    }
  }

  /**
   * Checked or not
   * @returns {boolean}
   */
  get checked() {
    return this.getAttribute('checked') === '';
  }

  /**
   * Checked or not
   * @param {boolean} val
   */
  set checked(val) {
    if (val) {
      this.setAttribute('checked', '');
    } else {
      this.removeAttribute('checked');
    }
  }
}

// Register custom element
window.customElements.define('x-todo-item', Todo);
```