span の中の文字列が変わったというイベントをトリガーにして処理したかったので調べました。 jQuery を使う前提で調べたのですが、調べた結果、 jQuery を使わない方法 (MutationObserver) になりました。 調べたことを書いておきます。
環境
- jQuery 3.3.1
- Microsoft Edge 16.16299
- Firefox 59.0.2
MutationEvent
最初、次のイベントが検索で見つかりましたので、実装してみました。
MutationEvent - Web APIs | MDN
次のようなファイルを作成しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MutationEvent</title>
</head>
<body>
<h1>MutationEvent</h1>
<hr>
<form>
<input type="text" id="text1" value="value1">
<span id="span1">span1</span>
</form>
<hr>
<script src="//code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script>
$(function () {
$('#text1').on('change', function () {
$('#span1').text($(this).val());
});
var callback = function (event) {
console.log('DOMSubtreeModified')
};
// Firefox の警告
// Mutation Event の使用は推奨されません。代わりに MutationObserver を使用してください。
$('#span1').on('DOMSubtreeModified', callback);
// document.querySelector('#span1').addEventListener('DOMSubtreeModified', callback); // jQuery を使わない場合
})
</script>
</body>
</html>
text の値を変えて、フォーカスを移動すると、 text の値が span に表示されて、コンソールに “DOMSubtreeModified” と出力されます。
なぜか 2 回も出力されます。
そして、 code blocks のコメントにも書きましたが、 Firefox で実行すると、次の警告がコンソールに出力されます。 “Mutation Event の使用は推奨されません。代わりに MutationObserver を使用してください。” これは、 jQuery から出力されているようです。
Deprecated
This feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped. Avoid using it and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.
ここにも非推奨であることが記載されていました。
MutationObserver
なので、 MutationObserver について調べました。
MutationObserver - Web APIs | MDN
次のファイルを作成しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MutationObserver</title>
</head>
<body>
<h1>MutationObserver</h1>
<hr>
<form>
<input type="text" id="text1" value="value1">
<span id="span1">span1</span>
</form>
<hr>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
document.querySelector('#text1').addEventListener('change', function () {
document.querySelector('#span1').textContent = this.value;
});
// Select the node that will be observed for mutations
var targetNode = document.querySelector('#span1');
// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true, characterData: true };
// Callback function to execute when mutations are observed
var callback = function (mutationsList) {
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
} else if (mutation.type === 'characterData') {
console.log('characterData was modified.');
}
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
});
</script>
</body>
</html>
text の値を変えて、フォーカスを移動すると、 text の値が span に表示されて、コンソールに “A child node has been added or removed.” と出力されます。 2 回も出力されますけれども。
var config = { attributes: true, childList: true, characterData: true };
について、監視対象を選択できるようで、次のような意味になるようです。
- attributes: 対象ノードの属性に対する変更を監視する場合は true にします。
- childList: 対象ノードの子ノード(テキストノードも含む)に対する追加・削除を監視する場合は true にします。
- characterData: 対象ノードのデータに対する変更を監視する場合は true にします。
- subtree: 対象ノードとその子孫ノードに対する変更を監視する場合は true にします。
- attributeOldValue: 対象ノードの変更前の属性値を記録する場合は true にします(attributes が true の時に有効)。
- characterDataOldValue: 対象ノードの変更前のデータを記録する場合は true にします(characterData が true の時に有効)。
- attributeFilter: すべての属性の変更を監視する必要がない場合は、(名前空間を除いた)属性ローカル名の配列を指定します。
コンソールに出力された内容を見ると、 span の中の文字列を変更すると、 childList が変更されたことになるようです。
MutationObserver の characterData
最初、 span の中の文字列が変わった場合に処理をしたかったので、 characterData
を使えば良いと思って、 characterData
だけ true
にしました。
が、イベントが発生しませんでした。
childList
を true
にしたところ、イベントが発生したので、そういうものなのかな?と思いました。
childList
対象ノードの子ノード(テキストノードも含む)に対する追加・削除を監視する場合は true にします。
テキストノードの追加・削除の監視も含まれるようです。
だから childList
のイベントが発生しているようではあります。
Stack Overflow
じゃあ、 characterData
っていうのは、どういうときに発生するのか?
もう少し調べてみると、次のような場合に発生するようです。
You can observe a text node directly. In that case you don't need to observe childList. There are many cases where it could be useful, in a contenteditable element for example. Like this:
// select the target node var target = document.querySelector('#some-id').childNodes[0]; // create an observer instance var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { console.log(mutation.type); }); }); // configuration of the observer: var config = { attributes: true, childList: false, characterData: true }; // pass in the target node, as well as the observer options observer.observe(target, config); <div id='some-id' contenteditable='true'>Modify content</div>
javascript - MutationObserver characterData usage without childList - Stack Overflow
document.querySelector('#some-id')
ではダメで、 document.querySelector('#some-id').childNodes[0]
というテキストノードを MutationObserver.observe
の 1 つ目の引数に渡す必要があるようでした。
修正 1
Stack Overflow を参考に、次のファイルを作成しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MutationObserver</title>
</head>
<body>
<h1>MutationObserver</h1>
<hr>
<form id="form1">
<input type="text" id="text1" value="value1">
<span id="span1" contenteditable="true">span1</span><!-- ← contenteditable="true" を追加する -->
</form>
<hr>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
document.querySelector('#text1').addEventListener('change', function () {
document.querySelector('#span1').textContent = this.value;
});
// Select the node that will be observed for mutations
var targetNode = document.querySelector('#span1').childNodes[0]; // ← テキストノード
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: false, characterData: true }; // ← characterData だけ true にする
// Callback function to execute when mutations are observed
var callback = function (mutationsList) {
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
} else if (mutation.type === 'characterData') {
console.log('characterData was modified.');
}
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
});
</script>
</body>
</html>
初めて知ったのですが、 span って、 contenteditable="true"
の属性をつけることで、ユーザーが直接 span の中の文字列を変えることができるのですね。
ユーザーが span の中の文字列を変えると、コンソールに “characterData was modified.” と出力されます。
characterData
のイベントが発生しているようです。
しかし、 text の値を変えてフォーカスを移動すると、 span の中の文字列は変わるのに、コンソールに “characterData was modified.” とは出力されません。 (何も出力されません) イベントが発生していないようです。
document.querySelector('#span1').childNodes[0]
(テキストノード)に対してイベントリスナーを設定しているので、 document.querySelector('#span1').textContent = this.value
とすることで、イベントリスナーを設定したテキストノード自体が別のテキストノードと入れ替わってしまっているように見えました。
入れ替わったテキストノードにはイベントリスナーは設定されていないから、その後、 span の中の文字列を直接変えてもイベントは発生しませんでした。
修正 2
なので、次のように修正しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MutationObserver</title>
</head>
<body>
<h1>MutationObserver</h1>
<hr>
<form id="form1">
<input type="text" id="text1" value="value1">
<span id="span1">span1</span><!-- ← contenteditable="true" を削除する -->
</form>
<hr>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
document.querySelector('#text1').addEventListener('change', function () {
document.querySelector('#span1').childNodes[0].textContent = this.value; // ← テキストノードに値を設定する
});
// Select the node that will be observed for mutations
var targetNode = document.querySelector('#span1').childNodes[0];
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: false, characterData: true };
// Callback function to execute when mutations are observed
var callback = function (mutationsList) {
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
} else if (mutation.type === 'characterData') {
console.log('characterData was modified.');
}
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
});
</script>
</body>
</html>
document.querySelector('#span1').childNodes[0].textContent = this.value
と、テキストノードに値を設定してみました。
text の値を変えて、フォーカスを移動すると、 text の値が span に表示されて、コンソールに “characterData was modified.” と出力されます。
思ったことは実現できました。
でも、 span の中の文字列を変えるのに .childNodes
とか、あまり記述したくない感じがします。
終わり
MutationObserver
を使って、 childList: true
の設定にするのが妥当なところかな。
それから、 MutationObserver
は IE11 から実装されたそうです。