Bài 5.1: Giới thiệu DOM và cách truy cập

Chào mừng bạn trở lại với chuỗi bài viết về Lập trình Web Front-end! Sau khi đã nắm vững cấu trúc HTML và cách "làm đẹp" với CSS, đã đến lúc chúng ta tìm hiểu cách thêm sự sống độngtương tác cho trang web của mình. Và để làm được điều đó, chúng ta cần làm quen với một khái niệm cực kỳ quan trọng: DOM - Document Object Model.

Hãy tưởng tượng trang web HTML của bạn như một ngôi nhà tĩnh lặng với các bức tường, cửa ra vào, cửa sổ được định vị sẵn (HTML) và đã được sơn sửa trang trí lộng lẫy (CSS). Nhưng làm thế nào để bạn có thể mở cửa, kéo rèm, bật đèn, hoặc thậm chí là thêm một căn phòng mới sau khi ngôi nhà đã được xây xong? Đây chính là lúc JavaScriptDOM phát huy sức mạnh của mình!

DOM là gì? "Bản đồ" sống của trang web

DOM là viết tắt của Document Object Model - tạm dịch là Mô hình Đối tượng Tài liệu. Đơn giản mà nói, khi trình duyệt web tải một trang HTML, nó sẽ không chỉ hiển thị nội dung mà còn tạo ra một biểu diễn có cấu trúc của trang đó trong bộ nhớ. Cái biểu diễn này chính là DOM.

Bạn có thể hình dung DOM như một cây cấu trúc (tree structure), nơi mỗi phần tử HTML (như <p>, <div>, <h1>, <img>, v.v.), mỗi thuộc tính (như id, class, src), thậm chí là mỗi đoạn text bên trong các thẻ đều được biểu diễn dưới dạng một nút (node). Nút gốc của cây này là đối tượng document, đại diện cho toàn bộ trang web.

  • HTML: Là văn bản nguồn mô tả cấu trúc.
  • CSS: Là các quy tắc mô tả hình thức.
  • DOM: Là biểu diễn sống của cấu trúc đó trong bộ nhớ, mà JavaScript có thể truy cập và thao tác.

Quan trọng hơn, DOM không chỉ là một cấu trúc dữ liệu. Nó còn là một API (Application Programming Interface - Giao diện Lập trình Ứng dụng). Điều này có nghĩa là DOM cung cấp cho chúng ta một tập hợp các phương thức (methods)thuộc tính (properties) mà JavaScript có thể sử dụng để:

  • Tìm kiếm và truy cập bất kỳ nút nào trong cây.
  • Thay đổi nội dung văn bản của các nút.
  • Thay đổi thuộc tính của các nút (ví dụ: thay đổi src của thẻ <img>, thay đổi href của thẻ <a>).
  • Thay đổi kiểu dáng (CSS) của các nút.
  • Thêm, xóa hoặc di chuyển các nút.
  • Phản ứng với các sự kiện từ người dùng (như click, gõ phím, di chuột).

Chính khả năng tương tác với DOM này đã biến JavaScript trở thành ngôn ngữ kịch bản mạnh mẽ cho web, cho phép chúng ta tạo ra các ứng dụng web độngphản hồi.

Tại sao cần truy cập các phần tử DOM?

Như đã đề cập ở trên, để JavaScript có thể "làm việc" với trang web (thay đổi nội dung, kiểu dáng, bắt sự kiện...), nó cần phải tìm thấytruy cập được phần tử HTML cụ thể mà chúng ta muốn tương tác. Bạn không thể nói với JavaScript là "thay đổi màu nền của cái nút đấy" nếu bạn không chỉ cho nó biết "cái nút đấy" là cái nào trong hàng trăm, hàng nghìn phần tử trên trang.

Việc truy cập các phần tử DOM là bước đầu tiên và quan trọng nhất trước khi thực hiện bất kỳ thao tác nào khác. DOM cung cấp nhiều phương thức khác nhau để làm điều này, mỗi phương thức có ưu điểm và trường hợp sử dụng riêng.

Các phương thức truy cập phần tử DOM phổ biến

Tất cả các phương thức truy cập DOM đều được gọi thông qua đối tượng document (hoặc đôi khi là một phần tử cha khác). Dưới đây là các phương thức bạn sẽ thường xuyên sử dụng:

