Ошибка дублирования функции редактирования DOM элементов в JavaScript

Пусть у нас есть некоторый список:

<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 = list.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); });

Пусть теперь мы хотим, чтобы и вновь добавленные пункты также можно было редактировать. Само по себе для них редактирование не заработает, ведь когда мы навешивали обработчик клика на пункты списка, этих пунктов еще не было.

Давайте посмотрим на возможные варианты решения этой проблемы.

Решение первое

Самое простое решение - это сдублировать код функции func, привязав ее и для вновь созданных пунктов:

adder.addEventListener('blur', function() { let li = document.createElement('li'); li.textContent = this.value; li.addEventListener('click', function func() { // тут мы дублируем код }); ul.append(li); });

Конечно, в этом решении мы сразу видим недостаток - дублировать код не правильно.

Решение второе

Для решения проблемы дублирования логично вынести функцию func наружу, сделав ее Function Declaration:

function func() { let input = document.createElement('input'); input.value = list.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 = list.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 in 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); });

Решение третье

Существует более изящное решение. Можно просто воспользоваться делегированием. В этом случае проблема с новыми пунктами списка просто не возникнет:

ul.addEventListener('click', function(event) { if (event.target.tagName === 'LI') { // ловим именно клик на li, не на инпут 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); });