Bài 5.3: Sự kiện trong DOM

Chào mừng các bạn đến với Bài 5.3 trong series Lập trình Web Front-end! Sau khi đã tìm hiểu về cấu trúc trang HTML, tạo kiểu với CSS và thao tác với cấu trúc DOM bằng JavaScript, hôm nay chúng ta sẽ đi sâu vào một khía cạnh cực kỳ quan trọng giúp trang web của bạn trở nên sống độngphản ứng với người dùng: Sự kiện trong DOM.

Nếu DOM là bộ xương và cơ bắp của trang web, thì sự kiện chính là hệ thần kinh, giúp trang web cảm nhận và phản hồi lại những tương tác từ thế giới bên ngoài (chủ yếu là từ người dùng).

Sự kiện DOM là gì?

Trong bối cảnh lập trình web, sự kiện (Event) là một tín hiệu cho biết có điều gì đó đã xảy ra trên trang web. Đây có thể là hành động do người dùng thực hiện (như nhấp chuột, gõ phím, cuộn trang) hoặc các sự kiện do trình duyệt tạo ra (như trang đã tải xong, video đã kết thúc phát).

Khi một sự kiện xảy ra, trình duyệt sẽ tạo ra một đối tượng sự kiện (Event Object) chứa thông tin chi tiết về sự kiện đó và kích hoạt các hàm xử lý sự kiện (event handlers) mà chúng ta đã đăng ký.

Hãy tưởng tượng bạn có một cái nút trên trang web. Khi người dùng nhấp vào nút đó, trình duyệt sẽ nhận biết hành động "nhấp chuột" (click) và tạo ra một sự kiện click. Nhiệm vụ của chúng ta là "lắng nghe" sự kiện này và thực hiện một hành động nào đó (ví dụ: hiển thị thông báo, thay đổi nội dung, gửi dữ liệu).

Tại sao Sự kiện lại quan trọng?

Sự kiện là linh hồn của các ứng dụng web động. Chúng cho phép chúng ta:

  1. Tạo tương tác người dùng: Phản ứng khi người dùng nhấp chuột, di chuyển chuột, gõ phím, kéo thả,...
  2. Xử lý form: Kiểm tra dữ liệu trước khi gửi, tự động điền, phản hồi khi người dùng thay đổi giá trị nhập liệu.
  3. Quản lý trạng thái trang: Biết khi nào trang đã tải hoàn chỉnh, khi nào các tài nguyên (hình ảnh, video) đã sẵn sàng.
  4. Xây dựng giao diện người dùng phức tạp: Mở/đóng menu dropdown, hiển thị modal, tạo hiệu ứng cuộn trang mượt mà.

Nói cách khác, mọi thứ khiến một trang web không chỉ là một tài liệu đọc mà trở thành một ứng dụng tương tác đều dựa trên sự kiện.

Các loại Sự kiện phổ biến

Có hàng trăm loại sự kiện khác nhau trong DOM, nhưng một số loại phổ biến mà bạn sẽ gặp thường xuyên bao gồm:

  • Sự kiện Chuột (Mouse Events):
    • click: Khi một phần tử được nhấp chuột.
    • dblclick: Khi một phần tử được nhấp đúp chuột.
    • mousedown/mouseup: Khi nút chuột được nhấn xuống/thả ra trên phần tử.
    • mouseover/mouseout: Khi con trỏ chuột di chuyển vào/ra khỏi phần tử.
    • mousemove: Khi con trỏ chuột di chuyển trên phần tử.
    • contextmenu: Khi người dùng nhấp chuột phải (thường mở menu ngữ cảnh).
  • Sự kiện Bàn phím (Keyboard Events):
    • keydown: Khi một phím trên bàn phím được nhấn xuống.
    • keyup: Khi một phím trên bàn phím được nhả ra.
    • keypress: Khi một phím tạo ra ký tự được nhấn (ít dùng hơn keydown/keyup).
  • Sự kiện Form (Form Events):
    • submit: Khi một form được gửi đi.
    • change: Khi giá trị của một phần tử input, select, hoặc textarea thay đổi và phần tử mất focus.
    • input: Khi giá trị của một phần tử input, textarea thay đổi ngay lập tức.
    • focus/blur: Khi một phần tử nhận focus/mất focus.
  • Sự kiện Tài liệu/Cửa sổ (Document/Window Events):
    • DOMContentLoaded: Khi cấu trúc HTML của trang đã được tải và phân tích xong (nhưng có thể các tài nguyên như hình ảnh, CSS chưa tải xong). Đây thường là thời điểm tốt nhất để bắt đầu thao tác DOM bằng JavaScript.
    • load: Khi toàn bộ trang, bao gồm tất cả tài nguyên phụ (hình ảnh, CSS, script), đã được tải xong.
    • resize: Khi kích thước cửa sổ trình duyệt thay đổi.
    • scroll: Khi người dùng cuộn trang.

