Błąd duplikacji funkcji edycji elementów DOM w JavaScript
Załóżmy, że mamy pewną listę:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
Pobierzmy samą listę i jej elementy do oddzielnych zmiennych:
let ul = document.querySelector('ul');
let lis = document.querySelectorAll('li');
Sprawmy, aby elementy naszej listy można było edytować pojawiającym się inputem:
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);
});
}
Załóżmy teraz, że chcemy, aby do listy można było dodawać nowe elementy. Załóżmy, że w tym celu pod listą będzie odpowiedni input:
<input id="adder">
Pobierzmy referencję do tego inputa do zmiennej:
let adder = document.querySelector('#adder');
Sprawmy, aby po utracie fokusu inputa do listy dodał się nowy element z tekstem pobranym z naszego inputa:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});
Załóżmy teraz, że chcemy, aby również nowo dodane elementy również można było edytować. Samo z siebie edytowanie dla nich nie zadziała, ponieważ kiedy wieszaliśmy obsługę kliknięcia na elementy listy, tych elementów jeszcze nie było.
Przyjrzyjmy się możliwym wariantom rozwiązania tego problemu.
Rozwiązanie pierwsze
Najprostsze rozwiązanie - to zduplikować
kod funkcji func, przywiązując ją
również dla nowo utworzonych elementów:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function func() {
// tutaj duplikujemy kod
});
ul.append(li);
});
Oczywiście, w tym rozwiązaniu od razu widzimy wadę - duplikowanie kodu jest nieprawidłowe.
Rozwiązanie drugie
Aby rozwiązać problem duplikowania
logicznie wynieść funkcję func
na zewnątrz, tworząc ją jako 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);
}
Tutaj właśnie czai się na nas problem.
Chodzi o to, że nasza funkcja używała
zmiennej li, uzyskanej
z zewnętrznego zakresu widoczności.
Ale po wyniesieniu funkcji ta
zmienna teraz nie jest widoczna!
Aby rozwiązać problem, będziemy przekazywać
naszą li parametrem:
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);
}
I tutaj nasze rozwiązanie generuje jeszcze jeden problem. Chodzi o to, że nie można po prostu tak przekazać parametru do obsługi zdarzenia:
for (let li of lis) {
li.addEventListener('click', func(li)); // nie działa!
}
Aby rozwiązać ten problem, po prostu wywołajmy naszą funkcję wewnątrz anonimowej obsługi:
for (let li of lis) {
li.addEventListener('click', function() {
func(li);
});
}
I podobnie postąpmy przy tworzeniu nowego elementu listy:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function() {
func(li);
});
ul.append(li);
});
Rozwiązanie trzecie
Istnieje bardziej eleganckie rozwiązanie. Można po prostu skorzystać z delegowania zdarzeń. W tym przypadku problem z nowymi elementami listy po prostu nie powstanie:
ul.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') { // łapiemy właśnie kliknięcie na li, nie na 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;
});
}
});
W tym przypadku pętla po elementach listy w ogóle nie będzie nam potrzebna, a kod dla tworzenia nowego elementu listy skróci się do takiego:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});