Học bằng cách đọc theo thứ tự

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">
Hai giá trị value của input quantity
Thuộc tính valuevalue="1"viết trong HTMLNhập vào rồivẫn là 1outerHTMLhiện value="1"Property valueGiá trị đang nhập"3"Đổi theo mỗilần gõ phímĐọc bằngquantity.value
"3" vừa gán chỉ nằm trong property value; thuộc tính value vẫn là 1. Giá trị người dùng nhập được đọc từ property value.

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.

Trong form đặt thuê ô tô, bạn tính tiền thuê từ số ngày và việc có cần ghế trẻ em hay không. Ô số ngày rentalDays (đã được gán giá trị) và checkbox childSeat đã được khai báo sẵn.

① Hãy in ra kiểu của giá trị trong ô số ngày.

② Hãy đổi số ngày sang số và in ra tiền thuê với giá 6000 yên mỗi ngày.

③ Nếu ô được đánh dấu, hãy cộng thêm 500 yên mỗi ngày và in ra tổng tiền cho cả số ngày thuê.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đế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
Chỉ gán thì số ký tự không đổi
postTitle.value= "Báo cáo tuần"Không có sự kiệninput nàoListener khôngđược gọiVẫn là0 / 40dispatchEvent(…)sau khi gánSự kiện inputđược phát raListener đọcvalue.lengthCập nhật thành12 / 40
Gán cho value không phát ra sự kiện input, nên vẫn là 0 / 40. Listener chỉ được gọi khi bạn phát ra sự kiện bằng dispatchEvent.

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ácSự kiện inputSự kiện change
Gõ 1 ký tự vào ô văn bảnXả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 raXảy ra nếu giá trị đổi
Nhấn checkbox để bật/tắtXảy raXảy ra

Trên màn hình thanh toán, bạn chỉ hiện các ô địa chỉ khi ô "Giao đến địa chỉ khác" được đánh dấu. Checkbox otherAddress và vùng ô địa chỉ addressFields đã được khai báo sẵn.

① Hãy đăng ký listener bỏ class hidden khỏi vùng ô địa chỉ khi checkbox được đánh dấu.

② Trước khi tạo click, hãy in ra HTML của vùng ô địa chỉ.

③ Hãy tạo click trên checkbox, rồi in ra trạng thái đánh dấu và HTML của vùng ô địa chỉ.

JavaScript / TypeScript Editor

Chạy code để xem đầu 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 @
Kết quả tùy vào có @ hay không và có gọi preventDefault() hay không
Listener củasubmit được gọiKhông có @:minh.example.comCó @:minh@example.comKhông có @:minh.example.compreventDefault()và thông báo lỗiKhông gọipreventDefault()Quên viếtpreventDefault()Không gửi,lỗi vẫn hiệnTrình duyệtgửi formForm được gửi,lỗi cũng mất
Gọi preventDefault() khi không có @ thì thông báo lỗi được giữ lại trên trang. Nếu quên gọi, trang bị chuyển đi và thông báo lỗi cũng mất theo.

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.

Bạn kiểm tra độ dài của mật khẩu mới. passwordForm, newPasswordpasswordError đã được khai báo sẵn.

① Hãy đăng ký listener hủy việc gửi và ghi thông báo lỗi nếu mật khẩu ngắn hơn 8 ký tự, còn không thì xóa trống thông báo.

② Hãy gán "cat2026", phát ra một sự kiện submit có thể hủy, rồi in ra xem việc gửi có bị hủy hay không.

③ Hãy gán "cat2026!", tạo lại sự kiện rồi phát ra, sau đó in ra xem việc gửi có bị hủy hay không, và in HTML của ô lỗi.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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 querySelectorvalue 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ệnGiá trị trong FormData
Ô tìm địa chỉKhông có nameKhông có
recipient và zipCó name, ô văn bảnTrần Linh và 700000
dropOffCó name, không đánh dấuKhông có, get trả về null
notifyCó name, có đánh dấuon (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.

Trong form ứng tuyển của một trang tuyển dụng, bạn kiểm tra nội dung đã nhập trước khi gửi. Form applyForm đã được khai báo sẵn.

① Trong listener của submit, hãy hủy việc gửi, gom giá trị các ô thành một object và in ra dưới dạng chuỗi JSON.

② Trong cùng listener, hãy đọc giá trị của ô đồng ý nhận email, và in ra "Chưa đồng ý" nếu không có giá trị.

③ Hãy phát ra một sự kiện submit có thể hủy.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

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?

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ì?