Câu 1Nếu gõ 3 ký tự vào ô văn bản rồi nhấn Tab để chuyển ô, mỗi sự kiện input và change xảy ra bao nhiêu lần?
Form — giá trị nhập vào và submit
Đọc ô nhập bằng value và checked, xử lý input và change, hủy submit bằng preventDefault và gom các ô bằng FormData.
Bạn đã biết cách nhận click bằng addEventListener, nhưng với form, bạn còn phải đọc giá trị người dùng gõ vào các ô và kiểm tra trước khi gửi. Khi nút gửi được nhấn, trình duyệt chuyển sang trang khác, nên bạn cũng cần cách giữ cho form không bị gửi đi trước khi kiểm tra xong.
Bài này nói về value, property để đọc giá trị trong ô nhập, và sự kiện submit xảy ra khi form được gửi.
Đọc giá trị đã nhập — value và checked
Giả sử bạn muốn đọc số lượng nhập vào ô số lượng của một form đặt hàng (phần gom các ô nhập và nút gửi trong phần tử form) để tính tổng tiền. Phần tử input không có thẻ đóng và không chứa được phần tử con hay chữ bên trong, nên đọc textContent của nó chỉ nhận về chuỗi rỗng.
Với ô nhập (phần tử input, có thuộc tính type quyết định loại ô, hoặc phần tử textarea cho văn bản nhiều dòng), bạn đọc value (property đọc và ghi giá trị hiện có trong ô dưới dạng chuỗi) như quantity.value, còn để biết ô có được đánh dấu hay không thì đọc checked. Thuộc tính value viết trong HTML là giá trị ban đầu khi trang mở, và nó không thay đổi khi người dùng nhập.
document.body.innerHTML = `<form id="order-form"><input id="quantity" type="number" value="1"><input id="gift-wrap" type="checkbox"></form>`;
const quantity = document.querySelector("#quantity");
const giftWrap = document.querySelector("#gift-wrap");
// Giả lập việc người dùng nhập 3 và đánh dấu ô gói quà
quantity.value = "3";
giftWrap.checked = true;
// Kể cả ở ô số, value vẫn là chuỗi, nên hãy đổi sang số trước khi tính
console.log(quantity.value + 1); // 31
console.log(Number(quantity.value) * 1200); // 3600
console.log(giftWrap.checked); // true
// outerHTML vẫn hiện thuộc tính value đã viết trong HTML
console.log(quantity.outerHTML); // <input id="quantity" type="number" value="1">
checked cũng vậy: đánh dấu ô không làm thuộc tính checked xuất hiện trong HTML. Nếu xóa trống ô số lượng, value thành chuỗi rỗng, mà Number("") trả về 0, nên sau khi đã đổi sang số, bạn không phân biệt được ô bỏ trống với ô nhập 0.
Đếm lại sau mỗi lần gõ — sự kiện input và change
Trên màn hình viết bài của blog nội bộ, bạn muốn hiện số ký tự như "12 / 40" bên dưới ô tiêu đề và cập nhật mỗi lần người dùng gõ. Nếu chỉ đọc value một lần khi trang mở rồi hiển thị, các ký tự gõ sau đó sẽ không được tính vào con số.
Ô nhập phát ra sự kiện input mỗi lần một ký tự được gõ, và sự kiện change khi giá trị đã thay đổi và người dùng rời khỏi ô. Trong console không gõ phím được, nên bạn phát ra sự kiện cùng tên bằng dispatchEvent (phương thức phát ra sự kiện được truyền vào ngay trên phần tử đó). Bạn tạo sự kiện cần truyền bằng cách chỉ định tên, như new Event("input").
document.body.innerHTML = `<form id="post-form"><input id="post-title"><p id="title-count">0 / 40</p></form>`;
const postTitle = document.querySelector("#post-title");
const titleCount = document.querySelector("#title-count");
// input: đếm lại số ký tự sau mỗi lần gõ
postTitle.addEventListener("input", () => {
titleCount.textContent = `${postTitle.value.length} / 40`;
});
// change: lưu bản nháp khi giá trị đã đổi và người dùng rời khỏi ô
postTitle.addEventListener("change", () => console.log("Đã lưu bản nháp"));
// Chỉ gán cho value
postTitle.value = "Báo cáo tuần";
console.log(titleCount.textContent); // 0 / 40
// Khi phát ra sự kiện, các listener đã đăng ký cho sự kiện cùng tên sẽ được gọi
postTitle.dispatchEvent(new Event("input"));
console.log(titleCount.textContent); // 12 / 40
postTitle.dispatchEvent(new Event("change")); // Đã lưu bản nháp
Listener của input được gọi sau từng ký tự. Với những việc không cần làm sau mỗi lần gõ, như lưu bản nháp, hãy đăng ký listener cho change, sự kiện xảy ra khi người dùng nhập xong. Bảng dưới đây cho biết mỗi thao tác của người dùng phát ra sự kiện nào trong hai sự kiện input và change.
| Thao tác | Sự kiện input | Sự kiện change |
|---|---|---|
| Gõ 1 ký tự vào ô văn bản | Xảy ra sau mỗi ký tự | Không xảy ra |
| Rời ô (click ra ngoài hoặc Tab) | Không xảy ra | Xảy ra nếu giá trị đổi |
| Nhấn checkbox để bật/tắt | Xảy ra | Xảy ra |
Hủy việc gửi form — submit và preventDefault
Với form đăng ký nhận bản tin, bạn muốn hiện thông báo lỗi và không cho gửi nếu địa chỉ email không có @. Khi nút gửi được nhấn, trình duyệt gửi nội dung form đi và chuyển sang trang khác, nên dù listener có đưa thông báo lỗi lên trang, thông báo đó cũng biến mất cùng với trang.
Ngay trước khi form được gửi, sự kiện submit xảy ra trên phần tử form. Nếu gọi preventDefault (phương thức hủy hành động mà trình duyệt sẽ làm tiếp sau sự kiện) trong listener đó, form sẽ không được gửi. Để biết việc hủy có thành công hay không, hãy đọc defaultPrevented của sự kiện (property có giá trị true nếu đã hủy được).
document.body.innerHTML = `<form id="newsletter-form"><input id="newsletter-email"><button>Đăng ký</button><p id="email-error"></p></form>`;
const newsletterForm = document.querySelector("#newsletter-form");
const emailInput = document.querySelector("#newsletter-email");
const emailError = document.querySelector("#email-error");
// Được gọi ngay trước khi gửi. Chỉ hủy việc gửi khi không có @
newsletterForm.addEventListener("submit", (event) => {
if (!emailInput.value.includes("@")) {
event.preventDefault();
emailError.textContent = "Địa chỉ email thiếu @";
}
});
// Thay cho việc gửi thật, tạo và phát ra một sự kiện submit có thể hủy
emailInput.value = "minh.example.com";
const submitEvent = new Event("submit", { cancelable: true });
newsletterForm.dispatchEvent(submitEvent);
console.log(submitEvent.defaultPrevented); // true (preventDefault đã được gọi)
console.log(emailError.textContent); // Địa chỉ email thiếu @
Cột trái và cột phải có cùng giá trị nhập; khác biệt duy nhất là có gọi preventDefault() hay không. Sự kiện submit xảy ra trên phần tử form chứ không phải trên nút gửi, nên hãy đăng ký listener cho form. Khi người dùng nhấn Enter trong một ô để gửi, cùng listener đó cũng nhận được sự kiện.
click() trên nút gửi sẽ gửi form thật
Phần tử button nằm trong form sẽ thành nút gửi nếu bạn không ghi type. Gọi click() trên nút đó mà không hủy thì form được gửi thật, và bạn chỉ thấy kết quả sau khi trang tải lại. Sự kiện submit phát ra bằng dispatchEvent không gửi form đi, nhưng nếu thiếu { cancelable: true } thì preventDefault() không có tác dụng.
Gom giá trị của các ô nhập cùng lúc — FormData
Giả sử với form nhập địa chỉ giao hàng, bạn muốn gom tên người nhận, mã bưu chính và lựa chọn có để hàng trước cửa hay không vào một object. Nếu viết querySelector và value cho từng ô, mỗi lần thêm ô là bạn phải viết thêm dòng để đọc nó. Khi form được gửi, thuộc tính name của mỗi ô (tên gắn cho giá trị) được gửi đi thành cặp với giá trị của ô.
Bạn tạo FormData (object gom các cặp tên và giá trị từ các ô trong form, lấy tên từ thuộc tính name) bằng new FormData(form), rồi đọc giá trị theo tên bằng get("zip"). Nếu truyền nó cho Object.fromEntries (phương thức làm ngược lại với Object.entries: tạo object từ danh sách các cặp [tên, giá trị]), bạn có một object chứa tất cả các ô.
document.body.innerHTML = `<form id="shipping-form">
<input id="address-search" value="Quận 1">
<input name="recipient" value="Trần Linh">
<input name="zip" value="700000">
<input name="dropOff" type="checkbox">
<input name="notify" type="checkbox" checked>
</form>`;
const shippingForm = document.querySelector("#shipping-form");
// Gom các cặp tên và giá trị từ các ô trong form
const shippingData = new FormData(shippingForm);
console.log(shippingData.get("zip")); // 700000
console.log(shippingData.get("dropOff")); // null
// Tạo object từ các cặp tên và giá trị
const shipping = Object.fromEntries(shippingData);
console.log(JSON.stringify(shipping));
// {"recipient":"Trần Linh","zip":"700000","notify":"on"}
| Ô nhập | Điều kiện | Giá trị trong FormData |
|---|---|---|
| Ô tìm địa chỉ | Không có name | Không có |
| recipient và zip | Có name, ô văn bản | Trần Linh và 700000 |
| dropOff | Có name, không đánh dấu | Không có, get trả về null |
| notify | Có name, có đánh dấu | on (khi không ghi value) |
Giống Map, FormData cho phép lấy lần lượt từng cặp [tên, giá trị], nên bạn truyền trực tiếp được cho Object.fromEntries. Chỉ cần gắn thuộc tính name cho ô mới, bạn không phải viết thêm dòng nào để đọc nó.
In thẳng FormData ra sẽ thấy {}
Console của bài tập đổi object thành chuỗi JSON trước khi in, nên nếu truyền thẳng FormData, như console.log(shippingData), bạn sẽ thấy {} dù giá trị các ô đã được gom đủ. Hãy kiểm tra nội dung bằng get, hoặc chuyển thành object bằng Object.fromEntries trước khi in.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Gọi event.preventDefault() trong listener submit của form thì điều gì bị hủy?
Câu 3Với checkbox name="dropOff" không được đánh dấu, get("dropOff") của FormData trả về gì?