Form pendaftaran lokakarya milik Siti punya tiga kebutuhan yang terasa sepele di permukaan: penghitung karakter yang ikut bertambah setiap huruf diketik, pemeriksaan kelengkapan yang berjalan ketika pengguna selesai mengisi satu kolom dan pindah ke kolom berikutnya, serta pengiriman data yang tidak memuat ulang halaman. Tiga kebutuhan itu ternyata dikerjakan tiga peristiwa yang berbeda — dan salah memilih peristiwa adalah kesalahan paling umum saat menangani form.

Ketiganya adalah input, change, dan submit. Perbedaan utamanya terletak pada kapan masing-masing terpicu, dan urutannya pun punya pola yang bisa kita buktikan dengan uji nyata. Chapter ini membedah ketiganya pada satu form yang sama, lalu menyimpulkan urutan kejadiannya dari keluaran yang kita catat sendiri.

input: Setiap Perubahan Nilai #

Peristiwa input terpicu setiap kali nilai elemen form berubah — bahkan sekecil satu huruf. Mengetik huruf demi huruf memicunya berulang kali, begitu juga menempel teks (paste) dan memotong (cut). Karena ketepatan waktunya, input adalah pilihan alami untuk reaksi yang harus terasa langsung: penghitung karakter, pratinjau langsung, atau pencarian yang difilter saat mengetik.

Listener input dipasang pada elemen form itu sendiri — kolom <input>, <textarea>, atau <select> — bukan pada form pembungkusnya. Isi terbaru selalu terbaca lewat event.target.value, sehingga handler tidak perlu menyimpan keadaan antara.

Jenis kolom menentukan bentuk isinya: kolom teks menghasilkan string, kotak centang membawa status checked berupa boolean, dan kolom berkas berisi daftar berkas yang dipilih. Satu pola listener input bisa melayani semuanya, asalkan handler membaca properti yang sesuai.

Satu kehati-hatian untuk pekerjaan berat: karena input menyala untuk setiap huruf, handler yang mahal — seperti pencarian yang menembus server — sebaiknya ditunda sampai pengguna berhenti mengetik sesaat. Teknik penundaan ini bernama debouncing dan dibahas pada section Performance; untuk penghitung karakter, reaksi langsung tanpa penundaan sudah tepat.

change: Nilai yang Dikomit #

Peristiwa change menunggu momen yang lebih formal: nilai dianggap “dikomit”. Untuk kolom teks, komit terjadi ketika pengguna selesai mengedit — lazimnya saat fokus meninggalkan kolom (blur) atau menekan Enter. Untuk elemen yang tidak butuh langkah komit — kotak centang, radio, dan dropdown <select> — change terpicu langsung pada saat pilihan diganti.

Karena itu, change cocok untuk reaksi yang hanya perlu berjalan sekali per pengisian: validasi sebuah kolom, penghitungan ulang total, atau pencatatan bahwa “kolom ini sudah diisi”. Menggunakan change untuk penghitung karakter justru keliru — penghitung akan tertinggal beberapa huruf sampai pengguna pindah kolom.

Bila kolom teks tidak pernah diubah, change juga tidak akan menyala meski fokus datang dan pergi — tidak ada perubahan, tidak ada komit.

submit: Form Dikirim #

Peristiwa submit terpicu ketika form dikirim — karena tombol submit ditekan, atau karena pengguna menekan Enter di dalam kolom teks. Dua detail penting: submit terpicu pada elemen <form>, bukan pada tombolnya, dan begitu peristiwa itu selesai tanpa pembatalan, browser menjalankan perilaku bawaannya — mengirim data dan memuat ulang halaman. Untuk aplikasi yang mengelola pengiriman sendiri (misalnya lewat fetch pada section jaringan), perilaku bawaan itu dihentikan dengan event.preventDefault() di awal handler.

Sangat penting pula membedakan type="submit" dan type="button". Tombol pertama adalah bagian dari mekanisme pengiriman form; tombol kedua tidak mengirim apa pun dan hanya menghasilkan click. Tombol bantu seperti “Tambah Baris” atau “Reset” sebaiknya type="button" agar tidak secara tak sengaja memicu pengiriman form.

Pemeriksaan Bawaan Sebelum submit #

Peristiwa submit punya satu pintu yang jarang disadari: browser memeriksa aturan validasi HTML terlebih dahulu. Atribut seperti required, type="email", min, dan max diperiksa otomatis saat form dikirim — bila ada yang belum lolos, browser menampilkan pesan bawaannya dan peristiwa submit tidak dikirim sama sekali. Handler kita hanya berjalan untuk data yang sudah lolos pemeriksaan bentuk paling dasar.

