JSでレンダリングしてMutationObserverでaddEventListenerする

[JSでレンダリングしてaddEventListenerする方法](https://qiita.com/nbstsh/items/3db678c812c02922898a)について投稿した際、@megmismさんからMutationObserverでも実現可能と助言をいただきました。

恥ずかしながらわたくしMutationObserver初耳でした、、、

ということで今回は、練習も兼ねて「JSでレンダリングした要素にclickイベントを付与する」という以前の記事と同様のシチュエーションをこのMutationObserverで実現してみたいと思います。

「MutationObserverって何？」となった自分のようなJS初学者の方向けです。


# MutationObserverの基本

[MDNのドキュメント](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)には
>MutationObserver とは、指定したコールバック関数を DOM の変更時に実行させる API です。

と記載されています。MutationOberverを使用することで、特定のhtml要素を監視し、変更が生じた際に何かしらの処理を実行することが可能になるわけですね。

基本的な使い方は以下のようになります。

```js

// observerインスタンスを作成
const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        //　DOMの変更時に行いたい処理
    })
})

// 監視対象のhtml要素を取得
const observedEl = document.querySelector('#observed')
// 監視する際の設定値
const config = { attributes: true, childList: true, characterData: true };
// 監視対象と設定を引数に渡して、監視を開始
observer.observe(observedEl, config)
```

詳しい使用法については、以下の記事が参考になると思います。非常にわかりやすいのでおすすめです。

- [JavaScriptのMutationObserverでDOMの変化を監視する方法] (JavaScriptのMutationObserverでDOMの変化を監視する方法)


```js

new MutationObserver((mutations) => {})
//mutations = MutationRecordの配列
```

MutationObserverのコールバック関数の第一引数には MutationRecord の配列が入ります。
[MDNのドキュメント](https://developer.mozilla.org/ja/docs/Web/API/MutationRecord)を参照しますと、

>MutationRecord とは、DOM に生じた個々の変更を表すオブジェクトです。このオブジェクトは MutationObserver のコールバック関数に渡されます。

とあります。つまり、このMutationRecordオブジェクトを操作することでDOMの変更に関する情報を取得することができます。



# 今回やったこと

1. 監視対象のdivを用意
2. 監視対象のdivにDOM変更時の処理を書く
3. 監視を開始する
4. 監視対象のdivに要素を追加していく


## 1. 監視対象のdivを用意

```html:mutation-observer.html
<div id="observed">
    <h1>observed div</h1>
</div>
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/329068/43f267d4-e05b-92cf-6223-5ffb2da738ca.png" width="70%">

id='observed'とした監視対象のdivを用意します。

## 2. 監視対象のdivにDOM変更時の処理を書く

監視対象のdivに変更が生じた際に実行したい処理は、

1. 追加された要素がid='target'だった場合,
2. clickイベントを追加する (クリックしたらalertする単純なもの)

です。この処理を実現する関数``addClickEvent()``を実装します。


```js:mutation-observer.js

const addClickEvent = (mutation) => {
    // 監視対象のdivに追加された要素を配列で取得
    const addedNodes = Array.from(mutation.addedNodes)
    // clickイベントの付与対象となる要素を取得
    const targetNodes = addedNodes.filter((addedNode) => addedNode.id === 'target')
    // 対象となる要素が存在しなければ終了
    if (targetNodes.length === 0) return 
  
    targetNodes.forEach((targetNode) => {
        // clickイベントを付与
        targetNode.addEventListener('click', (e) => { 
            alert('clicked !!!!!')
        })
    })
}
```

順をおって説明しますと、

```js
// 監視対象のdivに追加された要素を配列で取得
const addedNodes = Array.from(mutation.addedNodes)
```

``MutationRecord.addedNodes``には、監視対象のDOMに追加された要素が``NodeList``で格納されています。(arrayではない！) この後、idでフィルタリングをしたいのでArray.from()を使用して、このNodeListをarrayに変換しておきます。

```js
// clickイベントの付与対象となる要素を取得
const targetNodes = addedNodes.filter((addedNode) => addedNode.id === 'target')
```
追加された要素の中からidが'target'のものを取得します。

```js

// 対象となる要素が存在しなければ終了
if (targetNodes.length === 0) return 

targetNodes.forEach((targetNode) => {
    // clickイベントを付与
    targetNode.addEventListener('click', (e) => { 
        alert('clicked !!!!!')
    })
})
```

idが'target'の要素が存在した場合に、それらにクリックイベントを付与しています。

これで監視対象のDOMに変更が生じた際の処理が完成です。


##3. 監視を開始する

```js:mutation-observer.js
// observerインスタンスを作成
const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        addClickEvent(mutation)
    })
})

