DOMを監視して、要素の追加や削除を検知する方法【Javascript/MutationObserver】

JavaScript

Javascriptで動的に追加した要素にイベントを設定したい!または追加した要素が存在すればこういった処理をしたい。ってことが時々あります。

その場合、どうやって実装すれば良いか調べたのでメモ。
DOMの変更を監視する「MutationObserver」というAPIを使えばできそうです。

基本の構文

基本的な構文は次の通りとなります。

//オプション
const options = {
    childList: true, //直接の子の変更を監視
    characterData: true,  //文字の変化を監視
    characterDataOldValue: true, //属性の変化前を記録
    attributes: true,  //属性の変化を監視
    subtree: true, //全ての子要素を監視
}
//コールバック関数
function callback(mutationsList, observer) {
    for(const mutation of mutationsList) {
        // 処理
        mutation.target //ターゲット要素
        mutation.addedNodes //追加されたDOM
        mutation.removedNodes //削除されたDOM
    }
    //ターゲット要素の監視を停止
    obs.disconnect();
}

//ターゲット要素をDOMで取得
const target = document.querySelector(<selector>);
//インスタンス化
const obs = new MutationObserver(callback);
//ターゲット要素の監視を開始
obs.observe(target, options);

MutationObserverに設定したコールバック関数は、ターゲットに指定した要素内のDOMが変更されるたびに呼び出されます。

const obs = new MutationObserver(callback);

そして、observeメソッドで、監視する要素を指定します。

obs.observe(<監視するターゲット>, options);

optionでどの範囲の変更を監視するか設定します。
childList(直接の子の変更を監視)あたりを設定しておけば大丈夫そうです。

//オプション
const options = {
    childList: true, //直接の子の変更を監視
    characterData: true,  //文字の変化を監視
    characterDataOldValue: true, //属性の変化前を記録
    attributes: true,  //属性の変化を監視
    subtree: true, //全ての子要素を監視
}

コールバック関数の引数「mutationsList」には監視する要素、要素内で追加・削除されたDOMの情報などが格納されています。

//コールバック関数
function callback(mutationsList, observer) {
    for(const mutation of mutationsList) {
        // 処理
        mutation.target //ターゲット要素
        mutation.addedNodes //追加されたDOM
        mutation.removedNodes //削除されたDOM
    }
    //ターゲット要素の監視を停止
    obs.disconnect();
}

ターゲット要素はオブジェクトでわたってきます。
プロパティをforで1つずつ取り出して処理します。

文字の変更を感知する簡単なサンプル

練習用に簡単なサンプルを作成しました。
以下は、入力フォームっぽく見せたDIV要素です。contenteditable属性で編集可能にしています。
このDIVをターゲット要素に指定します。

See the Pen MutationObserver by donguri2020 (@m-ke) on CodePen.

編集したタイミングで、変更された文字が表示される簡単なサンプルです。
「要素の監視をストップする」ボタンをクリックすると要素の監視が終了します。

追加した要素にイベントを設定する(サンプル)

先程のサンプルは「MutationObserver」を使わなくても実装できます。

次のサンプルは、追加した要素に自身を削除するクリックイベントを設定しています。
また、追加した要素が存在すれば「リセット」ボタンを表示し、要素が全て削除されたら非表示にします。
なお、リセットボタンをクリックしたら、要素が全て削除され、順番(●番目)の数字もリセットされます。

実際の動作はこちらでご確認ください!(別ウィンドウが開きます)

ポイントとなる箇所をメモしておきます。
以下はコールバック関数です。ターゲット要素に変更があった時(DOMが追加or削除された時)に実行されます。

_ovserverEvent(mutationsList, observer) {
    for(const mutation of mutationsList) {
        // 子要素を取得
        this.addElms = mutation.target.children;

        if (this.addElms.length) {
            for(const addElm of this.addElms) {
                addElm.addEventListener("click", function () {
                    addElm.remove();
                });
            };
            //リセットボタン表示
            this.DomResetBtn.classList.remove("invisible");
        } else {
            //リセットボタン非表示
            this.DomResetBtn.classList.add("invisible");
        }
    }

}

ここでは追加された要素にイベントを設定し、要素が存在していればリセットボタンを表示しています。

リセットボタンがクリックされた時の処理は次の通りです。

_resetElm() {
    //数増減リセット
    this.increment = this._incrementNum();

    //追加した要素を削除
    const addElms = Array.prototype.slice.call(this.addElms);
    addElms.forEach(addElm => {
        addElm.remove();
    });
}

ターゲット要素の子要素addElmsは配列ではなく、HTMLCollectionというオブジェクトのような型になっています。
ここでは配列に変換してコードが短くなるようにしました。

まとめ

DOMを監視できるようになると、サンプルで作ったように後から挿入した要素に対して操作が可能です。

特に、外部から読み込んだスクリプトが生成した要素にこんな処理をしたい!って時に有効かと思います。