Bila pemeriksaan bawaan kurang pas untuk kebutuhan tertentu, atribut novalidate pada form mematikannya, dan validasi penuh berpindah ke tangan JavaScript. Untuk form umum, menggabungkan keduanya adalah praktik yang hemat: biarkan HTML menjaga bentuk dasar, lalu gunakan handler submit untuk aturan yang lebih halus — kecocokan antar kolom, batas usia, atau format khusus.

Urutan Kejadian yang Teruji #

Berikut form pendaftaran versi kecil yang memuat ketiga peristiwa sekaligus:

html
<form id="form-daftar">
  <input id="nama" name="nama" type="text">
  <button type="submit">Daftar</button>
</form>
// form-daftar.js — input, change, dan submit beserta urutannya
const form = document.querySelector("#form-daftar");
const nama = document.querySelector("#nama");

// input: setiap perubahan nilai, sekecil apa pun
nama.addEventListener("input", function (event) {
  console.log(`input  -> "${event.target.value}"`);
});

// change: ketika nilai "dikomit" (fokus meninggalkan kolom)
nama.addEventListener("change", function (event) {
  console.log(`change -> "${event.target.value}"`);
});

// submit: form dikirim
form.addEventListener("submit", function (event) {
  event.preventDefault(); // cegah halaman memuat ulang
  console.log(`submit -> kirim data: ${nama.value}`);
});

Simulasi kita meniru pendaftar sungguhan: mengetik "B" lalu "Bu", pindah fokus (komit pertama), kembali mengetik hingga "Budi Santoso", komit kedua, lalu menekan tombol Daftar. Keluaran berikut dicatat dari eksekusi nyata berkas ini di lingkungan DOM jsdom — urutan enam barisnya persis seperti console browser.

Output yang Dihasilkan (console browser):

text
input  -> "B"
input  -> "Bu"
change -> "Bu"
input  -> "Budi Santoso"
change -> "Budi Santoso"
submit -> kirim data: Budi Santoso

Mari kita bedah:

  1. Baris 6–8 (listener input): dua baris pertama keluaran memperlihatkan input menyala untuk setiap tahap mengetik, dengan nilai terbaru di tiap baris.
  2. Baris 11–13 (listener change): baris change -> "Bu" muncul hanya setelah fokus meninggalkan kolom. Perhatikan nilai yang dikomit adalah "Bu" — isi pada saat pengguna berhenti mengetik, bukan isi terakhir.
  3. Baris 16–19 (listener submit pada form): preventDefault() berjalan lebih dulu sehingga halaman tidak memuat ulang, lalu data dibaca dari nama.value dan dicatat. Urutannya wajib: pembatalan sebelum kode lain, agar tidak ada peluang perilaku bawaan menyela.
  4. Pola keseluruhan: dalam satu sesi mengisi kolom, input datang berkali-kali, change datang sekali per komit, dan submit menutup sesi. Bila pengguna menekan Enter di kolom teks, peristiwa submit tetap terpicu pada form — tanpa menyentuh tombol sama sekali.

Keluaran di atas juga memperlihatkan manfaat membaca event.target.value alih-alih menyalin nilai saat pendaftaran: handler selalu membaca keadaan terbaru kolom, apa pun yang sudah terjadi sebelumnya. Prinsip yang sama berlaku pada pembacaan nama.value di handler submit — data yang dikirim adalah data terakhir yang ada di kolom, bukan potongan-potongan lama.

Dengan tiga peristiwa ini, hampir seluruh kebutuhan form terjawab: reaksi langsung lewat input, pemeriksaan per kolom lewat change, dan pengiriman terkendali lewat submit. Satu arah terakhir tersisa untuk melengkapi kotak peralatan event kita: peristiwa yang kita rancang sendiri.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

form-daftar.js

Unduh berkas
javascript
// form-daftar.js — input, change, dan submit beserta urutannya
const form = document.querySelector("#form-daftar");
const nama = document.querySelector("#nama");

// input: setiap perubahan nilai, sekecil apa pun
nama.addEventListener("input", function (event) {
  console.log(`input  -> "${event.target.value}"`);
});

// change: ketika nilai "dikomit" (fokus meninggalkan kolom)
nama.addEventListener("change", function (event) {
  console.log(`change -> "${event.target.value}"`);
});

// submit: form dikirim
form.addEventListener("submit", function (event) {
  event.preventDefault(); // cegah halaman memuat ulang
  console.log(`submit -> kirim data: ${nama.value}`);
});