Lỗi trùng lặp hàm chỉnh sửa phần tử DOM trong JavaScript
Giả sử chúng ta có một danh sách nào đó:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
Lấy chính danh sách và các mục của nó vào các biến riêng biệt:
let ul = document.querySelector('ul');
let lis = document.querySelectorAll('li');
Hãy làm sao để các mục trong danh sách của chúng ta có thể được chỉnh sửa bằng phần tử input xuất hiện:
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);
});
}
Bây giờ giả sử chúng ta muốn có thể thêm các mục mới vào danh sách. Giả sử để làm điều này, bên dưới danh sách của chúng ta sẽ có một phần tử input tương ứng:
<input id="adder">
Lấy tham chiếu đến phần tử input này vào một biến:
let adder = document.querySelector('#adder');
Hãy làm sao để khi mất tiêu điểm (blur) ở phần tử input, một mục mới sẽ được thêm vào danh sách với văn bản lấy từ phần tử input của chúng ta:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});
Bây giờ giả sử chúng ta muốn các mục mới được thêm vào cũng có thể được chỉnh sửa. Bản thân tính năng chỉnh sửa sẽ không hoạt động cho chúng, vì khi chúng ta gắn trình xử lý sự kiện click vào các mục danh sách, những mục này chưa tồn tại.
Hãy xem xét các phương án giải quyết khả thi cho vấn đề này.
Giải pháp thứ nhất
Giải pháp đơn giản nhất - là sao chép y nguyên
mã của hàm func, gắn nó
cho cả các mục mới được tạo:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function func() {
// ở đây chúng ta sao chép mã
});
ul.append(li);
});
Tất nhiên, trong giải pháp này chúng ta ngay lập tức thấy nhược điểm - sao chép mã là không đúng.
Giải pháp thứ hai
Để giải quyết vấn đề trùng lặp mã,
hợp lý là đưa hàm func
ra ngoài, biến nó thành một 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);
}
Và ở đây vấn đề đang chờ đợi chúng ta.
Vấn đề là hàm của chúng ta đã sử dụng
biến li, được lấy
từ phạm vi bên ngoài.
Nhưng sau khi đưa hàm ra ngoài thì
biến này bây giờ không nhìn thấy được nữa!
Để giải quyết vấn đề, chúng ta sẽ truyền
biến li của chúng ta dưới dạng tham số:
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);
}
Và ở đây giải pháp của chúng ta lại tạo ra một vấn đề khác. Vấn đề là không thể đơn giản truyền một tham số vào trình xử lý sự kiện:
for (let li of lis) {
li.addEventListener('click', func(li)); // không hoạt động!
}
Để giải quyết vấn đề này, chỉ cần gọi hàm của chúng ta bên trong một trình xử lý sự kiện ẩn danh:
for (let li of lis) {
li.addEventListener('click', function() {
func(li);
});
}
Và làm tương tự khi tạo một mục danh sách mới:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
li.addEventListener('click', function() {
func(li);
});
ul.append(li);
});
Giải pháp thứ ba
Tồn tại một giải pháp thanh lịch hơn. Có thể đơn giản sử dụng ủy thác sự kiện. Trong trường hợp này vấn đề với các mục danh sách mới sẽ đơn giản là không phát sinh:
ul.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') { // bắt chính xác click vào li, không phải vào 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;
});
}
});
Trong trường hợp này, vòng lặp qua các mục danh sách nói chung sẽ không cần thiết cho chúng ta nữa, và mã để tạo mục danh sách mới sẽ được rút gọn thành như thế này:
adder.addEventListener('blur', function() {
let li = document.createElement('li');
li.textContent = this.value;
ul.append(li);
});