1. document.getElementById(id)

Đây là phương thức phổ biến và nhanh nhất để truy cập một phần tử duy nhất dựa vào thuộc tính id của nó. Thuộc tính id được thiết kế để duy nhất trên toàn bộ trang HTML.

  • HTML:
    <div id="main-content">
        <p>Đây là nội dung chính.</p>
    </div>
    <button id="myButton">Click Me</button>
    
  • JavaScript:

    // Lấy phần tử có id="main-content"
    const mainContentDiv = document.getElementById('main-content');
    console.log(mainContentDiv); // <div id="main-content">...</div>
    
    // Lấy phần tử có id="myButton"
    const clickButton = document.getElementById('myButton');
    console.log(clickButton); // <button id="myButton">Click Me</button>
    
    // Nếu không tìm thấy, nó trả về null
    const nonExistentElement = document.getElementById('non-existent-id');
    console.log(nonExistentElement); // null
    
  • Giải thích: Phương thức getElementById() nhận vào một chuỗi là giá trị của thuộc tính id mà bạn muốn tìm. Nó trả về duy nhất một phần tử nếu tìm thấy, hoặc null nếu không tìm thấy. Do tính chất duy nhất của id, phương thức này rất hiệu quả.
2. document.getElementsByClassName(className)

Phương thức này cho phép bạn truy cập một tập hợp các phần tử dựa trên thuộc tính class của chúng. Một phần tử có thể có nhiều lớp (class), và phương thức này sẽ trả về tất cả các phần tử có chứa ít nhất một trong các lớp được chỉ định.

  • HTML:
    <ul>
        <li class="item active">Item 1</li>
        <li class="item">Item 2</li>
        <li class="item active">Item 3</li>
    </ul>
    <div class="item other-class">Another item-like div</div>
    
  • JavaScript:

    // Lấy tất cả các phần tử có class="item"
    const listItems = document.getElementsByClassName('item');
    console.log(listItems); // HTMLCollection [li.item.active, li.item, li.item.active, div.item.other-class]
    
    // Lấy tất cả các phần tử có class="active"
    const activeItems = document.getElementsByClassName('active');
    console.log(activeItems); // HTMLCollection [li.item.active, li.item.active]
    
    // HTMLCollection là một tập hợp giống mảng, bạn có thể truy cập theo chỉ mục
    console.log(listItems[0]); // <li class="item active">Item 1</li>
    
    // Hoặc lặp qua nó
    for (let i = 0; i < listItems.length; i++) {
        console.log(listItems[i].textContent); // Item 1, Item 2, Item 3, Another item-like div
    }
    
  • Giải thích: Phương thức getElementsByClassName() nhận vào một chuỗi (hoặc nhiều chuỗi phân tách bằng dấu cách nếu muốn tìm phần tử có tất cả các class đó, nhưng cách này ít dùng hơn). Nó trả về một HTMLCollection - một dạng tập hợp các phần tử giống như mảng nhưng không có đầy đủ các phương thức của mảng (như map, filter). Bạn cần truy cập các phần tử trong HTMLCollection bằng chỉ mục số ([index]) hoặc lặp qua nó. Tập hợp này là live, nghĩa là nếu cấu trúc DOM thay đổi (thêm/bớt phần tử có class đó), HTMLCollection này sẽ tự động cập nhật.
3. document.getElementsByTagName(tagName)

Phương thức này cho phép bạn truy cập một tập hợp các phần tử dựa trên tên thẻ HTML của chúng (ví dụ: div, p, a, li).

  • HTML:
    <h1>Tiêu đề chính</h1>
    <p>Đoạn văn thứ nhất.</p>
    <p>Đoạn văn thứ hai với <strong>chữ in đậm</strong>.</p>
    
  • JavaScript:

    // Lấy tất cả các phần tử <p>
    const paragraphs = document.getElementsByTagName('p');
    console.log(paragraphs); // HTMLCollection [p, p]
    
    // Lấy tất cả các phần tử <strong>
    const strongTags = document.getElementsByTagName('strong');
    console.log(strongTags); // HTMLCollection [strong]
    
    // Truy cập và lặp giống getElementsByClassName
    console.log(paragraphs[1].textContent); // Đoạn văn thứ hai với chữ in đậm.
    
  • Giải thích: Tương tự như getElementsByClassName(), phương thức getElementsByTagName() trả về một HTMLCollection chứa tất cả các phần tử có tên thẻ được chỉ định. Tập hợp này cũng là live.
