この記事は、トラストバンク Advent Calendar 2021の3日目です。
この度、ふるさとチョイスのトップページや控除金額シミュレーションに控除上限額の中でもらえるお礼の品の組み合わせをおすすめする機能(以降、お礼の品の組み合わせ表示と省略)がリリースされました。
忙しい年末のふるさと納税に、ぜひご活用ください!
お礼の品の組み合わせ表示を制作するにあたり、JavaScriptの監視インスタンスであるMutationObserver
を利用しました。
複雑なDOM操作をシンプルに実装することができるMutationObserver
の活用方法を、お礼の品組み合わせ表示の紹介も交えつつ説明します。
お礼の品の組み合わせ表示の複雑な状態管理
お礼の品の組み合わせ表示とは、控除上限額に合わせてお礼の品を組み合わせ、合計金額と共に表示する機能です。
表示された組み合わせは、入れ替えるボタンや削除ボタンなどを用いて自分好みにカスタマイズ。
寄付するリストに追加をクリックすることで、一括でお礼の品を寄付するリストに追加することができます。
制作時に悩まされたのは、お礼の品が入れ替わる条件や動作の多さです。
全ての動作に対して個別に寄付金総額や品数の変更を実装すると、更新漏れやソースの難読化に繋がります。
そこで活用するのがMutationObserver
です。
<div id="mutation-wrap"></div>
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
// ここに寄付金総額と品数の再計算を記述する
});
observer.observe(mutationWrapElement, {
childList: true
});
#mutation-wrap
にDOMが追加されるたび、MutationObserver.observe
のcallbackとして指定した関数が実行され、寄付金総額と品数が再計算されます。
再計算を実行する条件が、#mutation-wrap
にDOMを追加・削除するである限り、新たにJavaScriptを書く必要はありません。
今回は、親となるDOMの直下に発生する操作のみを監視したいため、オプションにはchildList
を追加しました。
他にも指定できるオプションは様々です。
オプション名 | 内容 |
---|---|
subtree | 孫を監視対象に含める |
attributes | 属性値の変更を監視 |
attributeOldValue | 属性値が変更された際、変更前の値を取得する |
attributeFilter | 監視したい属性値をフィルタリングする |
characterData | テキストの変更を監視 |
characterDataOldValue | テキストが変更された際、変更前の値を取得する |
参考:DOM Living Standard - Interface MutationObserver
MutationObserver
を用いる際の注意事項
工夫次第で何にでも活用できるMutationObserver
ですが、利用にあたって注意すべき点がいくつか存在します。
DOMの追加・削除に伴うパフォーマンスの低下
監視したいDOMを単体で指定することができないため、DOMが操作された分だけcallback関数が実行されます。
Document.createDocumentFragment
やElement.innerHTML
などを用い、なるべく一括で操作を実行するようにします。
// NG
children.forEach(child => {
// childを追加した分実行される
obseverWrapper.appendChild(child);
});
children.forEach(child => {
// childを削除した分実行される
obseverWrapper.removeChild(child);
});
// OK
const fragment = document.createDocumentFragment();
children.forEach(child => {
fragment.appendChild(child);
});
// 追加を1回にまとめたので、observeも1回だけ実行される
obseverWrapper.appendChild(fragment);
// 削除を1回にまとめたので、observeも1回だけ実行される
obseverWrapper.innerHTML = '';
参考:DOM Living Standard - Interface DocumentFragment
無限ループに陥る場合がある
callback関数の中にMutationObserver.observe
で監視しているDOMに対する処理を記述すると、場合によっては無限ループに陥ります。
MutationObserver.disconnect
を用いて、一時的に監視を止めてから処理を実行しましょう。
// NG
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
// 属性値の変更が監視されているので、無限ループに陥る
mutationWrapElement.children[0].classList.add('view');
});
observer.observe(mutationWrapElement, {
childList: true,
attributes: true
});
// OK
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
// 監視を停止
observer.disconnect();
// 監視が停止しているので、無限ループを回避できる
mutationWrapElement.children[0].classList.add('view');
// 監視を再開
observer.observe(mutationWrapElement, {
childList: true,
attributes: true
});
});
observer.observe(mutationWrapElement, {
childList: true,
attributes: true
});
まとめ
JavaScriptには他にも、交差監視を実現するIntersectionObserver
やサイズの変化を監視するResizeObserver
など、多くの監視インスタンスが実装されています。
それぞれをうまく取り入れることで、いつかはフレームワークを利用しなくても、直感的で見やすいソースコードが書ける時代が来るかもしれません。
明日の記事は、@a_wis1056さんです。お楽しみに。