Hôm nay cafedev chia sẻ cho ace về sự kiện trong trình duyệt. Một sự kiện là một tín hiệu cho thấy điều gì đó đã xảy ra. Tất cả các nút DOM đều tạo ra các tín hiệu như vậy (nhưng các sự kiện không giới hạn ở DOM).

Dưới đây là danh sách các sự kiện DOM hữu ích nhất, bạn chỉ cần xem qua:

Sự kiện chuột:

  • click – khi con chuột nhấp vào một phần tử (thiết bị màn hình cảm ứng tạo ra nó khi chạm vào).
  • contextmenu – khi chuột nhấp chuột phải vào một phần tử.
  • mouseover/ mouseout – khi con trỏ chuột đi qua / rời khỏi một phần tử.
  • mousedown/ mouseup- khi nút chuột được nhấn / thả trên một phần tử.
  • mousemove – khi con chuột được di chuyển.

Sự kiện bàn phím:

  • keydown và keyup- khi nhấn và nhả phím bàn phím.

Các sự kiện phần tử biểu mẫu:

  • submit- khi khách truy cập gửi a <form>.
  • focus- khi khách truy cập tập trung vào một phần tử, ví dụ: vào một <input>.

Tài liệu sự kiện:

  • DOMContentLoaded – khi HTML được tải và xử lý, DOM được xây dựng hoàn chỉnh.

Sự kiện CSS:

  • transitionend – khi CSS-animation kết thúc.

Có nhiều sự kiện khác. Chúng ta sẽ đi vào chi tiết hơn về các sự kiện cụ thể trong các chương tiếp theo.

1. Trình xử lý sự kiện

Để phản ứng trên các sự kiện, chúng ta có thể gán một trình xử lý – một hàm chạy trong trường hợp có sự kiện.

Trình xử lý là một cách để chạy mã JavaScript trong trường hợp có hành động của người dùng.

Có một số cách để chỉ định một trình xử lý. Hãy xem chúng, bắt đầu từ cái đơn giản nhất.

1.1. Thuộc tính HTML

Một trình xử lý có thể được đặt trong HTML với một thuộc tính có tên on<event>.

Ví dụ, để gán một trình xử lý click cho một input, chúng ta có thể sử dụng onclick, như sau:

<input value="Click me" onclick="alert('Click!')" type="button">

Khi nhấp chuột, code bên trong onclick chạy.

Xin lưu ý rằng bên trong onclick chúng ta sử dụng dấu ngoặc kép, vì bản thân thuộc tính này nằm trong dấu ngoặc kép. Nếu chúng ta quên rằng code nằm bên trong thuộc tính và sử dụng dấu ngoặc kép bên trong, như onclick=”alert(“Click!”)”sau:, thì nó sẽ không hoạt động đúng.

Thuộc tính HTML không phải là nơi thuận tiện để viết nhiều code, vì vậy tốt hơn chúng ta nên tạo một hàm JavaScript và gọi nó ở đó.

Đây là một cú nhấp chuột chạy hàm countRabbits():

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">

Như chúng ta đã biết, tên thuộc tính HTML là không trường hợp nhạy cảm, vì vậy các sự kiến ONCLICK cũng như onClick và onCLICK… Nhưng thông thường thuộc tính được lowercased: onclick.

1.2. Thuộc tính DOM

Chúng ta có thể chỉ định một trình xử lý bằng cách sử dụng thuộc tính DOM on<event>.

Ví dụ elem.onclick:

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

Nếu trình xử lý được chỉ định bằng cách sử dụng thuộc tính HTML thì trình duyệt sẽ đọc nó, tạo một hàm mới từ nội dung thuộc tính và ghi nó vào thuộc tính DOM.

Vì vậy, cách này thực sự giống với cách trước.

Hai đoạn code này hoạt động giống nhau:

Chỉ HTML:

<input type="button" onclick="alert('Click!')" value="Button">

HTML + JS:

<input type="button" id="button" value="Button">
<script>
  button.onclick = function() {
    alert('Click!');
  };
</script>

Trong ví dụ đầu tiên, thuộc tính HTML được sử dụng để khởi tạo button.onclick, trong khi ở ví dụ thứ hai – tập lệnh, đó là tất cả sự khác biệt.

Vì chỉ có một thuộc tính onclick nên chúng ta không thể chỉ định nhiều hơn một trình xử lý sự kiện.

Trong ví dụ dưới đây, việc thêm một trình xử lý bằng JavaScript sẽ ghi đè lên trình xử lý hiện có:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // overwrites the existing handler
    alert('After'); // only this will be shown
  };
</script>

Để loại bỏ một trình xử lý – gán elem.onclick = null.

2. Truy cập phần tử: this

Giá trị của this bên trong một trình xử lý là phần tử. 

Trong đoạn code dưới đây button hiển thị nội dung của nó bằng cách sử dụng this.innerHTML:

<button onclick="alert(this.innerHTML)">Click me</button>

3. Những sai lầm có thể xảy ra

