Câu 1Khi có 3 li mang data-id, querySelector("[data-id]") trả về gì?
Cây DOM và cách lấy phần tử — querySelector và querySelectorAll
Lấy phần tử từ cây DOM bằng querySelector và querySelectorAll: tìm theo class, id, data attribute, chuyển NodeList thành mảng và dùng ?. khi không tìm thấy.
Giả sử bạn muốn đọc giá của mọi sản phẩm trong một danh sách sản phẩm. Cách tìm theo id như #total chỉ dùng được với phần tử có id, nên không hợp với một danh sách gồm các sản phẩm có cùng cấu trúc.
Bài này nói về selector để tìm theo điều kiện khác ngoài id, querySelectorAll để lấy tất cả phần tử khớp, và cách đọc một phần tử có thể không tồn tại.
Lấy một phần tử — querySelector và selector
Trong một danh sách thông báo, bạn chỉ muốn đọc tiêu đề của thông báo quan trọng. Danh sách gồm các li giống hệt nhau về cấu trúc và không mục nào có id. Mỗi id chỉ được gán cho đúng một phần tử trên trang, nên không thể dùng id để đánh dấu cả một nhóm như "thông báo quan trọng".
Selector (chuỗi mô tả điều kiện của phần tử cần tìm) chỉ định phần tử theo tên thẻ như li, theo id như #news, hoặc theo class (tên gắn chung cho các phần tử cùng loại) như .notice. document.querySelector(selector) trả về phần tử đầu tiên trong cây DOM khớp với điều kiện.
// Chuẩn bị danh sách thông báo (một phần tử có thể có nhiều class, cách nhau bằng dấu cách)
document.body.innerHTML = `<ul id="news">
<li class="notice">Phí giao hàng mới</li>
<li class="notice important">Bảo trì đột xuất</li>
<li class="notice">Hàng mới mùa thu</li>
</ul>`;
// Tìm theo tên thẻ / theo class (tên class viết sau dấu .)
console.log(document.querySelector("li").textContent); // Phí giao hàng mới
console.log(document.querySelector(".notice").textContent); // Phí giao hàng mới
console.log(document.querySelector(".important").textContent); // Bảo trì đột xuất
"li"và".notice"— cả hai dừng tìm ở đây- Chữ là "Phí giao hàng mới"
".important"— dừng tìm ở đây- Chữ là "Bảo trì đột xuất"
- Chữ là "Hàng mới mùa thu"
- Cả 3 selector đều không tìm đến đây
".notice" dừng tìm ở li đầu tiên. Giá trị trả về là phần tử đầu tiên khớp điều kiện.li thứ 2 có hai class nên khớp cả .notice lẫn .important. Muốn lấy riêng li thứ 2, bạn viết điều kiện mà chỉ li đó khớp, như .important.
Duyệt hết danh sách — querySelectorAll và NodeList
Nếu bạn muốn đọc tiêu đề của tất cả thông báo cùng lúc thì sao? querySelector dừng tìm ở phần tử khớp đầu tiên, nên gọi bao nhiêu lần với cùng selector thì cũng chỉ nhận được "Phí giao hàng mới", không bao giờ tới được thông báo thứ 2 trở đi.
document.querySelectorAll(selector) trả về mọi phần tử khớp dưới dạng một NodeList (object giống mảng, chứa các phần tử tìm được theo thứ tự trong cây DOM). Bạn đọc số phần tử bằng length và xử lý từng phần tử bằng forEach.
document.body.innerHTML = `<ul id="news">
<li class="notice">Phí giao hàng mới</li>
<li class="notice important">Bảo trì đột xuất</li>
<li class="notice">Hàng mới mùa thu</li>
</ul>`;
// Lấy mọi phần tử khớp dưới dạng NodeList
const notices = document.querySelectorAll(".notice");
console.log(notices.length); // 3
notices.forEach((notice) => console.log(notice.textContent));
// Phí giao hàng mới
// Bảo trì đột xuất
// Hàng mới mùa thu
// notices.map(...) báo lỗi TypeError. Chuyển thành mảng rồi mới gọi
const titles = [...notices].map((notice) => notice.textContent);
console.log(titles.join(" / ")); // Phí giao hàng mới / Bảo trì đột xuất / Hàng mới mùa thu
Dù vẫn là NodeList, bạn có thể đọc một phần tử theo chỉ số, như notices[1]. Mảng tạo bằng cú pháp spread là một object riêng, nên sau khi chuyển đổi, notices vẫn là NodeList và bạn vẫn dùng tiếp nó với forEach được.
Trỏ đúng một mục theo mã số — data attribute và selector thuộc tính
Giả sử bạn cần đúng thông báo có mã N-103. Nếu các thông báo đó còn được hiển thị ở một khu vực khác trên trang, id sẽ bị trùng nên không dùng được. class là tên để gom các phần tử cùng loại, nên cũng không phải chỗ hợp để lưu mã số.
Data attribute (thuộc tính có tên bắt đầu bằng data-) có thể chứa bất kỳ giá trị riêng nào bạn muốn, như một mã số. Selector tìm theo thuộc tính được đặt trong dấu ngoặc vuông: [data-id] khớp với phần tử có thuộc tính đó, còn [data-id="N-103"] chỉ khớp với phần tử có giá trị đúng bằng N-103.
document.body.innerHTML = `<ul id="news">
<li class="notice" data-id="N-101">Phí giao hàng mới</li>
<li class="notice important" data-id="N-102">Bảo trì đột xuất</li>
<li class="notice" data-id="N-103">Hàng mới mùa thu</li>
</ul>`;
// Chỉ tìm theo việc có thuộc tính hay không (cả 3 đều khớp)
console.log(document.querySelector("[data-id]").textContent); // Phí giao hàng mới
// Tìm phần tử khớp cả giá trị (giá trị nằm trong ", nên cả selector được bọc bằng ')
console.log(document.querySelector('[data-id="N-103"]').textContent); // Hàng mới mùa thu
[data-id] không có giá trị sẽ khớp cả 3 phần tử và trả về phần tử đầu tiên, còn [data-id="N-103"] có giá trị thì chỉ trỏ tới đúng phần tử có mã trùng khớp. Bảng dưới đây tóm tắt các selector dùng trong bài và phần tử mà mỗi selector trả về từ danh sách thông báo.
| Selector | Khớp với | Trả về từ danh sách |
|---|---|---|
| li | Thẻ li | li thứ 1 (Phí giao hàng mới) |
| .important | class có important | li thứ 2 (Bảo trì đột xuất) |
| #news | id là news | Phần tử ul |
| [data-id] | Có thuộc tính data-id | li thứ 1 (Phí giao hàng mới) |
| [data-id="N-103"] | data-id là N-103 | li thứ 3 (Hàng mới mùa thu) |
Giá trị bắt đầu bằng chữ số sẽ lỗi nếu thiếu dấu nháy
Có những giá trị vẫn chạy khi bỏ dấu nháy, như [data-id=N-103], nhưng giá trị bắt đầu bằng chữ số như [data-id=102] thì không được hiểu là selector hợp lệ, và dòng đó báo lỗi "is not a valid selector". Hãy luôn bọc giá trị trong ".
Không để code dừng khi không có phần tử — null và ?.
Hãy hình dung một trang chủ chỉ hiển thị tiêu đề khi có thông báo khẩn. Vào những ngày không có thông báo khẩn, phần tử đó cũng không tồn tại, nên dòng đọc chữ của tiêu đề sẽ báo lỗi TypeError và các xử lý viết phía sau cũng không chạy.
Khi không có phần tử nào khớp, querySelector trả về null. Nếu viết optional chaining ?. trước .textContent, khi kết quả là null biểu thức sẽ không đọc tiếp mà trả về undefined. Nối thêm ?? phía sau thì bạn đặt được giá trị mặc định.
// Danh sách chưa có thông báo khẩn (class là urgent)
document.body.innerHTML = `<ul><li class="notice">Hàng mới mùa thu</li></ul>`;
const urgent = document.querySelector(".urgent");
console.log(urgent); // null
// urgent.textContent sẽ báo lỗi TypeError
console.log(urgent?.textContent); // undefined
console.log(urgent?.textContent ?? "Không có"); // Không có
// Với danh sách có thông báo khẩn, so sánh khi có và không có dấu .
document.body.innerHTML = `<ul><li class="urgent">Sự cố máy chủ</li></ul>`;
console.log(document.querySelector(".urgent")?.textContent ?? "Không có"); // Sự cố máy chủ
console.log(document.querySelector("urgent")?.textContent ?? "Không có"); // Không có
Chỉ dùng ?. cho những phần tử có lúc không tồn tại, như thông báo khẩn. Nếu thêm nó cả cho những phần tử lúc nào cũng có, bạn sẽ không nhận ra lỗi gõ sai. Với những phần tử đó, hãy bỏ ?. để code báo lỗi TypeError, rồi dựa vào số dòng mà tìm nguyên nhân.
NodeList có 0 phần tử không phải là null
Kể cả khi không có phần tử nào khớp, querySelectorAll cũng không trả về null mà trả về một NodeList có length bằng 0. Gọi forEach trên nó không báo lỗi mà chỉ đơn giản là không làm gì, nên để biết có tìm thấy gì hay không, hãy kiểm tra length === 0 thay vì dùng ?..
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Điều gì xảy ra nếu gọi thẳng map trên giá trị trả về của querySelectorAll(".notice")?
Câu 3Khi phần tử không tồn tại, querySelector(".sale-price")?.textContent có giá trị gì?