Web Componentsを実装する

# はじめに
Web Componentsは再利用可能でカプセル化されたコンポーネントを作る機能です。
Web Componentsを実装するには3つの機能を利用する必要があります。それぞれ別記事にて紹介しましたので是非ご覧ください。
- [Custom Elements](https://qiita.com/KokiSakano/items/1ffa65a1adb3f7c11112) コンポーネントの要素と動作を定義するための機能です。
- [Shadow DOM](https://qiita.com/KokiSakano/items/21e511313e64e70c2221) カプセル化を担う機能です。
- [HTML Template](https://qiita.com/KokiSakano/items/c8f75f4741beca9f02c5) 再利用を可能にする機能です。
この記事ではこれらの機能を組み合わせてWeb Componentsの実装を行います。

# 実装
例として`tier-table`という名前でtier表(ランキングボード)を作成する要素を作ります。細かな仕様は以下の通りです。
- 　複数の対象をA、B、Cの三段階で分類して表示する
- 　表示する内容は`tier-table`の中に`li`を用いてslotでA、B、Cのどこに分類するか指定する
- 　表の背景色はA、B、Cのそれぞれ`tier-table`に属性で指定することができる

`tier-table`を利用して実装したサンプルをまず紹介します。
```html
<button id="random-color">背景色をランダムに変更する</button>
<tier-table id="tier-table" color-a="#f88"  color-b="#fb8"  color-c="#fd8">
  <li slot="tier-a">find</li>
  <li slot="tier-a">forEach</li>
  <li slot="tier-a">map</li>
  <li slot="tier-a">reduce</li>
  <li slot="tier-a">filter</li>
  
  <li slot="tier-b">concat</li>
  <li slot="tier-b">some</li>
  <li slot="tier-b">every</li>
  <li slot="tier-b">fill</li>
  <li slot="tier-b">pot</li>
  <li slot="tier-b">push</li>
  
  <li slot="tier-c">at</li>
  <li slot="tier-c">of</li>
</tier-table>
```
```js
const randomButton = document.getElementById('random-color');
randomButton.addEventListener('click', () => {
  const tierTable = document.getElementById('tier-table');
  tierTable.setAttribute('color-a', randomColor());
  tierTable.setAttribute('color-b', randomColor());
  tierTable.setAttribute('color-c', randomColor());
});

function randomColor() {
  return "#" + Math.floor(Math.random() * 16777215).toString(16);
}

```

表示は以下のようになります。![スクリーンショット 2022-12-06 22.10.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/c512b0ab-2f27-b9cc-4b25-3db9a7c9dcee.png)
小要素にリストを記述して属性を与えるだけで決まった表示を得ることができました。liの内容をフルーツの種類などに変えれば同じデザインの他の表にすることができますし、属性を変更することでデザインは変えずに背景色だけ変更した表を作ることができます。

利用例を見て作るもののイメージがついたところで実装の説明に入ります。`template`は以下のようになっています。
```html
<template id="tmp">
  <div class="ul-a">
    <p>A</p>
    <ul>
      <slot name="tier-a"></slot>
    </ul>
  </div>
  <div class="ul-b">
    <p>B</p>
    <ul>
      <slot name="tier-b"></slot>
    </ul>
  </div>
  <div class="ul-c">
    <p>C</p>
    <ul>
      <slot name="tier-c"></slot>
    </ul>
  </div>
</template>
```
A、B、Cごとに`div`で分離してあります。`p`はA、B、Cの段階の説明、`ul`は`slot`で受け取るリストをまとめるようなHTMLのテンプレートを作成しました。
次に`tier-table`の機能の実装です。
```js
class TierTable extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({
      mode: 'open'
    }).appendChild(document.getElementById('tmp').content.cloneNode(true));
    this.setStyle();
  }
  static get observedAttributes() {
    return ['color-a', 'color-b', 'color-c'];
  }
  attributeChangedCallback(){
    this.setStyle();
  }
  setStyle() {
    const style = document.createElement('style');
    style.textContent = `
      div { border: 1px solid }
      .ul-a { background: ${this.getAttribute('color-a')} }
      .ul-b { background: ${this.getAttribute('color-b')} }
      .ul-c { background: ${this.getAttribute('color-c')} }
    `;
    this.shadowRoot.appendChild(style);
  }
}
customElements.define('tier-table', TierTable);
```
`constructor`では`tier-table`を`attachShadow`によってShadow DOM化を行い、そこに先ほど作成した`template`の中身をクローンしたものを割り当てています。最後に呼び出している`setStyle`はその時の属性をstyleに割り当てるものです。
さらに`tier-table`では属性の変更によってstyleを変更する必要があるので`observedAttributes`で属性を追跡して`attributeChangedCallback`で属性が変化したタイミングで`setStyle`を呼び出してstyleを最新のものへ変更しています。
最後にそのクラスを`CustomElementRegistry.define`で登録して完了です。

これで実装は完了です。それぞれの機能を把握した後に行うととても簡単に作ることができました。`tier-table`は読み込んだファイルであれば何度でもhtmlの要素のように呼び出すことが可能ですし、呼び出された中身もShadow DOMによって隠蔽(カプセル化）されました。これでWeb Componentsの作成は完了です。
サンプルの全貌は以下のCodePenを参照して下さい。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="eYKbwPK" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/eYKbwPK">
  Web Components</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# UIライブラリとの比較
ReactやVueなどのUIライブラリを利用している方はWeb Componentsをわざわざ作る利点がわからなかったと思います。Web ComponentsはUIライブラリに依存していないのでどんな環境であってもコンポーネントとして利用することができるという利点があります(カプセル化されているというメリットもあります)。逆にUIライブラリではデータとDOMの同期に対して高い能力を持っており、状態管理の機能と豊富なエコシステムが整っている利点があります。どちらも一長一短ですので、それぞれに合わせたユースケースで利用して下さい。

# まとめ
Web Componentsの実装を行いました。UIライブラリでは実現できなかった機能なども実現できるので状況に合わせて使ってみてはいかがでしょうか。