Đây chỉ là một phần nhỏ, còn rất nhiều loại sự kiện khác như các sự kiện liên quan đến kéo thả (dragstart, drop), sự kiện cảm ứng trên thiết bị di động (touchstart, touchend), sự kiện media (play, pause),...

Xử lý Sự kiện (Event Handling)

Để phản ứng lại một sự kiện, chúng ta cần "đăng ký" một hàm (gọi là event handler hoặc event listener) để nó được gọi khi sự kiện xảy ra trên một phần tử cụ thể. Có một số cách để làm điều này:

1. Sử dụng thuộc tính Inline HTML (Không khuyến khích)

Đây là cách cũ và thường không được khuyến khích vì nó trộn lẫn logic JavaScript vào cấu trúc HTML, làm cho code khó đọc, khó bảo trì và khó tách biệt các vai trò (HTML cho cấu trúc, CSS cho kiểu dáng, JS cho hành vi).

<button onclick="alert('Chào bạn!')">Nhấn vào đây</button>
  • Giải thích: Thuộc tính onclick trên thẻ <button> được gán bằng một chuỗi chứa code JavaScript. Khi nút được click, code trong chuỗi này sẽ được thực thi.
2. Sử dụng thuộc tính DOM Element

Bạn có thể gán trực tiếp một hàm vào thuộc tính sự kiện của phần tử DOM trong JavaScript.

// Giả sử bạn đã lấy được phần tử button
const myButton = document.getElementById('myBtn');

myButton.onclick = function() {
  alert('Bạn vừa nhấn nút!');
};

// Hoặc dùng arrow function
// myButton.onclick = () => {
//   alert('Bạn vừa nhấn nút!');
// };
  • Giải thích: Chúng ta lấy phần tử nút bằng ID, sau đó gán một hàm ẩn danh (anonymous function) vào thuộc tính onclick của nó. Khi nút được click, hàm này sẽ được gọi.
  • Ưu điểm: Tách biệt HTML và JavaScript tốt hơn so với cách inline.
  • Nhược điểm: Mỗi thuộc tính sự kiện (ví dụ onclick) chỉ có thể lưu trữ một hàm duy nhất. Nếu bạn gán một hàm khác vào myButton.onclick sau đó, hàm đầu tiên sẽ bị ghi đè.
3. Sử dụng addEventListener() (Cách hiện đại và khuyến khích)

Đây là phương pháp tiêu chuẩn và linh hoạt nhất để xử lý sự kiện trong JavaScript hiện đại. Phương thức addEventListener() cho phép bạn đăng ký nhiều hàm xử lý cho cùng một sự kiện trên cùng một phần tử và cung cấp nhiều tùy chọn kiểm soát.

Cú pháp cơ bản:

element.addEventListener(eventType, handlerFunction, options);
  • eventType: Tên của sự kiện (chuỗi, ví dụ: 'click', 'mouseover', 'submit'). Lưu ý là không có tiền tố "on".
  • handlerFunction: Hàm sẽ được gọi khi sự kiện xảy ra.
  • options (Tùy chọn): Một đối tượng để cấu hình, phổ biến nhất là { capture: true } (để xử lý sự kiện ở giai đoạn Capture) và { once: true } (chỉ xử lý một lần rồi tự động gỡ bỏ listener). Mặc định là { capture: false } (xử lý ở giai đoạn Bubbling).