4. document.querySelector(selector)

Đây là một phương thức hiện đạirất mạnh mẽ. Nó cho phép bạn truy cập phần tử đầu tiên khớp với bộ chọn CSS (CSS selector) mà bạn cung cấp.

  • HTML: (Sử dụng lại ví dụ trên để minh họa)
    <div id="main-content">
        <p class="info first-paragraph">Đây là đoạn văn đầu tiên.</p>
        <p class="info">Đây là đoạn văn thứ hai.</p>
    </div>
    <ul>
        <li class="item active">Item 1</li>
        <li class="item">Item 2</li>
    </ul>
    
  • JavaScript:

    // Lấy phần tử có id="main-content" (giống getElementById)
    const mainDiv = document.querySelector('#main-content');
    console.log(mainDiv); // <div id="main-content">...</div>
    
    // Lấy phần tử *đầu tiên* có class="info"
    const firstInfo = document.querySelector('.info');
    console.log(firstInfo); // <p class="info first-paragraph">Đây là đoạn văn đầu tiên.</p>
    
    // Lấy phần tử *đầu tiên* có tên thẻ <p> (giống getElementsByTagName, nhưng chỉ lấy 1)
    const firstParagraph = document.querySelector('p');
    console.log(firstParagraph); // <p class="info first-paragraph">Đây là đoạn văn đầu tiên.</p>
    
    // Sử dụng bộ chọn phức tạp
    // Lấy phần tử <li> đầu tiên nằm trong <ul> và có class "active"
    const firstActiveItemInList = document.querySelector('ul li.active');
    console.log(firstActiveItemInList); // <li class="item active">Item 1</li>
    
    // Nếu không tìm thấy, trả về null
    const nonExistentSelector = document.querySelector('.non-existent-class');
    console.log(nonExistentSelector); // null
    
  • Giải thích: querySelector() nhận vào một chuỗi là bộ chọn CSS hợp lệ. Nó tìm kiếm toàn bộ cây DOM và trả về phần tử đầu tiên mà nó tìm thấy khớp với bộ chọn đó. Nếu không có phần tử nào khớp, nó trả về null. Sự linh hoạt của việc sử dụng bộ chọn CSS làm cho phương thức này cực kỳ hữu ích.
5. document.querySelectorAll(selector)

Cũng sử dụng bộ chọn CSS, nhưng thay vì chỉ trả về phần tử đầu tiên, querySelectorAll() sẽ trả về một tập hợp của tất cả các phần tử khớp với bộ chọn đó.

  • HTML: (Sử dụng lại ví dụ trên)
    <div id="main-content">
        <p class="info first-paragraph">Đây là đoạn văn đầu tiên.</p>
        <p class="info">Đây là đoạn văn thứ hai.</p>
    </div>
    <ul>
        <li class="item active">Item 1</li>
        <li class="item">Item 2</li>
        <li class="item active">Item 3</li>
    </ul>
    
  • JavaScript:

    // Lấy *tất cả* các phần tử có class="info" (giống getElementsByClassName)
    const allInfoParagraphs = document.querySelectorAll('.info');
    console.log(allInfoParagraphs); // NodeList [p.info.first-paragraph, p.info]
    
    // Lấy *tất cả* các phần tử <p> (giống getElementsByTagName)
    const allParagraphs = document.querySelectorAll('p');
    console.log(allParagraphs); // NodeList [p.info.first-paragraph, p.info]
    
    // Lấy *tất cả* các phần tử <li> trong <ul> có class "active"
    const allActiveItemsInList = document.querySelectorAll('ul li.active');
    console.log(allActiveItemsInList); // NodeList [li.item.active, li.item.active]
    
    // NodeList cũng giống mảng, bạn có thể truy cập theo chỉ mục
    console.log(allInfoParagraphs[0].textContent); // Đây là đoạn văn đầu tiên.
    
    // NodeList có phương thức forEach để lặp
    allActiveItemsInList.forEach(item => {
        console.log(item.textContent); // Item 1, Item 3
    });
    
    // Nếu không tìm thấy, trả về NodeList rỗng []
    const emptyNodeList = document.querySelectorAll('.non-existent-selector');
    console.log(emptyNodeList); // NodeList []
    
  • Giải thích: querySelectorAll() cũng nhận vào một chuỗi là bộ chọn CSS hợp lệ. Nó trả về một NodeList chứa tất cả các phần tử khớp với bộ chọn. Khác với HTMLCollection, NodeList được trả về bởi querySelectorAll thường là static (tĩnh) ở thời điểm nó được tạo, nghĩa là nó sẽ không tự động cập nhật nếu DOM thay đổi sau đó (trừ một số trường hợp đặc biệt, nhưng hãy coi là static cho đơn giản ban đầu). NodeList cung cấp phương thức forEach giúp lặp qua các phần tử dễ dàng hơn so với HTMLCollection truyền thống, và cũng có thể truy cập bằng chỉ mục.