Nếu bạn đang bắt đầu làm việc với các sự kiện – hãy lưu ý một số điều tinh tế.

Chúng ta có thể đặt một hàm hiện có làm trình xử lý:

function sayThanks() {
  alert('Thanks cafedev!');
}

elem.onclick = sayThanks;

Nhưng hãy cẩn thận: hàm nên được gán là sayThanks, không phải sayThanks().

// right
button.onclick = sayThanks;

// wrong
button.onclick = sayThanks();

Nếu chúng ta thêm dấu ngoặc đơn, thì hàm sayThanks() trở thành một lời gọi hàm. Vì vậy, dòng cuối cùng thực sự nhận kết quả của việc thực thi hàm, nghĩa là undefined(vì hàm không trả về gì) và gán nó cho onclick. Điều đó không hiệu quả.

… Mặt khác, trong phần đánh dấu, chúng ta cần các dấu ngoặc đơn:

<input type="button" id="button" onclick="sayThanks()">

Sự khác biệt rất dễ giải thích. Khi trình duyệt đọc thuộc tính, nó sẽ tạo ra một hàm xử lý với nội dung từ nội dung thuộc tính.

Vì vậy, đánh dấu tạo ra thuộc tính này:

button.onclick = function() {
  sayThanks(); // <-- the attribute content goes here
};

Không sử dụng setAttribute cho người xử lý.

Một cuộc gọi như vậy sẽ không hoạt động:

// a click on <body> will generate errors,
// because attributes are always strings, function becomes a string
document.body.setAttribute('onclick', function() { alert(1) });

Trường hợp thuộc tính DOM là vấn đề.

Gán một handler để elem.onclick, không elem.ONCLICK, bởi vì đặc tính DOM là trường hợp nhạy cảm.

4. addEventListener

Vấn đề cơ bản của các cách chỉ định trình xử lý đã nói ở trên – chúng ta không thể chỉ định nhiều trình xử lý cho một sự kiện.

Giả sử, một phần của code của chúng ta muốn đánh dấu một nút khi nhấp chuột và một phần khác muốn hiển thị thông báo trên cùng một lần nhấp.

Chúng ta muốn chỉ định hai trình xử lý sự kiện cho việc đó. Nhưng thuộc tính DOM mới sẽ ghi đè thuộc tính hiện có:

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // replaces the previous handler

Các developer chuẩn web đã hiểu điều đó từ lâu và đề xuất một cách thay thế để quản lý trình xử lý bằng cách sử dụng các phương pháp đặc biệt addEventListener và removeEventListener. Họ không có vấn đề như vậy.

Cú pháp để thêm một trình xử lý:

element.addEventListener(event, handler, [options]);

event

Tên sự kiện, ví dụ “click”.

handler

Hàm xử lý.

options

Một đối tượng tùy chọn bổ sung với các thuộc tính:

  • once: nếu true, thì trình nghe sẽ tự động bị xóa sau khi nó kích hoạt.
  • capture: giai đoạn xử lý sự kiện, sẽ được đề cập sau trong chương Sủi bọt và bắt giữ . Vì lý do lịch sử, options cũng có thể được false/true, đó là giống như {capture: false/true}.
  • passive: nếu true, thì trình xử lý sẽ không gọi preventDefault(), chúng tôi sẽ giải thích điều đó sau trong các hành động mặc định của Trình duyệt .

Để xóa trình xử lý, hãy sử dụng removeEventListener:

element.removeEventListener(event, handler, [options]);

Loại bỏ yêu cầu chức năng tương tự

Để loại bỏ một trình xử lý, chúng ta nên chuyển chính xác cùng một hàm như đã được gán.

Điều này không hoạt động:

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

Trình xử lý sẽ không bị xóa, vì removeEventListener nhận được một hàm khác – với cùng một mã, nhưng điều đó không quan trọng, vì nó là một đối tượng hàm khác.

Đây là cách đúng đắn:

function handler() {
  alert( 'Thanks!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

Xin lưu ý – nếu chúng ta không lưu trữ hàm trong một biến, thì chúng ta không thể xóa nó. Không có cách nào để “đọc lại” các trình xử lý được chỉ định addEventListener.

Nhiều cuộc gọi để addEventListener cho phép thêm nhiều trình xử lý, như sau:

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  };

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

Như chúng ta có thể thấy trong ví dụ trên, chúng ta có thể thiết lập xử lý cả hai sử dụng một DOM và addEventListener. Nhưng nói chung chúng ta chỉ sử dụng một trong những cách này.

Đối với một số sự kiện, trình xử lý chỉ hoạt động với addEventListener

Có những sự kiện không thể được chỉ định qua thuộc tính DOM. Chỉ với addEventListener.

Ví dụ: DOM ContentLoaded là sự kiện kích hoạt khi tài liệu được tải và DOM được tạo.

// will never run
document.onDOMContentLoaded = function() {
  alert("DOM built");
};
// this way it works
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});