Ví dụ 1: Đăng ký hàm xử lý click

<button id="myBtn2">Nhấn vào đây (Cách 3)</button>

<script>
  const myButton2 = document.getElementById('myBtn2');

  myButton2.addEventListener('click', function() {
    console.log('Nút đã được click!');
  });

  // Bạn có thể thêm listener khác cho cùng sự kiện
  myButton2.addEventListener('click', () => {
    alert('Lần click thứ hai!');
  });
</script>
  • Giải thích: Chúng ta lấy phần tử nút, sau đó dùng addEventListener để đăng ký hai hàm khác nhau cho sự kiện 'click'. Khi nút được click, cả hai hàm sẽ được thực thi theo thứ tự đăng ký.

Ví dụ 2: Xử lý sự kiện khi nhập liệu

<input type="text" id="myInput" placeholder="Nhập gì đó...">
<p id="displayText"></p>

<script>
  const myInput = document.getElementById('myInput');
  const displayText = document.getElementById('displayText');

  myInput.addEventListener('input', function() {
    displayText.textContent = 'Bạn đang nhập: ' + myInput.value;
  });
</script>
  • Giải thích: Sự kiện input xảy ra mỗi khi giá trị trong ô input thay đổi. Chúng ta lắng nghe sự kiện này trên <input> và cập nhật nội dung của thẻ <p> với giá trị hiện tại của input (myInput.value).

Gỡ bỏ Event Listener:

Bạn có thể dùng removeEventListener() để gỡ bỏ một hàm xử lý sự kiện đã đăng ký trước đó. Điều này hữu ích trong các tình huống cần dọn dẹp tài nguyên hoặc chỉ cần xử lý sự kiện một lần.

Lưu ý quan trọng: Để removeEventListener() hoạt động, bạn phải truyền cùng một hàm đã được truyền cho addEventListener(). Điều này có nghĩa là bạn không thể dùng hàm ẩn danh (anonymous function) nếu muốn gỡ bỏ nó sau này.

const myButton3 = document.getElementById('myBtn3');

function handleClick() {
  console.log('Clicked once and removed!');
  // Gỡ bỏ chính listener này sau khi nó được gọi
  myButton3.removeEventListener('click', handleClick);
}

myButton3.addEventListener('click', handleClick);
  • Giải thích: Chúng ta định nghĩa hàm handleClick bên ngoài, sau đó đăng ký nó bằng addEventListener. Bên trong hàm handleClick, chúng ta gọi removeEventListener với cùng tên hàm handleClick để gỡ bỏ listener đó.

Đối tượng Sự kiện (Event Object)

Khi một hàm xử lý sự kiện được gọi, trình duyệt sẽ tự động truyền vào một đối số đầu tiên. Đối số này là đối tượng sự kiện (Event object), chứa rất nhiều thông tin hữu ích về sự kiện vừa xảy ra.

const myButton4 = document.getElementById('myBtn4');

myButton4.addEventListener('click', function(event) {
  console.log(event.type);        // "click"
  console.log(event.target);      // Phần tử mà sự kiện gốc xảy ra (ví dụ: <i> bên trong button)
  console.log(event.currentTarget); // Phần tử mà listener được gắn vào (button)
  console.log(event.clientX, event.clientY); // Vị trí chuột khi click
  console.log(event);             // In toàn bộ đối tượng Event ra console
});
  • Giải thích: Đối số event (bạn có thể đặt tên khác, nhưng event hoặc e là quy ước phổ biến) trong hàm xử lý chính là đối tượng sự kiện. Chúng ta có thể truy cập các thuộc tính của nó như event.type, event.target, v.v.
    • event.target: Rất quan trọng! Nó là phần tử DOM mà sự kiện thực sự bắt nguồn từ đó. Ví dụ, nếu bạn có một <button> chứa thẻ <i> bên trong và người dùng click vào icon <i>, thì event.target sẽ là thẻ <i>, còn event.currentTarget (phần tử mà listener được gắn vào) vẫn là <button>.
    • event.currentTarget: Phần tử DOM mà addEventListener được gọi trên đó.
    • Các thuộc tính khác như clientX, clientY cung cấp thông tin vị trí chuột cho các sự kiện liên quan đến chuột; key, keyCode cho sự kiện bàn phím, v.v.

