Học bằng cách đọc theo thứ tự

Tạo, thay đổi và xóa phần tử — createElement / classList / remove

Thêm phần tử bằng createElement và append, xóa bằng remove, đổi class và thuộc tính bằng classList, dataset, và hiển thị dữ liệu nhập dạng chữ bằng textContent.

Nếu mỗi lần thay đổi, như thêm một mục vào danh sách, đánh dấu một mục đã xong hay xóa một mục, bạn đều gán lại innerHTML, thì cả những li bạn muốn giữ nguyên cũng bị dựng lại. Còn nếu gán dữ liệu người dùng nhập theo cách đó, các thẻ lẫn trong dữ liệu, như <a>, cũng trở thành phần tử.

Bài này nói về cách tạo và xóa phần tử, classListdataset để thay đổi class và thuộc tính, và cách đưa dữ liệu nhập vào dưới dạng chữ thuần.

Tạo và thêm phần tử — createElement và append

Giả sử bạn muốn thêm "Gửi hóa đơn" vào cuối danh sách công việc. Nếu nối thêm chuỗi vào innerHTML rồi gán lại, mọi li đang có trong danh sách cũng bị dựng lại thành phần tử mới. li mà bạn đã lưu vào biến bằng querySelector trước đó vẫn trỏ tới phần tử cũ, vốn không còn trên trang, nên đổi chữ qua biến đó cũng không làm thay đổi gì trên màn hình.

createElement (phương thức nhận tên thẻ và tạo một phần tử mới chưa nằm ở đâu trong cây DOM) được gọi như document.createElement("li"). Phần tử vừa tạo chỉ trở thành một phần của trang khi bạn thêm nó vào một phần tử cha bằng append (phương thức đặt phần tử nhận làm tham số vào cuối danh sách con của phần tử gọi nó).

document.body.innerHTML = `<ul id="tasks"><li>Lập báo giá</li><li>Chia sẻ biên bản họp</li></ul>`;
const taskList = document.querySelector("#tasks");

// Tạo phần tử li và đưa chữ vào (lúc này nó chưa có trên trang)
const newTask = document.createElement("li");
newTask.textContent = "Gửi hóa đơn";
console.log(newTask.outerHTML);                        // <li>Gửi hóa đơn</li>
console.log(document.querySelectorAll("li").length);   // 2

// Thêm vào cuối danh sách con của ul thì nó trở thành một phần của trang
taskList.append(newTask);
console.log(document.querySelectorAll("li").length);   // 3
console.log(taskList.outerHTML);
// <ul id="tasks"><li>Lập báo giá</li><li>Chia sẻ biên bản họp</li><li>Gửi hóa đơn</li></ul>
li vừa tạo được đưa vào trang như thế nào
createElement("li")newTask.textContent ="Gửi hóa đơn"taskList.append(newTask)Tạo ra <li></li>;trang vẫn có 2 li<li>Gửi hóađơn</li>chưa có trên trangVào cuối ul;giờ có 3 li
Sau dòng createElement và dòng textContent, trên trang vẫn chỉ có 2 li. Số li tăng lên ở dòng gọi append.

Biến newTask trỏ tới cùng một phần tử li cả trước lẫn sau khi append. Vì vậy, nếu gán cho newTask.textContent sau khi đã gọi append, chữ của li thứ 3 trên trang sẽ thay đổi.

Bạn thêm cùng lúc các yêu cầu hỗ trợ mới nhận vào danh sách đang chờ xử lý. Phần tử ul của danh sách inquiryList và mảng tiêu đề yêu cầu newInquiries đã được khai báo sẵn.

① Hãy biến từng tiêu đề trong mảng thành một phần tử li và thêm vào cuối danh sách.

② Hãy in ra số li có trên trang.

③ Hãy in ra HTML của danh sách, gồm cả thẻ của chính phần tử ul.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đổi dấu hiệu hoàn thành — classList và dataset