Nên dùng phương thức nào?

  • Sử dụng getElementById() khi bạn cần truy cập chỉ một phần tử duy nhất dựa trên id của nó. Đây là phương thức nhanh nhất.
  • Sử dụng getElementsByClassName() hoặc getElementsByTagName() khi bạn cần lấy một tập hợp các phần tử dựa trên class hoặc tên thẻ, và bạn cần một tập hợp live (tự cập nhật). Tuy nhiên, cần nhớ rằng chúng trả về HTMLCollection, không có đầy đủ các phương thức mảng tiện lợi như forEach.
  • Sử dụng querySelector() khi bạn cần lấy phần tử đầu tiên khớp với một bộ chọn phức tạp hoặc khi bạn muốn sự linh hoạt của bộ chọn CSS thay vì chỉ dựa vào ID, Class hay Tag name.
  • Sử dụng querySelectorAll() khi bạn cần lấy tất cả các phần tử khớp với một bộ chọn phức tạp. Phương thức này rất linh hoạt và trả về NodeList dễ làm việc (có forEach).

Trong thực tế phát triển web hiện đại, querySelector()querySelectorAll() thường được sử dụng phổ biến nhất do sự linh hoạt và mạnh mẽ của việc sử dụng bộ chọn CSS, giúp code ngắn gọn và dễ đọc hơn rất nhiều so với việc kết hợp getElementsBy... truyền thống.

Ví dụ nhỏ về thao tác sau khi truy cập

Để thấy được ý nghĩa của việc truy cập DOM, hãy xem một ví dụ đơn giản về cách thay đổi nội dung và kiểu dáng của một phần tử sau khi đã truy cập được nó:

  • HTML:
    <div id="greeting">Xin chào!</div>
    <button id="changeTextBtn">Thay đổi lời chào</button>
    
  • JavaScript:

    // 1. Truy cập phần tử
    const greetingDiv = document.getElementById('greeting');
    const changeButton = document.getElementById('changeTextBtn');
    
    // 2. Thêm sự kiện (sẽ học kỹ hơn ở bài sau)
    changeButton.addEventListener('click', () => {
        // 3. Thao tác với phần tử đã truy cập
        greetingDiv.textContent = 'Xin chào thế giới DOM!'; // Thay đổi nội dung text
        greetingDiv.style.color = 'blue'; // Thay đổi màu chữ
        greetingDiv.style.fontWeight = 'bold'; // In đậm chữ
    });
    
  • Giải thích:
    • Đầu tiên, chúng ta dùng getElementById để lấy tham chiếu đến thẻ <div> và thẻ <button>.
    • Sau đó, chúng ta thêm một "người lắng nghe sự kiện" (event listener) vào nút. Khi nút được click ('click'), hàm bên trong sẽ được thực thi.
    • Bên trong hàm, chúng ta sử dụng các thuộc tính của đối tượng greetingDiv (đã truy cập được) để thay đổi nội dung (textContent) và kiểu dáng (style). style là một đối tượng chứa các thuộc tính CSS (được viết theo kiểu camelCase).

Ví dụ này cho thấy chỉ cần có được "cánh cửa" để vào DOM (bằng các phương thức truy cập), bạn có thể bắt đầu điều khiển và biến đổi trang web của mình theo ý muốn bằng JavaScript.

Comments

There are no comments at the moment.