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

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
Mỗi selector tìm thấy phần tử nào
Phần tử body — tìm từ trên xuống
Phần tử ul — id là news
li thứ 1 — class là notice
  • "li"".notice" — cả hai dừng tìm ở đây
  • Chữ là "Phí giao hàng mới"
li thứ 2 — class là notice và important
  • ".important" — dừng tìm ở đây
  • Chữ là "Bảo trì đột xuất"
li thứ 3 — class là notice
  • Chữ là "Hàng mới mùa thu"
  • Cả 3 selector đều không tìm đến đây
Có 3 li mang class notice, nhưng ".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.

Trên trang danh sách sản phẩm, bạn lấy từng phần tử một, tùy trường hợp mà dùng tên thẻ, class hay id. HTML đã được tạo sẵn bằng lệnh gán cho document.body.innerHTML ở đầu code.

① Hãy tìm sản phẩm đầu tiên theo tên thẻ và in chữ của nó ra.

② Hãy tìm sản phẩm theo class chỉ sản phẩm được đề xuất mới có, và in chữ của nó ra.

③ Hãy tìm phần tử hiển thị số sản phẩm theo id và in chữ của nó ra.

(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

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
Chuyển thành mảng rồi mới gọi map
notices.map(...)NodeListkhông có mapBáo lỗiTypeErrortitles khôngđược tạo ra[...notices].map(...)Mảng chứacùng 3 phần tửmap của mảng chạytrên từng phần tử["Phí giao hàngmới", …]gồm 3 chuỗi
Gọi map trên chính NodeList sẽ báo lỗi, còn chuyển thành mảng trước thì chạy được. Chỉ gọi map và filter sau khi đã chuyển NodeList thành mảng.

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ên trang chi tiết đơn hàng, bạn đọc tất cả sản phẩm đã mua cùng lúc, rồi kiểm tra có bao nhiêu sản phẩm và bao nhiêu sản phẩm có giá từ 2000 yên trở lên. HTML của đơn hàng đã được tạo sẵn bằng lệnh gán cho document.body.innerHTML ở đầu code.

① Hãy lấy tất cả phần tử sản phẩm trong đơn hàng và in ra số lượng.

② Hãy duyệt lần lượt các phần tử vừa lấy được và in chữ của từng sản phẩm theo thứ tự.

③ Hãy lấy tất cả phần tử giá, chuyển thành mảng, chỉ giữ lại những giá từ 2000 yên trở lên rồi in ra số lượng.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Từ thuộc tính đến chuỗi selector
data-id="N-103"[data-id="N-103"]'[data-id="N-103"]'Thuộc tính của lidạng tên="giá trị"Ngoặc vuông biếnnó thành selectorBên trong có "nên bọc bằng '
Bọc thuộc tính HTML trong ngoặc vuông sẽ thành selector, và khi truyền cho JavaScript thì bọc thêm bằng ' ở ngoài. Dấu " quanh giá trị và dấu ' ở ngoài có vai trò khác nhau.

[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.

SelectorKhớp vớiTrả về từ danh sách
liThẻ lili thứ 1 (Phí giao hàng mới)
.importantclass có importantli thứ 2 (Bảo trì đột xuất)
#newsid là newsPhần tử ul
[data-id]Có thuộc tính data-idli thứ 1 (Phí giao hàng mới)
[data-id="N-103"]data-id là N-103li 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 ".

Trong danh sách tồn kho của một cửa hàng văn phòng phẩm, bạn lấy ra sản phẩm cần tìm dựa vào mã sản phẩm và dấu hiệu đang giảm giá. HTML của danh sách tồn kho đã được tạo sẵn bằng lệnh gán cho document.body.innerHTML ở đầu code.

① Hãy tìm phần tử có mã sản phẩm P-203 và in chữ của nó ra.

② Hãy tìm phần tử có thuộc tính giảm giá và in chữ của nó ra.

③ Hãy lấy mọi phần tử có thuộc tính mã sản phẩm và in ra số lượng.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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ó
3 cách tìm và chữ được hiển thị
querySelector(…)?.textContent?? "Không có"Không có urgent,tìm ".urgent"Có urgent,tìm ".urgent"Có urgent,tìm "urgent"Nhận null;?. dừng tại đóTìm thấy li,đọc chữ của nóTìm thẻ urgent,nhận nullHiện "Không có"Hiện "Sự cốmáy chủ"Có phần tử nhưnghiện "Không có"
Cột 1 và cột 3 đều đi từ null đến "Không có". ?. không phân biệt được phần tử không tồn tại với lỗi gõ sai selector.

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 ?..

Trên trang chi tiết sản phẩm, bạn đọc phần đánh giá và giá khuyến mãi — hai phần có thể không có trên trang. HTML của trang chi tiết sản phẩm đã được tạo sẵn bằng lệnh gán cho document.body.innerHTML ở đầu code.

① Hãy in ra chữ của tên sản phẩm.

② Hãy lấy tất cả li đánh giá; nếu không có li nào thì in ra "Chưa có đánh giá nào".

③ Hãy in ra chữ của giá khuyến mãi theo cách viết không báo lỗi khi phần tử không tồn tại.

④ Khi không có giá khuyến mãi, hãy in ra chữ của giá thường.

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 1Khi có 3 li mang data-id, querySelector("[data-id]") trả về gì?

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ì?