Tiếp theo, bạn muốn thêm class done cho công việc đã xong để CSS hiển thị nó với đường gạch ngang. Thuộc tính class của li đó là một chuỗi duy nhất task urgent, nên nếu ghi đè giá trị thành done, bạn cũng mất luôn class task mà CSS dùng để tạo style cho dòng đó.

classList (object để làm việc với class của phần tử theo từng từ) có add để thêm một từ, remove để bỏ một từ, và toggle để thêm nếu chưa có, bỏ đi nếu đã có. Các thuộc tính khác class được ghi bằng setAttribute(tên, giá trị), còn thuộc tính bắt đầu bằng data- thì ghi qua dataset (object để đọc và ghi data attribute theo tên).

document.body.innerHTML = `<ul><li class="task urgent" data-id="T-1">Lập báo giá</li><li class="task urgent" data-id="T-2">Gửi hóa đơn</li></ul>`;
const quote = document.querySelector('[data-id="T-1"]');
const invoice = document.querySelector('[data-id="T-2"]');

// classList thêm và bỏ từng từ một (các class khác vẫn giữ nguyên)
quote.classList.add("done");
console.log(quote.outerHTML);    // <li class="task urgent done" data-id="T-1">Lập báo giá</li>
quote.classList.toggle("urgent");   // Đã xong nên bỏ urgent (đang có nên toggle sẽ bỏ nó đi)
console.log(quote.outerHTML);    // <li class="task done" data-id="T-1">Lập báo giá</li>

// setAttribute thay toàn bộ giá trị (dùng cho class thì task cũng mất)
invoice.setAttribute("class", "done");
console.log(invoice.outerHTML);  // <li class="done" data-id="T-2">Gửi hóa đơn</li>

// Thuộc tính bắt đầu bằng data- thì ghi qua dataset (doneAt trở thành data-done-at)
invoice.dataset.doneAt = "9/10";
console.log(invoice.outerHTML);
// <li class="done" data-id="T-2" data-done-at="9/10">Gửi hóa đơn</li>
add thì thêm, setAttribute thì thay
quote.classList.add("done")Thêm từ donevào task urgentclass làtask urgent doneDòng task vẫngiữ styleinvoice.setAttribute("class", "done")Thay toàn bộgiá trị classclass chỉcòn doneDòng taskmất style
add giữ lại 2 từ và thêm một từ nữa, còn setAttribute thay giá trị thành done. Muốn thêm class, hãy dùng classList.

Tên dùng với dataset là tên thuộc tính đã bỏ data-, và chữ cái đứng sau mỗi dấu gạch ngang còn lại được viết hoa. Bạn đọc và ghi data-id qua dataset.id, data-done-at qua dataset.doneAt; khi gán giá trị, thuộc tính được ghi vào HTML dưới tên gốc.

dataset không nhận tên có dấu gạch ngang

Nếu đọc mà vẫn giữ dấu gạch ngang, như dataset["done-at"], bạn nhận về undefined chứ không có lỗi nào, nên rất khó nhận ra lỗi gõ sai. Còn gán theo cách đó thì dòng đó báo lỗi. Tên lỗi là SyntaxError, nhưng khác với lỗi cú pháp thật trong code, nó xảy ra lúc chạy nên các dòng phía trước vẫn được thực thi.

Bạn cập nhật class hoàn thành trong checklist thủ tục nhận việc của nhân viên mới. HTML của danh sách đã được tạo sẵn.

① Hãy thêm class done cho mục C-2.

② Mục C-1 đã bị đánh dấu xong nhầm. Hãy bật/tắt class done của nó chỉ bằng một lần gọi.

③ Hãy ghi data attribute ngày hoàn thành cho mục C-2.

④ Hãy in ra HTML của danh sách, gồm cả thẻ của chính phần tử ul.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Xóa phần tử — remove và tham chiếu còn lại trong biến