Vì vậy, addEventListener là phổ biến hơn. Mặc dù, những sự kiện như vậy là một ngoại lệ chứ không phải là quy luật.

5. Đối tượng sự kiện

Để xử lý đúng một sự kiện, chúng ta muốn biết thêm về những gì đã xảy ra. Không chỉ là một “nhấp chuột” hoặc một “phím xuống”, mà là tọa độ con trỏ là gì? Phím nào đã được nhấn? Và như thế.

Khi một sự kiện xảy ra, trình duyệt tạo một đối tượng sự kiện(event), đưa các chi tiết vào nó và chuyển nó như một đối số cho trình xử lý.

Dưới đây là một ví dụ về việc lấy tọa độ con trỏ từ đối tượng sự kiện:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // show event type, element and coordinates of the click
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

Một số thuộc tính của đối tượng event:

event.type

Loại sự kiện, đây rồi “click”.

event.currentTarget

Phần tử đã xử lý sự kiện. Điều đó chính xác giống như this, trừ khi trình xử lý là một hàm mũi tên, hoặc nó this được liên kết với một cái gì đó khác, thì chúng ta có thể lấy phần tử từ đó event.currentTarget.

event.clientX / event.clientY

Tọa độ cửa sổ tương đối của con trỏ, cho các sự kiện con trỏ.

Có nhiều thuộc tính hơn. Nhiều người trong số họ phụ thuộc vào loại sự kiện: sự kiện bàn phím có một tập hợp thuộc tính, sự kiện con trỏ – một tập hợp khác, chúng ta sẽ nghiên cứu chúng sau khi chúng ta đến các sự kiện khác nhau chi tiết.

Đối tượng sự kiện cũng có sẵn trong trình xử lý HTML

Nếu chúng ta chỉ định một trình xử lý trong HTML, chúng ta cũng có thể sử dụng đối tượng event, như sau:

<input type="button" onclick="alert(event.type)" value="Event type">

Đó là có thể bởi vì khi trình duyệt đọc thuộc tính, nó tạo ra một handler như thế này: function(event) { alert(event.type) }. Đó là: đối số đầu tiên của nó được gọi “event”, và phần thân được lấy từ thuộc tính.

6. Trình xử lý đối tượng: handleEvent

Chúng ta có thể gán không chỉ một hàm mà còn một đối tượng như một trình xử lý sự kiện bằng cách sử dụng addEventListener. Khi một sự kiện xảy ra, phương thức  handleEvent của nó được gọi.

Ví dụ:

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

Như chúng ta có thể thấy, khi addEventListener nhận một đối tượng làm trình xử lý, nó sẽ gọi obj.handleEvent(event) trong trường hợp có sự kiện.

Chúng ta cũng có thể sử dụng một lớp cho điều đó:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Mouse button pressed";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

Ở đây cùng một đối tượng xử lý cả hai sự kiện. Xin lưu ý rằng chúng ta cần thiết lập rõ ràng các sự kiện để lắng nghe bằng cách sử dụng addEventListener. Đối tượng menu chỉ nhận được mousedown và mouseup ở đây, không phải bất kỳ loại sự kiện nào khác.

Phương thức handleEvent này không phải tự mình thực hiện tất cả các công việc. Thay vào đó, nó có thể gọi các phương thức dành riêng cho sự kiện khác, như sau:

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

Giờ đây, các trình xử lý sự kiện được tách biệt rõ ràng, có thể dễ hỗ trợ hơn.

7. Tóm lược

Có 3 cách để chỉ định trình xử lý sự kiện:

  1. Thuộc tính HTML: onclick=”…”.
  2. Thuộc tính DOM: elem.onclick = function.
  3. Phương pháp: elem.addEventListener(event, handler[, phase])thêm, removeEventListenerbớt.

Các thuộc tính HTML được sử dụng ít, vì JavaScript ở giữa thẻ HTML trông hơi kỳ cục và xa lạ. Cũng không thể viết nhiều mã trong đó.

Các thuộc tính DOM có thể sử dụng, nhưng chúng ta không thể chỉ định nhiều hơn một trình xử lý của sự kiện cụ thể. Trong nhiều trường hợp, sự hạn chế đó không phải là vấn đề cấp bách.

Cách cuối cùng là cách linh hoạt nhất, nhưng cũng là cách viết lâu nhất. Ví dụ transitionend, có một số sự kiện chỉ hoạt động với nó và DOMContentLoaded(sẽ được đề cập). Cũng hỗ trợ các đối tượng addEventListener như trình xử lý sự kiện. Trong trường hợp đó, phương thức handleEvent được gọi trong trường hợp của sự kiện.

Bất kể bạn gán trình xử lý như thế nào – nó sẽ lấy một đối tượng sự kiện làm đối số đầu tiên. Đối tượng đó chứa các chi tiết về những gì đã xảy ra.

Chúng ta sẽ tìm hiểu thêm về các sự kiện nói chung và về các loại sự kiện khác nhau trong các chương tiếp theo.

Nguồn và tài liệu tiếng anh tham khảo:

Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!