Data sudah bisa dibaca, divalidasi, dan lolos gerbang validasi bawaan. Tinggal satu tahap: mengirim. Perilaku bawaan form adalah berpindah halaman — seluruh isian dikirim, halaman dibongkar, lalu jawaban server digambar sebagai halaman baru. Untuk aplikasi interaktif, cara ini terasa kasar: keranjang belanja Siti tak boleh hilang hanya karena satu pesanan dikirim.
Chapter ini menyusun handler submit yang lengkap: hentikan perpindahan halaman dengan event.preventDefault(), kumpulkan data, jalankan validasi terakhir, lalu kirim secara asinkron di belakang layar. Karena pengiriman sungguhan lewat fetch baru kita pelajari pada section Browser APIs, pengirim di contoh berupa fungsi tiruan berbasis Promise — bentuk dan caranya menunggu hasil sama persis dengan yang akan kita pakai nanti.
Alur Lengkap Sebuah Handler Submit #
Empat pekerjaan harus ada di setiap handler pengiriman yang rapi:
event.preventDefault()di baris pertama — halaman tetap tinggal.- Kumpulkan dan periksa data sekali lagi di dalam handler, sebagai lapisan pemeriksaan terakhir.
- Matikan tombol kirim selama pengiriman berjalan, agar pengguna yang sabar menekan tombol tidak menciptakan dua pesanan.
- Setelah jawaban tiba, tampilkan hasilnya, aktifkan kembali tombol, dan bersihkan form.
Daftar itu terlihat sepele, tetapi setiap langkah menjawab kegagalan nyata: langkah pertama mencegah isian terhapus bersama halaman lama; langkah kedua menahan data yang lolos dari celah (misalnya dikirim lewat jalur yang tidak melewati validasi bawaan); langkah ketiga mencegah pengiriman ganda yang menggandakan pesanan; langkah keempat memastikan form siap dipakai lagi tanpa perlu memuat ulang.
Skenario: form pemesanan kafe milik Budi — pilihan menu, jumlah porsi, satu tombol kirim, dan satu paragraf status yang menyapa pengguna.
<form id="form-pesan">
<label for="menu">Menu</label>
<select id="menu" name="menu">
<option value="kopi-gayo">Kopi Gayo</option>
<option value="teh-melati">Teh Melati</option>
</select>
<label for="porsi">Porsi</label>
<input id="porsi" name="porsi" type="number" min="1" max="20" value="2">
<button type="submit" id="tombol-kirim">Pesan Sekarang</button>
<p id="status" role="status"></p>
</form>Fungsi Kirim Tiruan #
Pengirim tiruan kita meniru bentuk fetch yang sesungguhnya: diberi data, mengembalikan Promise, dan jawaban datang belakangan — bukan seketika. Di sini penundaan dibuat dengan setTimeout 200 milidetik, cukup untuk melihat urutan kejadian yang sama dengan jaringan nyata. Jawabannya sengaja berupa object kecil dengan nomor pesanan tetap, sehingga keluaran contoh selalu bisa direproduksi; server sungguhan nantinya mengganti isi object itu tanpa mengubah bentuk kontraknya. Perhatikan juga bahwa data diterima sebagai parameter meskipun fungsi tiruan tidak memakainya — tanda tangan yang sama dengan fungsi kirim sungguhan membuat penggantian nanti tidak menyentuh handler sama sekali.
// kirim-pesanan.js — menangani submit tanpa memuat ulang halaman
const form = document.querySelector("#form-pesan");
const status = form.querySelector("#status");
const tombol = form.querySelector("#tombol-kirim");
// fungsi tiruan pengganti fetch: selesai setelah 200 ms dengan jawaban tetap
function kirimPesanan(data) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ status: "terkirim", nomor: "ORD-1042" });
}, 200);
});
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
const porsi = Number(data.porsi);
if (!Number.isInteger(porsi) || porsi < 1 || porsi > 20) {
status.textContent = "Porsi harus angka 1 sampai 20.";
console.log("Gagal :", status.textContent);
return;
}
tombol.disabled = true;
status.textContent = "Mengirim pesanan...";
console.log("Status :", status.textContent);
const hasil = await kirimPesanan(data);
status.textContent = "Pesanan " + hasil.nomor + " terkirim.";
tombol.disabled = false;
form.reset();
console.log("Status :", status.textContent);
});Tiga kejadian kita jalankan berurutan: submit normal dengan porsi 2, percobaan mengirim dengan porsi 50 yang diselipkan lewat kode (event submit dikirim manual sehingga gerbang validasi bawaan tidak berjalan — sebagaimana dibahas pada chapter sebelumnya), lalu submit normal lagi dengan porsi 5.
Output yang Dihasilkan (console browser):
Status : Mengirim pesanan...
Status : Pesanan ORD-1042 terkirim.
Gagal : Porsi harus angka 1 sampai 20.
Status : Mengirim pesanan...
Status : Pesanan ORD-1042 terkirim.Mari kita bedah bagian pentingnya:
- Baris 7–14 (fungsi tiruan):
kirimPesanan(data)mengembalikanPromiseyang resolve dengan object jawaban setelah 200 ms. Handler yang memanggilnya tidak menunggu di tempat; baris setelah pemanggilan belum boleh menganggap pesanan sudah terkirim. - Baris 16–19 (
preventDefaultdan pengumpulan data): halaman dicegah berpindah, laluFormDatamengumpulkan seluruh kontrol danObject.fromEntriesmerapikannya menjadi object — kombinasi dari chapter Membaca Form Data. - Baris 20–24 (lapisan pemeriksaan terakhir):
data.porsidatang sebagai string"2", sehinggaNumber()mengubahnya dulu. Skenario kedua pada keluaran memperlihatkan lapisan ini bekerja: porsi50yang lewat tanpa validasi bawaan tetap tertahan di handler. - Baris 26–28 (umpan balik sebelum menunggu): tombol dimatikan dengan
disableddan teks status diubah — pengguna langsung tahu prosesnya berjalan. BarisStatus : Mengirim pesanan...tercatat sebelum jawaban datang. - Baris 30–35 (menunggu jawaban):
awaitmenghentikan eksekusi handler sampaiPromiseresolve. Setelah itu status diperbarui dengan nomor pesanan, tombol diaktifkan kembali, danform.reset()mengembalikan semua kontrol ke nilai awalnya — menu kembali ke pilihan pertama, porsi kembali ke2.
Perhatikan urutan keluaran: pesan “Mengirim pesanan…” selalu muncul lebih dulu, dan baris “terkirim” baru muncul setelah jeda. Inilah wajah asinkron pada form — pekerjaan pengiriman tidak menahan halaman, tetapi handler kita harus selalu siap menerima jawabannya belakangan.
Dua detail pada contoh berhak digarisbawahi. form.reset() mengembalikan seluruh kontrol ke nilai bawaan atributnya: porsi kembali ke 2 karena atribut value="2" di HTML, dan menu kembali ke opsi pertama. Reset bukan penghapus isi — dia mengembalikan ke titik awal yang tertulis di markup. Dan pemanggilan tombol.disabled = false terjadi di kedua cabang keluar handler: baik sukses maupun gagal, tombol tidak boleh tertinggal dalam keadaan mati.
Paragraf Status dan Pembaca Layar #
Paragraf status di form kita membawa atribut role="status" — persiapan kecil menuju chapter Accessible Form Feedback: pengumuman yang berubah sendiri perlu ditandai agar teknologi bantu menyadarinya. Untuk saat ini cukup dicatat dua kebiasaan yang sudah kita pegang: teks status selalu diisi dengan textContent sehingga apa yang ditulis sama dengan apa yang tampil, dan isinya mengikuti urutan kejadian — apa yang sedang berjalan lebih dulu, hasilnya kemudian. Pengguna yang melihat layar dan yang mengandalkan pembaca layar menerima cerita yang sama.
Menangani Jalur Gagal #
Jaringan bisa menolak: server sibuk, koneksi terputus, atau permintaan ditolak. Fungsi pengirim yang jujur memanggil reject pada kondisi itu, dan handler menangkapnya dengan try-catch sekitar await — pola yang sudah kita pelajari pada section Asynchronous JavaScript. Blok finally memastikan tombol kembali aktif apa pun yang terjadi, sehingga kegagalan tidak membuat form terkunci.
// kirim-pesanan-gagal.js — jalur gagal: server menolak, tangkap dengan try-catch
const form = document.querySelector("#form-pesan");
const status = form.querySelector("#status");
const tombol = form.querySelector("#tombol-kirim");
// fungsi tiruan yang kali ini selalu menolak
function kirimKeServer(data) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
reject(new Error("Server sibuk, coba lagi."));
}, 150);
});
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
tombol.disabled = true;
status.textContent = "Mengirim pesanan...";
try {
const hasil = await kirimKeServer(data);
status.textContent = "Pesanan " + hasil.nomor + " terkirim.";
} catch (galat) {
status.textContent = "Pengiriman gagal: " + galat.message;
} finally {
tombol.disabled = false;
console.log("Status :", status.textContent);
}
});Output yang Dihasilkan (console browser):
Status : Pengiriman gagal: Server sibuk, coba lagi.Struktur handler tidak berubah jauh dari versi sukses — bedanya hanya pemisahan dua cabang: catch menyusun pesan kegagalan dari galat.message, dan finally menjadi tempat pemulihan (tombol aktif lagi). Pesan kegagalan ditulis dengan kata-kata yang bisa ditindaklanjuti, bukan kode galat mentah.
Pengiriman tanpa berpindah halaman kini lengkap dari hulu ke hilir. Satu kemampuan pendukung masih perlu diperjelas, karena selama ini kita memanggilnya tanpa membahas detail: event.preventDefault(), mesin yang menghentikan perilaku bawaan browser — dan bedanya dengan penghenti perambatan event.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
kirim-pesanan-gagal.js
Unduh berkas// kirim-pesanan-gagal.js — jalur gagal: server menolak, tangkap dengan try-catch
const form = document.querySelector("#form-pesan");
const status = form.querySelector("#status");
const tombol = form.querySelector("#tombol-kirim");
// fungsi tiruan yang kali ini selalu menolak
function kirimKeServer(data) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
reject(new Error("Server sibuk, coba lagi."));
}, 150);
});
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
tombol.disabled = true;
status.textContent = "Mengirim pesanan...";
try {
const hasil = await kirimKeServer(data);
status.textContent = "Pesanan " + hasil.nomor + " terkirim.";
} catch (galat) {
status.textContent = "Pengiriman gagal: " + galat.message;
} finally {
tombol.disabled = false;
console.log("Status :", status.textContent);
}
});
kirim-pesanan.js
Unduh berkas// kirim-pesanan.js — menangani submit tanpa memuat ulang halaman
const form = document.querySelector("#form-pesan");
const status = form.querySelector("#status");
const tombol = form.querySelector("#tombol-kirim");
// fungsi tiruan pengganti fetch: selesai setelah 200 ms dengan jawaban tetap
function kirimPesanan(data) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ status: "terkirim", nomor: "ORD-1042" });
}, 200);
});
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
const porsi = Number(data.porsi);
if (!Number.isInteger(porsi) || porsi < 1 || porsi > 20) {
status.textContent = "Porsi harus angka 1 sampai 20.";
console.log("Gagal :", status.textContent);
return;
}
tombol.disabled = true;
status.textContent = "Mengirim pesanan...";
console.log("Status :", status.textContent);
const hasil = await kirimPesanan(data);
status.textContent = "Pesanan " + hasil.nomor + " terkirim.";
tombol.disabled = false;
form.reset();
console.log("Status :", status.textContent);
});