// 監視対象のdivを取得
const observedEl = document.querySelector('#observed')
// 監視の設定
const config = { childList: true }
// 監視開始
observer.observe(observedEl, config)
```

MutationObserverのインスタンスを作成します。コンストラクタの引数に渡すコールバック関数内で、DOMに変更が生じた際に、先ほど実装した``addClickEvent()``を呼び出します。

監視対象と設定値を引数に渡して``MutationObserver.observe()`` を呼び出し、監視を開始します。今回の監視対象は``div#observed``ですので、この要素を取得し、設定値``config``と共に``observe``に渡しています。設定値に関しては、必要最低限のものだけに絞ってます。

> childList : 対象ノードの子ノード（テキストノードも含む）に対する追加・削除を監視する場合は true にします。


今回のケースでは子ノードの追加を監視したいだけなので ``{ childList: true } ``のみとしています。その他、設定値の詳細に関しては[こちら](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)をどうぞ。



## 4. 監視対象のdivに要素を追加していく

新しい要素を作成する``generateElement()``を用意します。

```js:mutation-observer.js

const generateElement = ({ tag = 'p', textContent, className, id }) => {
    const newEl = document.createElement(tag)
    
    if (textContent) newEl.textContent = textContent
    if (className) newEl.classList.add(className)
    if (id) newEl.id = id

    return newEl
}

```

``generateElement()``は引数に渡したタグ名、テキスト、クラス名、id名をもつhtml要素を作成し、返します。デフォルトでpタグを返すようにしてあります。

id="target"のターゲット要素を監視対象のdivに追加する関数も用意します。

```js:mutation-observer.js

const renderTarget = () => {
    // clickイベント付与対象の要素を作成
    const newEl = generateElement({
        textContent: 'target element',
        className: 'target',
        id: 'target'
    })
    // ターゲット要素を監視対象のdivに追加
    document.querySelector('#observed').append(newEl)
}
```

(cssのためにclassも付与していますが、今回の本筋とは関係ありません。)

id="target"の要素にのみclickイベントが付与されているか確認するため、ダミーの要素をdivに追加する関数も用意しておきます。

```js

const renderDummy = () => {
    // dummyの要素を作成
    const newEl = generateElement({
        textContent: 'dummy element',
        className: 'dummy',
        id: 'dummy'
    })
    // ダミー要素を監視対象のdivに追加
    document.querySelector('#observed').append(newEl)
}
```

これでレンダリングする準備が整いました。

### htmlを調整

わかりやすくするために要素追加用の作成し、ボタンを押したら監視対象のdivに要素が追加されるようにします。


ダミー要素、ターゲット要素それぞれを追加するボタンを作成します。

```html
<div id="observed">
    <h1>observed div</h1>
    <button id="generate-dummy">generate dummy</button>
    <button id="generate-target">generate target</button>
</div>
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/329068/4ae473a0-79a7-53df-6206-c6e7ff271230.png" width="70%">


generate dummyボタンをクリックしたら``renderDummy()``でダミー要素をレンダリングし、

```js:mutation-observer.js
// generate dummy ボタンをクリックしたらダミー要素を追加
document.querySelector('#generate-dummy').addEventListener('click', (e) => {
    renderDummy()
})
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/329068/0a7dc2bf-0e35-2952-0385-b05de57280b9.gif" width="70%">

generate dummyボタンをクリックしたら``renderTarget()``でターゲット要素をレンダリングします。

```js:mutation-observer.js
// generate target ボタンをクリックしたらターゲット要素を追加
document.querySelector('#generate-target').addEventListener('click', (e) => {
    renderTarget()
})
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/329068/7f1c37dd-1c18-ffdf-70ec-834f97edb7fe.gif" width="70%">


## 動作確認

これで完成です。きちんと動作しているか確認します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/329068/2ca4df7d-a791-7273-ed1a-f376fc0fa404.gif" width="80%">

追加したターゲット要素にのみclickイベントが付与されていますので、OKです。


# まとめ

MutationObserverを初めて使用してみましたが、DOMの変更をより細かいレベルで監視・制御する際に重宝しそうです。

[MutationRecordオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/MutationRecord)を使用すれば、属性値や削除されたノードの情報、変更前の値など様々な情報を取得できますし、MutationObserverのconfigもより細かく設定することが可能です。今回の例で触れていない項目がいくつもあるのでもう少し遊んでみたいと思います。


#参考リンク


- [MutationObserver : MDNドキュメント](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)
- [MutationRecord : MDN ドキュメント](https://developer.mozilla.org/ja/docs/Web/API/MutationRecord)
- [JavaScriptのMutationObserverでDOMの変化を監視する方法] (JavaScriptのMutationObserverでDOMの変化を監視する方法)