Pengiriman formulir HTML

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.

Preet SutharPendiri, Ikiform

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.

Apa yang dilakukan script saat mengirim formulir dengan fetch alih-alih membiarkan browser bernavigasievent submitsetelahpemeriksaanbrowser sendiripreventDefault()tanpa navigasifetchFormDataAccept:application/json201tampilkan baristerima kasihpesan difieldfokus pindah kesana, tombolberfungsi lagi422 atau error jaringanApa yang dilakukan script saat mengirim formulir dengan fetch alih-alih membiarkan browser bernavigasievent submitsetelah pemeriksaanbrowser sendiripreventDefault()tanpa navigasifetch FormDataAccept:application/json201tampilkan baris terimakasihpesan difieldfokus pindahke sana,tombolberfungsilagi422 atau error jaringan
  1. Browser menjalankan pemeriksaan batasannya lebih dulu. Event submit hanya terpicu saat formulir valid.
  2. Listener Anda memanggil preventDefault, jadi tab tidak bernavigasi.
  3. Listener mengirim data formulir dengan fetch dan meminta JSON.
  4. Pada 201, listener mengganti formulir dengan pesan status dan memindahkan fokus ke sana.
  5. Pada 422 atau kegagalan jaringan, listener menampilkan pesan di samping field, memfokuskannya, dan mengaktifkan pengiriman lagi.
Validasi browser tetap berjalan sebelum kode Anda. Semua setelah preventDefault menjadi tanggung jawab Anda.
Yang dulu dilakukan browserKini script Anda harus
Menampilkan status loading di tabTampilkan satu, dan umumkan ke pembaca layar
Membuat klik ganda hampir tidak berbahayaAbaikan submit kedua selama satu masih berjalan
Menampilkan halaman errorPetakan error.fields ke setiap input
Menampilkan halaman error jaringanTangkap request yang gagal dan timeout
Memindahkan fokus ke halaman baruPindahkan 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.

Apakah halaman ini membantu?
Tempel ini di tab Fields formulir. Dengan field terdefinisi, Ikiform juga menolak field apa pun yang tidak dikenalnya.
feedback.html
<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

page-feedback.js
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, ditambah name dan value tombol yang diklik jika ada. Susun sebelum Anda mengubah apa pun di formulir.
  • Tombol mendapat aria-disabled, bukan disabled. Flag sending yang benar-benar memblokir, dan tombol tetap bisa difokus agar pengguna pembaca layar tidak kehilangan posisinya.
  • Tanpa header Content-Type. Dengan body FormData, browser mengatur multipart/form-data dengan boundary yang benar.
  • Accept: application/json membuat Ikiform menjawab dalam JSON. Panggilan fetch mengirim Sec-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. Header Idempotency-Key melakukan 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:

Trik lama
<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.

Framework server bisa memberi Anda tanpa reload tanpa kode fetch di klien: server action Next.js, 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

  1. Di Allowed domains formulir, tambahkan domain situs Anda, ditambah localhost selama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja.
  2. Kirim Yes tanpa komentar. Formulir berubah menjadi Thanks, that helps. dan panel Network menampilkan 201.
  3. Tempel lebih dari 2.000 karakter ke komentar, hapus maxlength di developer tools, lalu kirim. Pesan muncul di bawah komentar dan fokus pindah ke sana.
  4. Klik dua kali Send. Dashboard menampilkan satu kiriman, bukan dua.
  5. Matikan JavaScript dan kirim. Anda mendarat di halaman _redirect.
  6. Atur throttling ke Offline dan kirim. Baris status menyatakan tidak bisa menjangkau server, dan Send berfungsi lagi.

Hal yang sering menjebak

MasalahPenyebab dan solusi
Sebuah field hilang dari postField 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 kosongKode mengatur Content-Type: multipart/form-data secara manual, tanpa boundary. Hapus header itu
Halaman tetap dimuat ulangListener 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 errorRequest pergi ke tempat yang menjawab HTML, biasanya action relatif. Baca form.action di console; harus berupa URL Ikiform Anda
403 challenge_requiredPemeriksaan manusia formulir aktif. Kembali ke form.submit() seperti di atas, atau dapatkan token dengan challenge()
403 origin_not_allowed, atau setiap kiriman masuk ke spamDomain halaman tidak ada di Allowed domains formulir. Tambahkan, ditambah localhost untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.