↗️
【Vanilla JS - MutationObserver】動的に追加されたDOMにeventを仕込みたい
背景
- Vueで作られたフロントアプリケーションに、特定のDOMにeventを仕込むVanilla JS製の独自プラグインを入れたい
- 考えなく
addEventListenr
を使用した場合、pluginが読み込まれるタイミングでは狙いたいdomがまだ描画されていないケースがある
結論
-
MutationObserver
を使う - https://developer.mozilla.org/ja/docs/Web/API/MutationObserver
- どのブラウザも対応してそう
Point
-
observe
関数のoptionにsubtree: true
を指定する- これにより孫要素以降も監視対象となる
実装
- ここではdata属性を使用してDOMの特定を行なっています
- 変更が分かりやすいように意図的に処理を遅延させている箇所があります
plugin.js
document.addEventListener("DOMContentLoaded", () => {
const targetContainer = document.querySelector(
'[data-dynamic-elements-continer="true"]'
);
const observer = new MutationObserver((records) => {
records.forEach((record) => {
// 追加されたnodeに対しての処理
record.addedNodes.forEach((addedNode, i) => {
if (addedNode instanceof Element) {
const targets = addedNode.querySelectorAll(
'[data-dynamic-element="true"]'
);
targets.forEach((target) => {
target.addEventListener("click", (e) => {
alert(`click!!: ${e.target.dataset.index}`);
});
});
}
});
});
});
observer.observe(targetContainer, { childList: true, subtree: true });
});
Usage
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<!-- ... -->
<!-- new!! -->
<script src="./plugins/plugin.js" defer></script>
</body>
</html>
App.vue
<template>
<div className="App">
<h1>Mutation Observer Playground</h1>
<div data-dynamic-elements-continer="true">
<DynamicChild
v-for="(data, i) in data"
:key="data"
:label="data"
:index="i"
/>
</div>
</div>
</template>
<script>
import DynamicChild from "./components/DynamicChild.vue";
export default {
name: "App",
components: {
DynamicChild,
},
data() {
return {
data: [],
};
},
async mounted() {
// 数秒止める
await this.sleep(1000);
this.data = ["child1", "child2", "child3"];
},
methods: {
sleep(waitTime) {
return new Promise((resolve) => setTimeout(resolve, waitTime));
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
DynamicChild.vue
<template>
<div>
<p v-if="loading">loading...</p>
<div v-else>
<p class="button" data-dynamic-element="true" :data-index="index + 1">
{{ label }}
</p>
</div>
</div>
</template>
<script>
export default {
props: ["label", "index"],
data() {
return {
loading: true,
};
},
async mounted() {
// 数秒止める
await this.sleep(2000 * this.index || 0);
this.loading = false;
},
methods: {
sleep(waitTime) {
return new Promise((resolve) => setTimeout(resolve, waitTime));
},
},
};
</script>
<style scoped>
.button {
cursor: pointer;
}
</style>
後書き
- 背景としてはレアケースな気はする
- 昨今のフロントFWとは別にモジュール作りたいケースがあれば知っておいても損は無いかも?
Discussion