【Web Componentsを学ぶ】Custom Elements編

# はじめに
この記事ではWeb Componentsを利用する準備のためにCustom Elementsを紹介する記事です。Web Componentsとは他のコードから独立して利用できる再利用可能なコンポーネント(要素)を指します。
Web Componentを利用するために必要な他の要素として[Shadow DOM](https://qiita.com/KokiSakano/items/21e511313e64e70c2221)、[HTML Template](https://qiita.com/KokiSakano/items/c8f75f4741beca9f02c5)についての記事も書きましたので併せてご覧ください。また、Web Components自体の実装は[こちらの記事](https://qiita.com/KokiSakano/items/334e0b3af7d72ee4d230)で行いました。

# Custom Elementsとは
Custom ElementsはJavaScriptに実装されたAPIの一つです。あらかじめ機能を定義した要素をHTMLの標準要素と同じように利用することができます。要素の名前はkebab-caseである必要があり、標準の要素との区別のために必ず2つ以上の単語で構成されている必要があります。
Custom Elementsは二種類の方法で利用することができます。
## Autonomous custom element(自律カスタム要素)
標準の要素を利用しない独立した要素を定義する方法です。ReactなどのUIライブラリを利用する方にとって親近感のあるCustom Elementsです。
例えばこのAPIを用いてcustom-elementという要素を定義した場合以下のように利用することができます。
```html
<custom-element></custom-element>
```

# Customized built-in element(カスタム組み込み要素)
標準の要素を拡張するように機能を定義する方法です。定義した要素のis属性に作成したCustom Elementの名前を代入することで利用できます。例えばdiv要素を拡張して以下のように利用することができます。
```html
<div is="custom-element"></div>
```

# Custom Elementsの制御
Custom Elementsは`CustomElementRegistry`インターフェイスを利用して制御を行います。このインターフェイスには`window.customElements`からアクセスすることができます。このプロパティには4つのメソッドがあります。
## define
Custom Elementsの定義を行うことができます。
Autonomous custom elementの場合は以下のように定義します。
```js
customElements.define('custom-element', CustomElement);
```
Customized built-in elementの場合は以下のように定義します。
```js
customElements.define('custom-element', CustomElement, { extends: 'div' });
```
第一引数はCustom Elementsを判別するための名前を渡します。先述の通りkebab-caseで2つ以上の単語で構成する必要があります。
第二引数はCustom Elementsの機能などを定義した要素のクラスを渡します。こちらについては後ほど紹介します。
第三引数はCustome Elementsを制御するためのオプションをオブジェクトで渡します。現在はextendsしか対応していないので、Customized built-in elementを利用する場合のみ使用する引数となっています。extendsをキーとしてCustom Elementsを組み込む要素をバリューとしたオブジェクトを渡します。Autonomous custom elementの場合は利用しません。
## get
定義したCustom Elementsのクラスを取得する関数です。
```js
customElements.get('custom-element')
```
第一引数にCustom Elementsの名前をとり、存在すればその要素のクラスを返します。存在しなければundefinedです。
## upgrade
Custom Elementsの定義を引数の要素に反映させます。
```js
const targetElement = document.createElement('custom-element');

customElements.define('custom-element', CustomElement);

// false
console.log(targetElement instanceof CustomElement)

customElements.upgrade(targetElement);

// true
console.log(targetElement instanceof CustomElement)
```
上のコードはupgradeによってtargetElementが定義された時点で定義されていないcustom-elementsが定義された要素として扱われるようになっています。

## whenDefined
指定した名前の要素が定義されると解決されるPromiseを返すメソッドです。
```js
const defineElement = () => setTimeout(() => {
    console.log(1);
    customElements.define('custom-element', CustomElement);
}, 500);

customElements.whenDefined('custom-element').then(() => console.log(2));
defineElement();
```
約500ms後に発火してcustom-elementを定義するような関数をwhenDefinedメソッド呼び出したのちに呼び出しました。1の後に2が呼び出されていることから定義が完了した後に呼び出されているのがわかります。
比較のためにCustom Elementsが関係ないsetTimeoutとPromiseの実行順を見てみると、2の後に1が呼び出されているので確かに先ほどの例を用いた説明は正しそうということがわかります(曖昧な言い方をしたのは完全に正しいことはこのコードでは保証されていないからです。)。
```js
const defineElement = () => setTimeout(() => {
    console.log(1);
}, 500);
Promise.resolve().then(() => console.log(2));
defineElement();
```
# Custom Elementsのクラス
Custom Elementsの機能などを実装するためにはHTMLの要素を継承したクラスを作成する必要があります。このクラスは先ほど確認したCustom Elementsの定義(`CustomElementRegistry.define`)で第二引数として渡されます。クラスを利用するのでES6以降の環境でなければいけないことに注意して下さい。
継承するクラスはAutonomous custom elementを利用する場合はHTMLElementを、Customized built-in elementの場合は組み込む要素のクラス(HTMLDivElementなど)を用います。
基本的な機能の実装は`constructor`、`connectedCallback`、`disconnectedCallback`、`adoptedCallback`、`attributeChangedCallback`の5つメソッドに行います。それぞれに割り当てられたライフサイクルのタイミングで実行されます。
## constructor
要素が作られるタイミングで発火するメソッドです。利用する場合は内部でsuper()を必ず呼び出す必要があります。
## connectedCallback
DOMに追加されるタイミングで発火するメソッドです。
## disconnectedCallback
DOMから削除されるタイミングで発火するメソッドです。
## adoptedCallback
DOMから移動するタイミングで発火するメソッドです。
## attributeChangeCallback
属性が追加、変更、削除されたタイミングで発火するメソッドです。変更を監視する属性は明示的に定義する必要があリます。`observedAttributes`を用いて定義することができます。

これらのメソッドが指定したライフサイクルで発火されることを確認するためにサンプルを作成しました。`adoptedCallback`はなかなか使う機会がないことと、条件の設定が難しいためサンプルでは定義しておりません。`attributeChangeCallback`はどんな属性でも変化を追跡しているわけではなく、`static get observedAttributes`によって返される配列に含まれる名前の属性だけ追跡してくれることに注意して下さい。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="JjZeXxe" 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/JjZeXxe">
  CustomElementDemo</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>

CustomElementを作成(new）することで`constructor`が発火すること、作成したCustomElementをDOMに追加(appendChild)ことで`connectedCallback`が発火すること、作成したCustomElementをDOMから削除(removeChild)することで`disconnectedCallback`が発火すること、属性の追加、変更、削除を監視して`attributeChangeCallback`が発火することを確認できます。CustomElementを利用するために先ほど紹介した`CustomElementRegistry.define`を利用する必要があることに注意が必要です。

# まとめ
Web Componentsを利用するために必要なCustom Elementsを学びました。この機能を用いてHTMLにはない自分で作成した要素を利用できるようになります。