Giờ giả sử bạn muốn gỡ một công việc đã xong khỏi danh sách. Nếu viết lại danh sách bằng innerHTML, bạn phải ghi lại cả các class và data attribute đã thêm ở phần trước vào chuỗi HTML, và chỉ cần sót một chỗ là công việc được giữ lại sẽ mất dấu hiệu hoàn thành.

remove (phương thức tách phần tử gọi nó ra khỏi phần tử cha và xóa nó khỏi cây DOM) được gọi không có tham số trên phần tử bạn muốn xóa, như doneItem.remove(). classList.remove("done") ở phần trước chỉ bỏ một từ khỏi class, là phương thức khác hẳn với remove() xóa chính phần tử.

document.body.innerHTML = `<ul id="tasks"><li data-id="T-1">Lập báo giá</li><li data-id="T-2" class="done">Gửi hóa đơn</li><li data-id="T-3">Chia sẻ biên bản họp</li></ul>`;
const tasks = document.querySelectorAll("li");

// Tìm li đã xong và xóa nó khỏi cây DOM
const doneItem = document.querySelector(".done");
doneItem.remove();
console.log(document.querySelectorAll("li").length);   // 2

// Phần tử đã xóa và NodeList lấy từ trước đều vẫn còn trong biến
console.log(doneItem.textContent);                     // Gửi hóa đơn
console.log(tasks.length);                             // 3 (3 phần tử tìm được lúc lấy)

// Vẫn còn trong biến nên append có thể đưa nó trở lại cuối danh sách
document.querySelector("#tasks").append(doneItem);
console.log(document.querySelectorAll("li").length);   // 3
Số lượng khi gọi lại và số lượng đã lưu
Gọi lạiquerySelectorAll("li")Tìm trong DOMsau khi removeli của T-1và T-3length là 2tasks, lấytrước khi removeVẫn là các phần tửlúc lấyli của T-1,T-2 và T-3length là 3
Gọi lại sau remove thì chỉ tìm thấy T-1 và T-3, còn tasks vẫn có 3 phần tử. Muốn biết số lượng hiện tại, hãy gọi lại.

querySelectorAll chỉ trả về các phần tử tìm được ngay lúc bạn gọi nó; nó không theo dõi những thay đổi sau đó của DOM. doneItem đã bị xóa cũng vẫn còn trong biến, nên bạn vẫn đọc được chữ của nó hoặc đưa nó trở lại danh sách bằng append.

Trên màn hình soạn email, bạn gỡ cùng lúc các file đính kèm đã hủy khỏi danh sách. HTML của danh sách đã được tạo sẵn, và phần tử ul được lưu trong attachmentList.

① Hãy lấy tất cả phần tử file đã hủy và xóa từng cái một khỏi danh sách.

② Sau khi xóa, hãy in ra số phần tử mà ① đã lấy được.

③ Hãy lấy lại các phần tử file đã hủy và in ra số lượng.

④ Hãy in ra HTML của danh sách, gồm cả thẻ của chính phần tử ul.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Hiển thị dữ liệu nhập dưới dạng chữ — textContent và innerHTML

Hãy hình dung phần đánh giá sản phẩm hiển thị nhận xét do người dùng đăng. Nhận xét được lưu lại, và cùng chuỗi đó cũng hiện trên trang của những người xem khác. Nếu nhận xét chứa thẻ a (thẻ tạo liên kết, đích đến ghi trong thuộc tính href), việc gán nó cho innerHTML sẽ biến liên kết giả do người đăng cài vào thành một liên kết thật trên trang.

Chuỗi gán cho innerHTML được phân tích như HTML, và các thẻ trong đó trở thành phần tử. Chuỗi gán cho textContent thì không được phân tích, nên kể cả < cũng chỉ là một ký tự bình thường. Đưa dữ liệu nhập vào innerHTML không chỉ cho phép người khác cài liên kết giả, mà còn có thể dẫn đến XSS (lỗ hổng khiến code mà người dùng lén chèn vào dữ liệu nhập chạy trên trang của người xem khác).

