Bài 5.5: Bài tập thực hành DOM manipulation

Bài 5.5: Bài tập thực hành DOM manipulation
Chào mừng trở lại với series Lập trình Web Front-end! Sau khi đã tìm hiểu về các kiến thức cơ bản của HTML, CSS và JavaScript, giờ là lúc chúng ta bắt đầu sử dụng JavaScript để biến trang web tĩnh của mình thành _một thứ gì đó sống động_, có khả năng phản hồi lại tương tác của người dùng và thay đổi nội dung một cách linh hoạt. Công cụ mạnh mẽ giúp chúng ta làm điều đó chính là DOM manipulation.
Bài viết này sẽ không đi sâu vào lý thuyết DOM là gì (chúng ta đã học ở bài trước), mà sẽ tập trung hoàn toàn vào thực hành. Hãy cùng 'xắn tay áo' lên và bắt đầu thao tác với cây DOM của trang web nhé!
DOM manipulation là gì? Nhắc lại nhanh!
Đơn giản mà nói, DOM (Document Object Model) là một _cây cấu trúc_ mà trình duyệt tạo ra khi tải trang HTML. Mỗi thành phần trên trang của bạn, từ thẻ <html>
, <head>
, <body>
cho đến các thẻ <p>
, <div>
, <a>
, ... đều là một _nút_ (node) trong cây DOM.
DOM manipulation chính là việc sử dụng JavaScript để truy cập, thay đổi, thêm mới hoặc xóa bỏ các nút này. Bằng cách này, chúng ta có thể làm được rất nhiều thứ như:
- Thay đổi nội dung văn bản hoặc HTML của một phần tử.
- Thay đổi màu sắc, kích thước hoặc các kiểu dáng CSS khác.
- Thêm hoặc xóa các class CSS để áp dụng bộ style phức tạp.
- Thay đổi các thuộc tính của phần tử (ví dụ:
href
của thẻ<a>
,src
của thẻ<img>
). - Tạo ra các phần tử HTML hoàn toàn mới và thêm chúng vào trang.
- Xóa bỏ các phần tử không cần thiết.
- Phản ứng lại các sự kiện của người dùng (click, gõ phím, rê chuột...).
Nghe có vẻ hấp dẫn đúng không? Hãy bắt đầu ngay với các ví dụ thực tế!
1. Truy cập (Chọn) các phần tử trong DOM
Trước khi làm bất cứ điều gì với một phần tử, chúng ta cần phải tìm và "chọn" nó trong cây DOM. JavaScript cung cấp nhiều phương thức để làm điều này.
1.1. Chọn một phần tử theo ID
Cách phổ biến và nhanh nhất để chọn một phần tử duy nhất là sử dụng phương thức getElementById()
. Nó trả về phần tử đầu tiên (và duy nhất theo chuẩn HTML) có id
được chỉ định.
HTML:
<p id="introParagraph">Đây là đoạn giới thiệu.</p>
JavaScript:
// Chọn phần tử có ID là 'introParagraph' const myParagraph = document.getElementById('introParagraph'); // Log ra console để kiểm tra xem đã chọn đúng chưa console.log(myParagraph); // Kết quả in ra console sẽ là chính phần tử <p id="introParagraph">...</p>
Giải thích:
document.getElementById('introParagraph')
tìm trong toàn bộ tài liệu (document) phần tử có thuộc tínhid
bằng'introParagraph'
và gán nó vào biếnmyParagraph
.
1.2. Chọn một hoặc nhiều phần tử theo Class, Tag name, Selector CSS
getElementById()
chỉ chọn theo ID. Để chọn theo các tiêu chí khác như class CSS, tên thẻ (tag name) hoặc bất kỳ selector CSS phức tạp nào, chúng ta dùng các phương thức querySelector()
và querySelectorAll()
.
querySelector()
: Trả về phần tử đầu tiên khớp với selector CSS được cung cấp.querySelectorAll()
: Trả về tất cả các phần tử khớp với selector CSS được cung cấp dưới dạng mộtNodeList
(giống mảng, có thể lặp qua).HTML:
<div class="product-card">Sản phẩm A</div> <div class="product-card featured">Sản phẩm B (Nổi bật)</div> <p>Một đoạn văn</p> <div class="product-card">Sản phẩm C</div>
JavaScript:
// Chọn phần tử đầu tiên có class 'product-card' const firstProduct = document.querySelector('.product-card'); console.log(firstProduct); // Kết quả: <div class="product-card">Sản phẩm A</div> // Chọn phần tử đầu tiên có cả hai class 'product-card' và 'featured' const featuredProduct = document.querySelector('.product-card.featured'); console.log(featuredProduct); // Kết quả: <div class="product-card featured">Sản phẩm B (Nổi bật)</div> // Chọn tất cả các phần tử có class 'product-card' const allProducts = document.querySelectorAll('.product-card'); console.log(allProducts); // Kết quả: NodeList [div.product-card, div.product-card.featured, div.product-card] // Chọn tất cả các thẻ <p> const allParagraphs = document.querySelectorAll('p'); console.log(allParagraphs); // Kết quả: NodeList [p] - chỉ có một thẻ p trong ví dụ HTML này
Giải thích:
querySelector
vàquerySelectorAll
sử dụng cú pháp selector CSS quen thuộc (.class
,#id
,tag
,tag.class
,tag#id
,parent > child
,element:pseudo-class
, v.v...).querySelector
dừng lại sau khi tìm thấy kết quả đầu tiên, cònquerySelectorAll
quét toàn bộ và trả về tất cả các kết quả tìm được. Kết quả củaquerySelectorAll
làNodeList
, trông giống mảng nhưng không có đầy đủ các phương thức như mảng (ví dụ: không cómap
,filter
, ...). Tuy nhiên, nó cóforEach
và có thể dùng vòng lặpfor...of
hoặcfor
truyền thống.
2. Thay đổi nội dung của phần tử
Sau khi đã chọn được phần tử, việc thay đổi nội dung bên trong nó là rất phổ biến. Có hai thuộc tính chính để làm điều này: textContent
và innerHTML
.
textContent
: Thay đổi hoặc lấy về nội dung _chỉ là văn bản thuần_ của phần tử và tất cả các phần tử con của nó. An toàn và không bị ảnh hưởng bởi các thẻ HTML.innerHTML
: Thay đổi hoặc lấy về toàn bộ nội dung _bao gồm cả các thẻ HTML_ bên trong phần tử đó. Mạnh mẽ hơn nhưng cần cẩn thận với vấn đề bảo mật (Cross-Site Scripting - XSS) nếu nội dung đến từ nguồn không đáng tin cậy.HTML:
<h1 id="mainTitle">Tiêu đề ban đầu</h1> <div id="contentArea"> <p>Đoạn văn <strong>in đậm</strong>.</p> </div>
JavaScript:
const mainTitle = document.getElementById('mainTitle'); const contentArea = document.getElementById('contentArea'); // Thay đổi textContent của tiêu đề mainTitle.textContent = 'Tiêu đề mới hấp dẫn!'; console.log(mainTitle.textContent); // Kết quả in ra console: "Tiêu đề mới hấp dẫn!" // Thay đổi innerHTML của khu vực nội dung contentArea.innerHTML = '<h2>Đây là tiêu đề phụ mới</h2><p>Nội dung mới với _chữ nghiêng_.</p>'; console.log(contentArea.innerHTML); // Kết quả in ra console sẽ là chuỗi HTML vừa được gán vào. // Minh họa sự khác nhau: innerHTML giữ lại thẻ strong, textContent thì không const paragraphInside = document.querySelector('#contentArea p'); console.log(paragraphInside.innerHTML); // Kết quả (trước khi innerHTML của contentArea bị thay đổi): "Đoạn văn <strong>in đậm</strong>." console.log(paragraphInside.textContent); // Kết quả (trước khi innerHTML của contentArea bị thay đổi): "Đoạn văn in đậm."
Giải thích:
textContent
chỉ thao tác với văn bản. Khi bạn gán một chuỗi vàotextContent
, mọi ký tự HTML đặc biệt (<, >, &...) sẽ được xử lý như văn bản thông thường. Ngược lại,innerHTML
sẽ phân tích chuỗi được gán như mã HTML, cho phép bạn thêm các thẻ, áp dụng style inline, v.v... Luôn ưu tiên dùngtextContent
nếu bạn chỉ cần thay đổi văn bản thuần để tránh rủi ro XSS.
3. Thay đổi thuộc tính (Attributes) của phần tử
Các phần tử HTML có các thuộc tính như href
, src
, class
, id
, alt
, title
, v.v... Chúng ta có thể thay đổi, thêm, hoặc xóa bỏ các thuộc tính này bằng JavaScript.
setAttribute(name, value)
: Đặt giá trị cho một thuộc tính. Nếu thuộc tính chưa tồn tại, nó sẽ được thêm vào.getAttribute(name)
: Lấy giá trị hiện tại của một thuộc tính.removeAttribute(name)
: Xóa bỏ một thuộc tính.HTML:
<a id="websiteLink" href="https://oldlink.com">Truy cập website</a> <img id="productImage" src="old-image.jpg" alt="Ảnh sản phẩm cũ">
JavaScript:
const websiteLink = document.getElementById('websiteLink'); const productImage = document.getElementById('productImage'); // Thay đổi thuộc tính href của link websiteLink.setAttribute('href', 'https://newlink.com'); websiteLink.textContent = 'Truy cập trang web mới!'; // Đồng thời thay đổi nội dung hiển thị // Thêm thuộc tính target="_blank" để mở link trong tab mới websiteLink.setAttribute('target', '_blank'); // Lấy giá trị thuộc tính src của ảnh const currentSrc = productImage.getAttribute('src'); console.log('Đường dẫn ảnh hiện tại:', currentSrc); // Thay đổi thuộc tính src và alt của ảnh productImage.setAttribute('src', 'new-image.jpg'); productImage.setAttribute('alt', 'Ảnh sản phẩm mới'); // Xóa thuộc tính alt của ảnh (ví dụ) // productImage.removeAttribute('alt');
Giải thích: Các phương thức
setAttribute
,getAttribute
,removeAttribute
giúp bạn tương tác trực tiếp với các thuộc tính HTML. Lưu ý rằng một số thuộc tính phổ biến nhưid
,href
,src
,className
(lưu ýclassName
trong JS tương ứng vớiclass
trong HTML),value
có thể được truy cập trực tiếp như các thuộc tính của đối tượng DOM element (ví dụ:element.id
,element.href = '...'
,element.className = '...'
). Tuy nhiên,setAttribute
là cách tổng quát nhất dùng được cho mọi thuộc tính, kể cả cácdata-*
attributes tùy chỉnh.
4. Thay đổi kiểu dáng (Styles) của phần tử
Bạn có thể thay đổi trực tiếp các kiểu dáng CSS của một phần tử thông qua thuộc tính style
.
HTML:
<p id="styledText">Đoạn văn bản cần tô màu.</p> <button id="toggleBtn">Bật/Tắt màu</button>
JavaScript:
const styledText = document.getElementById('styledText'); const toggleBtn = document.getElementById('toggleBtn'); // Thay đổi trực tiếp màu chữ và background color styledText.style.color = 'blue'; styledText.style.backgroundColor = '#f0f0f0'; // Lưu ý dùng camelCase cho background-color styledText.style.fontWeight = 'bold'; // camelCase cho font-weight // Ví dụ kết hợp với sự kiện (sẽ học kỹ hơn sau): // Khi click nút, thay đổi màu chữ toggleBtn.addEventListener('click', function() { if (styledText.style.color === 'blue') { styledText.style.color = 'red'; } else { styledText.style.color = 'blue'; } });
Giải thích: Mỗi phần tử DOM có một thuộc tính
style
, là một đối tượng chứa tất cả các kiểu dáng CSS được áp dụng trực tiếp cho phần tử đó (style inline). Bạn truy cập các thuộc tính CSS thông qua đối tượngstyle
này. Lưu ý quan trọng: Các thuộc tính CSS có dấu gạch ngang (nhưbackground-color
,font-size
) sẽ được viết bằng camelCase trong JavaScript (thànhbackgroundColor
,fontSize
).Lưu ý quan trọng về Style: Thay đổi style trực tiếp bằng thuộc tính
style
chỉ áp dụng style inline và thường không phải là cách tốt nhất để quản lý giao diện phức tạp. Cách _được khuyến khích_ hơn là sử dụng JavaScript để thêm/xóa các class CSS (sẽ nói rõ ở mục 5).
5. Làm việc với Class CSS (Cách quản lý Style hiệu quả hơn)
Như đã đề cập, thay vì thay đổi style trực tiếp, việc thêm hoặc xóa các class CSS vào phần tử thường là cách linh hoạt và dễ quản lý hơn. Mỗi phần tử DOM có một thuộc tính classList
, là một đối tượng cung cấp các phương thức tiện lợi để làm việc với danh sách class của phần tử đó.
classList.add(className1, className2, ...)
: Thêm một hoặc nhiều class vào phần tử.classList.remove(className1, className2, ...)
: Xóa một hoặc nhiều class khỏi phần tử.classList.toggle(className, force)
: Nếu class tồn tại, nó sẽ bị xóa; nếu không, nó sẽ được thêm vào. Đối số thứ haiforce
(true/false) có thể ép buộc thêm hoặc xóa class.classList.contains(className)
: Kiểm tra xem phần tử có chứa class được chỉ định hay không (trả về true/false).HTML:
<style> .highlight { border: 2px solid yellow; padding: 10px; } .error-text { color: red; font-weight: bold; } .hidden { display: none; } </style> <div id="classyDiv" class="box">Div ban đầu</div> <button id="addClassBtn">Thêm highlight</button> <button id="toggleClassBtn">Ẩn/Hiện</button>
JavaScript:
const classyDiv = document.getElementById('classyDiv'); const addClassBtn = document.getElementById('addClassBtn'); const toggleClassBtn = document.getElementById('toggleClassBtn'); // Thêm class 'error-text' ngay khi trang tải classyDiv.classList.add('error-text'); // Khi click nút 'Thêm highlight' addClassBtn.addEventListener('click', function() { // Kiểm tra xem đã có class 'highlight' chưa if (!classyDiv.classList.contains('highlight')) { classyDiv.classList.add('highlight'); console.log('Đã thêm class highlight.'); } else { console.log('Class highlight đã tồn tại.'); } }); // Khi click nút 'Ẩn/Hiện' toggleClassBtn.addEventListener('click', function() { // Bật/tắt class 'hidden' classyDiv.classList.toggle('hidden'); console.log('Đã toggle class hidden.'); }); console.log('Div ban đầu có class "box"?', classyDiv.classList.contains('box')); // true
Giải thích:
classList
là cách _hiệu quả và sạch sẽ_ để áp dụng nhiều kiểu dáng cùng lúc hoặc thay đổi giao diện dựa trên trạng thái. Thay vì viết nhiều dòngelement.style.property = value
, bạn chỉ cần định nghĩa sẵn các bộ style trong CSS và dùngclassList.add()
hoặcclassList.toggle()
để áp dụng chúng. Điều này giúp tách biệt logic JavaScript khỏi phần trình bày (CSS), làm code dễ đọc, dễ bảo trì hơn.toggle
là phương thức cực kỳ hữu ích cho các chức năng bật/tắt (dropdown, menu, modal...).
6. Tạo và thêm phần tử mới vào DOM
Trang web không chỉ thay đổi nội dung sẵn có mà còn có thể tạo ra các phần tử mới "trên bay" và chèn chúng vào trang.
document.createElement(tagName)
: Tạo ra một phần tử HTML mới với tên thẻ được chỉ định (ví dụ:document.createElement('div')
,document.createElement('p')
). Phần tử này tồn tại trong bộ nhớ nhưng chưa có trên trang web.parentNode.appendChild(childElement)
: Thêm một phần tử _con_ vào cuối danh sách các phần tử con của một phần tử _cha_.parentNode.insertBefore(newElement, referenceElement)
: Chèn một phần tử _mới_ vào trước một phần tử _tham chiếu_ đã tồn tại trong danh sách con của phần tử _cha_.HTML:
<div id="container"> <!-- Các phần tử được thêm bởi JS sẽ ở đây --> <p id="firstPara">Đoạn văn bản đầu tiên.</p> </div>
JavaScript:
const container = document.getElementById('container'); const firstPara = document.getElementById('firstPara'); // 1. Tạo một phần tử <p> mới const newParagraph = document.createElement('p'); // Đặt nội dung cho nó newParagraph.textContent = 'Đây là đoạn văn mới được tạo bằng JS!'; // Thêm một class (nếu cần) newParagraph.classList.add('dynamic-content'); // 2. Thêm phần tử mới vào cuối container container.appendChild(newParagraph); console.log('Đã thêm đoạn văn mới vào cuối container.'); // 3. Tạo một phần tử <h2> mới const newHeading = document.createElement('h2'); newHeading.textContent = 'Tiêu đề thêm vào trước đoạn đầu tiên'; // 4. Thêm phần tử <h2> mới vào trước phần tử firstPara (phần tử tham chiếu) container.insertBefore(newHeading, firstPara); console.log('Đã thêm tiêu đề mới vào trước đoạn đầu tiên.'); // Tạo thêm một span và chèn nó vào giữa newHeading và firstPara const separatorSpan = document.createElement('span'); separatorSpan.textContent = ' --- '; container.insertBefore(separatorSpan, firstPara); // Vẫn dùng firstPara làm tham chiếu console.log('Đã thêm span phân cách.');
Giải thích:
createElement
chỉ tạo ra đối tượng trong bộ nhớ. Để nó hiển thị trên trang, bạn phải gắn nó vào một phần tử cha đã có mặt trong DOM bằngappendChild
hoặcinsertBefore
.appendChild
là cách phổ biến nhất để thêm vào cuối.insertBefore
cho phép kiểm soát vị trí chèn chi tiết hơn.
7. Xóa bỏ phần tử khỏi DOM
Khi không cần một phần tử nữa, bạn có thể xóa nó khỏi cây DOM. Có hai cách chính.
parentNode.removeChild(childElement)
: Xóa một phần tử _con_ cụ thể khỏi phần tử _cha_ của nó. Bạn cần biết cả phần tử cha và phần tử con.element.remove()
: Xóa trực tiếp phần tử đó. Đây là cách hiện đại và thường gọn gàng hơn nếu bạn đã có tham chiếu đến phần tử muốn xóa.HTML:
<div id="parentElement"> <p id="childToRemove">Đoạn văn bản này sẽ bị xóa.</p> <p id="anotherChild">Đoạn văn bản khác.</p> </div> <button id="removeBtn">Xóa đoạn văn</button>
JavaScript:
const parentElement = document.getElementById('parentElement'); const childToRemove = document.getElementById('childToRemove'); const removeBtn = document.getElementById('removeBtn'); // Cách 1: Sử dụng removeChild (cần biết cha) // parentElement.removeChild(childToRemove); // console.log('Đã xóa phần tử con bằng removeChild.'); // Cách 2: Sử dụng remove() (thường đơn giản hơn) // Khi click nút "Xóa đoạn văn" removeBtn.addEventListener('click', function() { // Kiểm tra xem phần tử còn tồn tại trong DOM không trước khi xóa if (childToRemove) { // Hoặc parentElement.contains(childToRemove) childToRemove.remove(); console.log('Đã xóa phần tử con bằng remove().'); } else { console.log('Phần tử đã bị xóa trước đó rồi.'); } });
Giải thích: Phương thức
remove()
(phổ biến hơn) trực tiếp gọi lên phần tử mà bạn muốn xóa. Phương thứcremoveChild
gọi lên phần tử cha và truyền vào phần tử con muốn xóa. Cả hai đều loại bỏ phần tử khỏi DOM, khiến nó không còn hiển thị trên trang nữa.
8. Thao tác với sự kiện (Events)
Mặc dù chủ đề sự kiện rất rộng và xứng đáng có bài riêng, nhưng DOM manipulation thường đi đôi với việc phản ứng lại các sự kiện của người dùng (click, submit form, hover...). Phương thức phổ biến nhất để lắng nghe sự kiện là addEventListener()
.
element.addEventListener(event, handler)
: Gắn một hàm (handler) sẽ được thực thi khi một loại sự kiện (event) cụ thể xảy ra trên phần tử đó.HTML:
<button id="clickMeBtn">Click vào đây!</button> <p id="eventOutput">Kết quả sự kiện sẽ hiển thị ở đây.</p>
JavaScript:
const clickMeBtn = document.getElementById('clickMeBtn'); const eventOutput = document.getElementById('eventOutput'); // Thêm lắng nghe sự kiện 'click' cho nút clickMeBtn.addEventListener('click', function() { // Khi nút được click, thay đổi nội dung của đoạn văn eventOutput.textContent = 'Nút đã được click vào lúc: ' + new Date().toLocaleTimeString(); eventOutput.style.color = 'green'; }); // Thêm lắng nghe sự kiện 'mouseover' (rê chuột vào) clickMeBtn.addEventListener('mouseover', function() { clickMeBtn.style.backgroundColor = 'yellow'; }); // Thêm lắng nghe sự kiện 'mouseout' (rê chuột ra) clickMeBtn.addEventListener('mouseout', function() { clickMeBtn.style.backgroundColor = ''; // Xóa background để về mặc định });
Giải thích:
addEventListener
nhận hai đối số chính: tên sự kiện dưới dạng chuỗi (ví dụ: 'click', 'mouseover', 'submit', 'keydown') và một hàm (gọi là callback function hoặc event handler) sẽ chạy khi sự kiện đó xảy ra trên phần tử được chọn. Đây là nền tảng cho mọi tương tác động trên trang web của bạn.
Comments