[Javascript] 動的に追加・削除される要素に対してスクリプトをあてるにはMutationObserverを使う

## はじめに
以前にShopify開発パートナーさんから、ある機能を追加してみたが動作しない、というご相談を受けました。Shopifyの構造自体よくわかっていないということもあり、勉強がてら解析してみました。どうやら目的の機能を追加するには動的に生成された要素に対して既存のスクリプトをあてる必要があることがわかりました。

このような**動的に追加されたり削除されたりする要素**（例えば、スクリプト中で`appendChild`したり、`removeChild`することは多々あると思います。私はよく行ないます。）に対して既存のスクリプトをあてる場合には、`MutationObserver`が有効[^1] です。今回はその備忘録です。

[^1]: 実案件では直接、管理画面でのコード編集ができない状況でした。Shopifyに限らず、CMSのプラグイン等で有効かと思いますが、機能追加によって生じる関連部分への影響をしっかりと検証することは必要ですね。

## MutationObserverとは
> MutationObserver とは、指定したコールバック関数を DOM の変更時に実行させる API です。この API は、DOM3 Events の仕様で定義されていた Mutation Events を新しく設計し直したものです。

指定した要素の監視役ですね。変更があった場合、条件に合致すれば任意の処理を実行します。今回は**機能追加したい要素の親要素**の監視をします。

## デモ
ボタンクリックで**動的に生成**される`<div id="id3">`に対して、以下のスクリプトをあてたいと思います。

``` Javascript
const showAlert = () => alert('clicked')
```

前提となるHTMLとJavascript。

```HTML
<div id="container">
  <input type="button" id="btn_genDiv" value="click">
  <!-- ここにid0～id9のdivを生成 -->
</div>
```

```Javascript
window.onload = () => {
  const showAlert = () => alert('clicked')
  const genDiv    = e => {
    const container = document.querySelector('#container')
    if (container.querySelectorAll('[id^=div]').length) return
    for (let i = 0; i < 10; i++) {
      let div = document.createElement('div')
      container.appendChild(div)
      div.id = `div${i}`
      div.innerText = i
    }
  }

  // ボタンクリクでdiv要素を生成
  const btn = document.querySelector('#btn_genDiv')
  btn.addEventListener('click', genDiv, false)

  /* 以降省略 */
}
```

### 動作しないコード

```Javascript
window.onload = () => {
  /* 省略 */
  const div3 = document.querySelector('#div3')
  div3.addEventListener('click', showAlert, false)
}
```

そもそも当該の要素が`window.onload`の処理時にDOMtreeに含まれていない[^2] ので`Uncaught TypeError`となります。
[^2]: `display: none;`や`visibility: hidden;`からの表示・非表示切り替えの場合、`window.onload`の処理時にはDOMtreeに含まれているためエラーになりません。

### 動作するコード

```Javascript
window.onload = () => {
  /* 省略 */
  const target = document.querySelector('#container') // スクリプト設定対象の親要素を設定
  const config = {childList: true, subtree: true} // 監視条件の設定

  //親要素に変化があった場合の処理設定
  const observer = new MutationObserver(() => {
    const div3 = document.querySelector('#div3')
    div3.addEventListener('click', showAlert, false)
  })

  // 監視開始
  observer.observe(target, config)
}
```
ボタンクリックでDIV要素が展開され、特定の要素`<div id="id3">`がクリックされると意図通りアラートダイアログが表示されます。

#### configの設定について
プロパティについては[MDN](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)にて確認できますが、以下に引用します。


|プロパティ|意味|
|:--|:--|
|**childList**|対象ノードの子ノード（テキストノードも含む）に対する追加・削除を監視する場合は true にします。|
|**attributes**|対象ノードの属性に対する変更を監視する場合は true にします。|
|**characterData**|対象ノードのデータに対する変更を監視する場合は true にします。|
|subtree|対象ノードとその子孫ノードに対する変更を監視する場合は true にします。|
|attributeOldValue|対象ノードの変更前の属性値を記録する場合は true にします（attributes が true の時に有効）。|
|characterDataOldValue|対象ノードの変更前のデータを記録する場合は true にします（characterData が true の時に有効）。|
|attributeFilter|すべての属性の変更を監視する必要がない場合は、（名前空間を除いた）属性ローカル名の配列を指定します。|

注意点として`childList`、`attributes`、`characterData`のいずれか１つ、かつ値を`true`に設定する必要があります。

今回は子ノードのみの監視ですが、子孫ノードも含める場合は`subtree: true`を追加すれば良いです。

### 結果
<p class="codepen" data-height="351" data-theme-id="dark" data-default-tab="js,result" data-user="STSHISHO" data-slug-hash="zYoWOjy" style="height: 351px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="MutationObserver Sample">
  <span>See the Pen <a href="https://codepen.io/STSHISHO/pen/zYoWOjy">
  MutationObserver Sample</a> by STSHISHO (<a href="https://codepen.io/STSHISHO">@STSHISHO</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 最後に
サンプルのため特定の要素`<div id="id3">`が確実に読み込まれることを前提として記述していますので、追加されることが不確実な場合は存在判定が必要になるでしょう。

[JSFiddle](https://jsfiddle.net/STSHISHO/oxmajn6f/)でもサンプル公開中です。

https://jsfiddle.net/STSHISHO/oxmajn6f/

### [参考]
+ [MutationObserver - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)