Javascriptでchageイベントの取れないhidden項目やiframeの変化をMutationObserverで取得し処理する

HTMLのinput要素の値が書き換わった時に、何かの処理を行いたい場合、changeイベントで処理しますが、type属性がhiddenになっている場合、changeイベントが発火しないため、値が書き換わったら何かの処理を行うという事ができません。

iframeで読み込んでいるページの中身が変化したら処理を行いたいという場合もあるでしょう。
(私はiframeで読み込んでいるページに処理を追加できない構成でiframeの中身が変化したら親ページでの処理が必要でした)

これらは、MutationObserverを利用する事で解決します。

## MutationObserver
MutationObserverはDOMが変化した時に処理を行うAPIです。
(APIと言っても何かを新たに読み込んだりする必要はありません)
Chrome, Edge, Firefox, Safari, IE11でも対応しているので安心です。

[MDN MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)

### 使い方
まずは、何が変化した時に処理を行うかを決めます。変化した時に処理を行いたい物をtrueにします。

- childList
- attributes
- characterData
- subtree
- attributeOldValue
- characterDataOldValue
- attributeFilter

詳細：[MutationObserverInit](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver#MutationObserverInit)

## 実装
### hidden項目の場合
今回は、Addボタンを押したらcounterの値が+1ずつ追加され、counterの数が5で割り切れる時に、Rankが1つ上がる。Rank上限は5
という処理を作ってみます。(実際はAddボタンを押した時に処理すればいいですが...)

```HTML
<input type="hidden" name="counter" value="0">
<input type="button" name="add" value="Add">
<p>Rank: <input type="number" name="rank" value="1" readonly></p>
```

#### 要素の定義

counter: クリックするたびに+1されるinputを定義
rank: ランクの値を表示するinputを定義

```Javascript
const counter = document.querySelector('[name=counter]');
const rank = document.querySelector('[name=rank]');
```

#### MutationObserverの設定

指定したDOM要素に変更があった時に行う処理です。
mutationsには、変更されたDOM要素が配列で入って来るためループし、変更された属性(attributeName)がvalueの時がinputの値が変わった時なので、続きの処理を行っています。

```Javascript
const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        rank.value = Math.floor(parseInt(mutation.target.value, 10) / 5) + 1;
        if (rank.value == 5) observer.disconnect();
    });
});
```

rankの最大値を5にしているので、もしもrankの値が5に変わったら、DOMの監視を終了させています。
DOMの監視が不要になったら終了しましょう。

```Javascript
observer.disconnect();
```

このconfigでtrueを指定した物が、監視対象となります。
今回はvalue値の変化だけを監視するので、属性の変化を監視するattributesをtrueにし、attributeFilterにて監視るる属性をvalueに限定させています。

監視対象をなるべく絞り込んで置く事で

```Javascript
const config = {
    attributes: true,
    attributeFilter: ['value']
};
```

そして、監視を開始します。

```Javascript
observer.observe(counter, config);
```

以上をまとめた物がこちらです。

```HTML
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <title>Rank</title>
    </head>
    <body>
        <input type="hidden" name="counter" value="0">
        <input type="button" name="add" value="Add">
        <p>Rank: <input type="number" name="rank" value="1" readonly></p>
        <script>
            (() => {
                'use strict';
                const counter = document.querySelector('[name=counter]');
                const rank = document.querySelector('[name=rank]');
                
                const observer = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        rank.value = Math.floor(parseInt(mutation.target.value, 10) / 5) + 1;
                        if (rank.value == 5) observer.disconnect();
                    });
                });
                const config = {
                    attributes: true,
                    attributeFilter: ['value']
                };
                observer.observe(counter, config);

                document.querySelector('[name=add]').addEventListener('click', () => {
                    counter.value = parseInt(counter.value, 10) + 1;
                });
            })();
        </script>
    </body>
</html>
```

### valueの値が書き換わっていない時

```Javascript
document.querySelector('[name=hoge]').value = "fuga";
```
もしも、[name=hoge]の値変化を監視していて、上記のように値をセットした時、
画面上ではinputの値が変化しているのに、DOMが書き換わっておらず、MutationObserverで変更処理が走らない場合があります。

その時は、このようにsetAttributeを使う事でDOMが変化し、変更処理が走るようになります。

```Javascript
document.querySelector('[name=hgoe]').setAttribute('value', 'fuga');
```
### iframeの中身の変化
iframeの中身が変わった時の処理はjQueryを利用すると楽に実装できます。
iframeのbodyの中身が変化した時の処理ですが、iframeの読み込みが終わるのをsetIntervalで待ってから、処理を行っています。

```HTML
<iframe src="#" id="frame"></iframe>
<script>
    const frameLoading = () => {
            setInterval(() => {
            if ($('#frame').contents().find('body') != 'undefined') {
                clearInterval(frameLoading);
                const target = $('#frame').contents().find('body');
                const config = {
                    attributes: true,
                    characterData: true,
                    childList: true,
                    subtree: true
                };
                new MutationObserver((mutations) => {
                    // do something.
                    console.log(mutations);
                }).observe(target, config);
            }
        }, 100);
    };
    frameLoading();
</script>
```