Pengiriman formulir HTML6 menit baca
Kirim formulir HTML tanpa reload halaman
Kirim formulir dengan fetch dan FormData agar pengunjung tetap di halaman, dengan error field, status loading, percobaan ulang, dan cadangan saat script gagal.
Dengarkan event submit formulir, panggil preventDefault(), dan kirim new FormData(form) dengan fetch dan header Accept: application/json. Ikiform lalu menjawab dengan JSON alih-alih redirect: 201 saat sukses, atau error field yang Anda tampilkan di samping setiap input. Pertahankan action dan method sungguhan di formulir agar tetap terkirim dengan cara biasa jika script Anda tidak pernah berjalan.
Contoh di sini adalah kotak Was this page helpful? di bagian bawah halaman dokumentasi. Ini kasus klasik untuk tetap di halaman: pembaca sedang di tengah tugas, dan mengirimnya ke halaman terima kasih hanya untuk mencatat satu klik akan konyol. Pola yang sama bekerja untuk formulir apa pun.
Apa yang berubah saat Anda mengambil alih submit
Form post biasa memberi Anda banyak hal secara gratis: browser menampilkan indikator loading, membuat klik ganda tidak terlalu berpengaruh, menangani tombol back, dan menampilkan halaman apa pun yang kembali. Begitu Anda memanggil preventDefault(), semua itu menjadi tugas Anda. Panggilan fetch-nya sendiri hanya tiga baris; sisa halaman ini adalah pekerjaan yang dulu dilakukan browser.
- Browser menjalankan pemeriksaan batasannya lebih dulu. Event submit hanya terpicu saat formulir valid.
- Listener Anda memanggil preventDefault, jadi tab tidak bernavigasi.
- Listener mengirim data formulir dengan fetch dan meminta JSON.
- Pada 201, listener mengganti formulir dengan pesan status dan memindahkan fokus ke sana.
- Pada 422 atau kegagalan jaringan, listener menampilkan pesan di samping field, memfokuskannya, dan mengaktifkan pengiriman lagi.
| Yang dulu dilakukan browser | Kini script Anda harus |
|---|---|
| Menampilkan status loading di tab | Tampilkan satu, dan umumkan ke pembaca layar |
| Membuat klik ganda hampir tidak berbahaya | Abaikan submit kedua selama satu masih berjalan |
| Menampilkan halaman error | Petakan error.fields ke setiap input |
| Menampilkan halaman error jaringan | Tangkap request yang gagal dan timeout |
| Memindahkan fokus ke halaman baru | Pindahkan fokus ke hasil atau field salah yang pertama |
Formulirnya
Tiga field: ya atau tidak yang wajib, komentar opsional, dan alamat halaman yang diisi script. Coba definisi di bawah; itulah yang ditegakkan Ikiform di server, jadi komentar lebih dari 2.000 karakter juga mendapat 422 di sini.
Pratinjau
Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.
<form id="page-feedback" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<fieldset>
<legend>Was this page helpful?</legend>
<label><input type="radio" name="helpful" value="yes" required /> Yes</label>
<label><input type="radio" name="helpful" value="no" /> No</label>
</fieldset>
<label for="comment">What was missing?</label>
<textarea id="comment" name="comment" rows="3" maxlength="2000" aria-describedby="comment-error"></textarea>
<p id="comment-error" hidden></p>
<input type="hidden" name="page" />
<input type="hidden" name="_redirect" value="https://docs.example.com/feedback-sent" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Send</button>
<p id="feedback-status" role="status" tabindex="-1"></p>
</form>
<script type="module" src="/page-feedback.js"></script>Formulir sudah lengkap tanpa script. Dengan JavaScript mati atau diblokir, formulir mengirim dengan cara biasa, Ikiform menjawab 303 dan pembaca mendarat di halaman _redirect. Script hanya menyempurnakannya.
Script-nya
const form = document.querySelector("#page-feedback");
const status = form.querySelector("#feedback-status");
const button = form.querySelector("button");
const pageInput = form.elements.namedItem("page");
const idempotencyKey = crypto.randomUUID();
let sending = false;
pageInput.value = location.href;
function showFieldErrors(fields) {
for (const input of form.querySelectorAll("[aria-invalid]")) input.removeAttribute("aria-invalid");
for (const hint of form.querySelectorAll("[id$='-error']")) hint.hidden = true;
let first = null;
for (const [name, messages] of Object.entries(fields)) {
const control = form.elements.namedItem(name);
const hint = form.querySelector(`#${name}-error`);
if (control instanceof HTMLElement) control.setAttribute("aria-invalid", "true");
if (hint) {
hint.textContent = messages[0];
hint.hidden = false;
}
first ??= control instanceof RadioNodeList ? control[0] : control;
}
first?.focus();
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (sending) return;
const body = new FormData(form, event.submitter);
body.set("_idempotency", idempotencyKey);
sending = true;
button.setAttribute("aria-disabled", "true");
status.textContent = "Sending...";
try {
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body,
signal: AbortSignal.timeout(15000),
});
const result = await response.json();
if (result.ok) {
form.replaceChildren(status);
status.textContent = "Thanks, that helps.";
status.focus();
return;
}
if (result.error.code === "challenge_required") {
form.submit();
return;
}
if (result.error.fields) {
status.textContent = "";
showFieldErrors(result.error.fields);
} else if (response.status === 429 && response.headers.has("Retry-After")) {
const wait = response.headers.get("Retry-After");
status.textContent = `Too many tries. Wait ${wait} seconds and send again.`;
} else {
status.textContent = result.error.message;
}
} catch {
status.textContent = "Couldn't reach the server. Check your connection and send again.";
} finally {
sending = false;
button.removeAttribute("aria-disabled");
}
});new FormData(form, event.submitter)membaca setiap kontrol bernama, ditambahnamedanvaluetombol yang diklik jika ada. Susun sebelum Anda mengubah apa pun di formulir.- Tombol mendapat
aria-disabled, bukandisabled. Flagsendingyang benar-benar memblokir, dan tombol tetap bisa difokus agar pengguna pembaca layar tidak kehilangan posisinya. - Tanpa header
Content-Type. Dengan bodyFormData, browser mengaturmultipart/form-datadengan boundary yang benar. Accept: application/jsonmembuat Ikiform menjawab dalam JSON. PanggilanfetchmengirimSec-Fetch-Mode: cors, yang toh akan memilih JSON, tetapi header ini memperjelas maksudnya.- Satu idempotency key per pemuatan halaman, dikirim sebagai field
_idempotency. Jika pembaca mengirim, jaringan menjatuhkan jawabannya, lalu ia mengirim lagi, Ikiform memutar ulang jawaban pertama alih-alih menyimpan kiriman kedua. Post yang ditolak tidak pernah mengklaim key, jadi kiriman ulang yang sudah diperbaiki tetap diproses. HeaderIdempotency-Keymelakukan hal yang sama, tetapi header kustom membuat browser mengirim request preflight lebih dulu. JavaScript dan fetch berisi aturan persisnya. AbortSignal.timeout(15000)mengubah request yang macet menjadi cabang catch alih-alih Sending... tanpa akhir.- Baris status punya
role="status", jadi teksnya dibacakan tanpa memindahkan fokus. Saat sukses, fokus pindah ke sana karena field di sekitarnya sudah hilang.
Saat formulir punya pemeriksaan manusia
Jika Spam check formulir diatur ke Ikiform, post fetch tanpa token Turnstile mendapat 403 challenge_required. Script di atas menanganinya dengan memanggil form.submit(). Method itu melewati listener submit dan mengirim dengan cara biasa, dan untuk post biasa Ikiform menampilkan halaman pemeriksaan singkatnya sendiri, lalu lanjut ke halaman _redirect Anda. Pembaca hanya meninggalkan halaman saat pemeriksaan aktif.
Agar tetap di halaman bahkan saat itu, dapatkan token lebih dulu dengan challenge(formId) dari SDK dan berikan ke submit(). Dokumentasi spam menunjukkan alurnya.
Alternatif tanpa JavaScript
Sebelum ada fetch, triknya adalah menargetkan iframe tersembunyi, jadi respons dimuat di sana dan halaman tetap di tempat:
<iframe name="feedback-sink" hidden></iframe>
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" target="feedback-sink">
...
</form>Trik ini masih berfungsi, tetapi iframe memuat halaman di ikiform.com, dan browser tidak mengizinkan script Anda membaca frame lintas origin. Anda tidak bisa membedakan sukses dari error validasi, jadi pembaca juga tidak mendapat umpan balik apa pun. Gunakan hanya jika script benar-benar tidak bisa berjalan.
use:enhance SvelteKit, dan <Form> React Router mengirim dengan cara biasa sebelum halaman ter-hydrate dan tanpa reload sesudahnya. Ringkasan menunjukkan masing-masing.Pastikan berfungsi
- Di Allowed domains formulir, tambahkan domain situs Anda, ditambah
localhostselama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja. - Kirim Yes tanpa komentar. Formulir berubah menjadi Thanks, that helps. dan panel Network menampilkan
201. - Tempel lebih dari 2.000 karakter ke komentar, hapus
maxlengthdi developer tools, lalu kirim. Pesan muncul di bawah komentar dan fokus pindah ke sana. - Klik dua kali Send. Dashboard menampilkan satu kiriman, bukan dua.
- Matikan JavaScript dan kirim. Anda mendarat di halaman
_redirect. - Atur throttling ke Offline dan kirim. Baris status menyatakan tidak bisa menjangkau server, dan Send berfungsi lagi.
Hal yang sering menjebak
| Masalah | Penyebab dan solusi |
|---|---|
| Sebuah field hilang dari post | Field itu disabled saat FormData disusun. Kontrol yang disabled tidak pernah dikirim. Susun data lebih dulu, atau kunci input dengan readonly |
400 invalid_body atau kiriman kosong | Kode mengatur Content-Type: multipart/form-data secara manual, tanpa boundary. Hapus header itu |
| Halaman tetap dimuat ulang | Listener melempar error sebelum preventDefault(), atau klik terjadi sebelum script termuat. Panggil preventDefault() lebih dulu; klik awal mengirim dengan cara biasa, yang tetap berfungsi |
response.json() melempar error | Request pergi ke tempat yang menjawab HTML, biasanya action relatif. Baca form.action di console; harus berupa URL Ikiform Anda |
403 challenge_required | Pemeriksaan manusia formulir aktif. Kembali ke form.submit() seperti di atas, atau dapatkan token dengan challenge() |
403 origin_not_allowed, atau setiap kiriman masuk ke spam | Domain halaman tidak ada di Allowed domains formulir. Tambahkan, ditambah localhost untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan. |
Pertanyaan
Tambahkan listener submit dan panggil event.preventDefault() di baris pertamanya, lalu kirim datanya sendiri dengan fetch. Dengarkan event submit formulir, bukan click tombol, agar tombol Enter juga tercakup.
Tidak, dan sebaiknya jangan. Browser mengatur multipart/form-data dengan string boundary. Header yang Anda tulis sendiri tidak punya boundary, dan server tidak bisa mem-parsing body.
Ya, selama Anda mendengarkan event submit, yang hanya terpicu setelah pemeriksaan batasan browser lolos. Memanggil form.submit() dari kode melewati pemeriksaan maupun event-nya; form.requestSubmit() menjalankan keduanya.
Abaikan submit selama request masih berjalan, dan kirim satu idempotency key per pemuatan halaman, sebagai field _idempotency atau header Idempotency-Key. Jika percobaan ulang tetap sampai ke Ikiform, Ikiform memutar ulang jawaban pertama alih-alih menyimpan kiriman lagi.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.