document.body.innerHTML = `<p class="review"></p>`;
const review = document.querySelector(".review");
// Chuỗi người dùng nhập vào ô nhận xét (có lẫn một liên kết giả)
const reviewText = '<a href="https://example.com/login">Đăng nhập lại</a> để tiếp tục';

// Đưa vào innerHTML thì thẻ a trở thành phần tử a thật
review.innerHTML = reviewText;
console.log(document.querySelectorAll("a").length);   // 1

// Đưa vào textContent thì cả thẻ cũng được giữ nguyên dạng chữ
review.textContent = reviewText;
console.log(document.querySelectorAll("a").length);   // 0
console.log(review.outerHTML);
// <p class="review">&lt;a href="https://example.com/login"&gt;Đăng nhập lại&lt;/a&gt; để tiếp tục</p>
Phần tử p sau khi đưa vào cùng một dữ liệu
p sau khi gán cho innerHTML
Phần tử a — href là example.com/login
  • Chữ bên trong là "Đăng nhập lại"
  • Nhấn vào sẽ chuyển sang trang web khác
Chữ — để tiếp tục
  • Nằm sau phần tử a
p sau khi gán cho textContent
Chữ — toàn bộ dữ liệu nhập là một khối
  • <a href="…">Đăng nhập lại</a> để tiếp tục
  • Có 0 phần tử a
Với innerHTML, thẻ a trở thành phần tử; với textContent, nó vẫn là chữ. textContent là cách giữ cho dữ liệu nhập không trở thành phần tử.

&lt;&gt; trong outerHTML là cách HTML viết <> dưới dạng chữ; khi đọc lại bằng textContent, bạn nhận về đúng các ký tự đã nhập. Bảng dưới đây tóm tắt các cách thay đổi phần tử trong bài, theo từng đối tượng cần thay đổi.

Đối tượng thay đổiCách viếtĐiều cần lưu ý
Chính phần tửcreateElement / append / removeChưa có trên trang cho đến khi append
classclassList.add / remove / togglesetAttribute thay toàn bộ giá trị
Thuộc tính bắt đầu bằng data-dataset.doneAt = "9/10"Bỏ data-, viết hoa chữ sau dấu -
Các thuộc tính khácsetAttribute("href", "/tasks/T-2")Thêm nếu chưa có, có rồi thì thay giá trị
Chữ bên trongtextContentĐưa dữ liệu người dùng nhập vào đây
HTML bên tronginnerHTMLChỉ HTML do bạn tự viết

Code trong onerror của img vẫn chạy

<script> đưa vào innerHTML sẽ không chạy, nhưng nếu thẻ img (thẻ hiển thị ảnh) được viết như <img src="x" onerror="…"> thì onerror (thuộc tính chứa code chạy khi tải ảnh thất bại) vẫn chạy. Vì vậy, việc <script> không chạy không có nghĩa là đưa dữ liệu nhập vào innerHTML là an toàn.

Bạn hiển thị một bài đăng trong phần đánh giá sản phẩm. Tên người đăng author, nhận xét comment và phần tử khu vực đánh giá reviewArea đã được khai báo sẵn.

① Hãy tạo phần tử article, rồi lần lượt thêm vào bên trong một phần tử strong chứa tên người đăng và một phần tử p chứa nhận xét. Hãy đưa dữ liệu nhập vào dưới dạng chữ thuần.

② Hãy thêm article vào khu vực đánh giá và in ra số phần tử a có trên trang.

③ Hãy in ra HTML, gồm cả thẻ của chính phần tử article.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Ngay sau const li = document.createElement("li");, li nằm ở đâu trên trang?

Câu 2Nếu class của một li là task urgent, sau khi gọi setAttribute("class", "done") class sẽ là gì?

Câu 3Sau p.textContent = "<b>Mới</b>";, bên trong p có bao nhiêu phần tử b?