Luồng Sự kiện (Event Flow): Nổi Bọt (Bubbling) và Chụp (Capturing)

Một khái niệm quan trọng cần hiểu về sự kiện là luồng sự kiện hay lan truyền sự kiện. Khi một sự kiện xảy ra trên một phần tử trong DOM (ví dụ: click vào một nút nằm trong một div, div nằm trong body...), sự kiện đó không chỉ xảy ra trên phần tử đó mà còn "duy chuyển" qua các phần tử cha hoặc con của nó. Có hai giai đoạn trong luồng sự kiện:

  1. Giai đoạn Chụp (Capturing Phase): Sự kiện bắt đầu từ gốc của cây DOM (thường là window hoặc document) và đi xuống qua các phần tử cha đến phần tử đích mà sự kiện gốc xảy ra.
  2. Giai đoạn Đích (Target Phase): Sự kiện đến phần tử đích.
  3. Giai đoạn Nổi Bọt (Bubbling Phase): Sự kiện bắt đầu từ phần tử đích và đi ngược lên qua các phần tử cha đến gốc của cây DOM.

Hầu hết các sự kiện DOM đều có giai đoạn Bubbling. Giai đoạn Capturing ít phổ biến hơn và phải được kích hoạt rõ ràng.

Mặc định, addEventListener() lắng nghe sự kiện ở giai đoạn Bubbling. Nếu bạn muốn lắng nghe ở giai đoạn Capturing, bạn cần truyền { capture: true } làm đối số thứ ba.

Ví dụ minh họa Luồng Sự kiện:

Giả sử có cấu trúc HTML sau:

<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>

Và JavaScript:

const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const btn = document.getElementById('btn');

// Lắng nghe ở giai đoạn Bubbling (mặc định)
outer.addEventListener('click', () => { console.log('Outer Div (Bubbling)'); });
inner.addEventListener('click', () => { console.log('Inner Div (Bubbling)'); });
btn.addEventListener('click', () => { console.log('Button (Target/Bubbling)'); }); // Là phần tử đích

// Lắng nghe ở giai đoạn Capturing
outer.addEventListener('click', () => { console.log('Outer Div (Capturing)'); }, true);
inner.addEventListener('click', () => { console.log('Inner Div (Capturing)'); }, true);
// btn.addEventListener('click', ...) trên phần tử đích xảy ra ở giai đoạn Target, không phân biệt Capturing/Bubbling

Nếu bạn nhấp vào nút Click me:

  1. Sự kiện click bắt đầu từ window/document.
  2. Đi xuống theo giai đoạn Capturing: Lắng nghe trên outer (capturing) được kích hoạt -> in "Outer Div (Capturing)".
  3. Đi xuống tiếp: Lắng nghe trên inner (capturing) được kích hoạt -> in "Inner Div (Capturing)".
  4. Đến phần tử đích (btn). Listener trên btn được kích hoạt -> in "Button (Target/Bubbling)".
  5. Đi lên theo giai đoạn Bubbling: Listener trên inner (bubbling) được kích hoạt -> in "Inner Div (Bubbling)".
  6. Đi lên tiếp: Listener trên outer (bubbling) được kích hoạt -> in "Outer Div (Bubbling)".

Thứ tự in ra console sẽ là:

  • Outer Div (Capturing)
  • Inner Div (Capturing)
  • Button (Target/Bubbling)
  • Inner Div (Bubbling)
  • Outer Div (Bubbling)

Hiểu rõ luồng sự kiện giúp bạn viết code xử lý sự kiện mạnh mẽ hơn, đặc biệt là khi làm việc với ủy quyền sự kiện (event delegation), một kỹ thuật tối ưu hiệu suất bằng cách gắn một listener duy nhất vào phần tử cha để xử lý sự kiện cho nhiều phần tử con.

