JavaScriptにおけるDOM要素編集関数の重複エラー
いくつかのリストがあるとします:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
リスト自体とその項目を別々の変数に取得しましょう:
let ul = document.querySelector('ul');
let lis = document.querySelectorAll('li');
リストの項目を、表示される入力フィールドで編集できるようにしましょう:
for (let li of lis) {
li.addEventListener('click', function func() {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
});
}
次に、リストに新しい項目を追加できるようにしたいとします。 そのために、リストの下に対応する入力フィールドを置きましょう:
<input id="adder">
この入力フィールドへの参照を変数に取得します:
let adder = document.querySelector('#adder');
入力フィールドがフォーカスを失ったときに、 入力フィールドから取ったテキストで 新しい項目がリストに追加されるようにしましょう:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});
次に、新しく追加された項目も編集できるようにしたいとします。 これらに対しては、編集機能自体は自動的には機能しません。 なぜなら、リスト項目へのクリックハンドラを設定したとき、 これらの項目はまだ存在していなかったからです。
この問題の解決策の選択肢を見ていきましょう。
解決策 その1
最も単純な解決策は、新しく作成された項目に対しても
関数 func をバインドして、
コードを複製することです:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function func() {
// ここでコードを複製します
});
ul.append(li);
});
もちろん、この解決策には欠点がすぐにわかります。 コードを複製するのは正しくありません。
解決策 その2
重複を解消するためには、関数 func を外に出して、
Function Declaration にするのが理にかなっています:
function func() {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
ここで問題が待ち受けています。
この関数は、外側のスコープから取得した
変数 li を使用していたという事実です。
しかし、関数を外に出した後では、
この変数はもう見えません!
この問題を解決するために、 li を
パラメータとして渡すようにします:
function func(li) {
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
li.addEventListener('click', func);
});
li.removeEventListener('click', func);
}
そして、この解決策はさらに別の問題を引き起こします。 イベントハンドラにパラメータを単純に渡すことはできないという事実です:
for (let li of lis) {
li.addEventListener('click', func(li)); // 動作しません!
}
この問題を解決するために、 無名ハンドラ内で関数を呼び出すようにしましょう:
for (let li of lis) {
li.addEventListener('click', function() {
func(li);
});
}
そして、新しいリスト項目を作成する際にも 同じように対応します:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function() {
func(li);
});
ul.append(li);
});
解決策 その3
よりエレガントな解決策が存在します。 単にイベント委譲を利用すればよいのです。 この場合、新しいリスト項目の問題はそもそも発生しません:
ul.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') { // li へのクリックだけをキャッチ、input へのクリックは対象外
let li = event.target;
let input = document.createElement('input');
input.value = li.textContent;
li.textContent = '';
li.append(input);
input.addEventListener('blur', function() {
li.textContent = this.value;
});
}
});
この場合、リスト項目に対するループはそもそも不要になり、 新しいリスト項目を作成するコードは以下のように短縮できます:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});