Ngăn chặn Hành động Mặc định và Dừng Lan truyền Sự kiện

Đối tượng sự kiện cung cấp hai phương thức quan trọng để kiểm soát hành vi của sự kiện:

1. event.preventDefault()

Phương thức này ngăn chặn hành động mặc định mà trình duyệt sẽ thực hiện khi sự kiện xảy ra.

Ví dụ:

  • Ngăn chặn form gửi đi khi nhấp vào nút submit (để thực hiện validation bằng JS trước).
  • Ngăn chặn link chuyển hướng trang khi nhấp vào thẻ <a>.
  • Ngăn chặn trình duyệt mở menu ngữ cảnh khi nhấp chuột phải.
<a href="https://google.com" id="myLink">Đi đến Google</a>
<form id="myForm">
  <input type="text">
  <button type="submit">Gửi form</button>
</form>

<script>
  const myLink = document.getElementById('myLink');
  const myForm = document.getElementById('myForm');

  // Ngăn link chuyển trang
  myLink.addEventListener('click', function(event) {
    alert('Chuyển hướng đã bị chặn!');
    event.preventDefault(); // Ngăn hành động mặc định của thẻ <a>
  });

  // Ngăn form gửi đi (cho phép validation trước)
  myForm.addEventListener('submit', function(event) {
    alert('Form submission bị chặn tạm thời!');
    event.preventDefault(); // Ngăn hành động mặc định của form (gửi dữ liệu và tải lại trang)

    // Tại đây, bạn có thể thêm logic validation hoặc xử lý dữ liệu bằng AJAX/Fetch
  });

  // Ngăn context menu (nhấp chuột phải)
  document.addEventListener('contextmenu', function(event) {
    alert('Context menu bị chặn!');
    event.preventDefault(); // Ngăn hành động mặc định mở menu ngữ cảnh
  });

</script>
  • Giải thích: Bằng cách gọi event.preventDefault(), chúng ta thông báo cho trình duyệt bỏ qua hành động mặc định của phần tử cho sự kiện đó.
2. event.stopPropagation()

Phương thức này ngăn chặn sự kiện lan truyền thêm trong cả hai giai đoạn (Capturing và Bubbling). Nó dừng sự kiện tại phần tử hiện tại và không cho nó đi tiếp lên (hoặc xuống) cây DOM.

Ví dụ: Nếu bạn nhấp vào một nút bên trong một div và div đó cũng có listener cho sự kiện click, bạn có thể dùng stopPropagation() trên nút để ngăn listener của div bị kích hoạt.

<div id="outerDiv">
  <button id="myStopBtn">Click và Dừng lan truyền</button>
</div>

<script>
  const outerDiv = document.getElementById('outerDiv');
  const myStopBtn = document.getElementById('myStopBtn');

  outerDiv.addEventListener('click', function() {
    console.log('Click trên Outer Div');
  });

  myStopBtn.addEventListener('click', function(event) {
    console.log('Click trên Button');
    event.stopPropagation(); // Dừng sự kiện lại tại đây
  });
</script>
  • Giải thích: Khi nhấp vào nút, listener trên nút được kích hoạt, in "Click trên Button". Sau đó, event.stopPropagation() được gọi. Điều này ngăn sự kiện nổi bọt lên phần tử cha (outerDiv). Kết quả là listener trên outerDiv không bị kích hoạt. Nếu bạn bỏ dòng event.stopPropagation();, cả hai thông báo sẽ được in ra console (theo thứ tự Bubbling).

Lưu ý: event.stopPropagation() chỉ dừng sự lan truyền của sự kiện hiện tại. Nó không ngăn các event listener khác trên cùng một phần tử cho cùng một sự kiện được chạy. Nó cũng không ngăn các hành động mặc định của trình duyệt (ví dụ: click vào checkbox vẫn check/uncheck, trừ khi bạn dùng preventDefault).

Comments

There are no comments at the moment.