Pesan bawaan browser pada Bagian 2 berfungsi, tetapi kalimatnya ditentukan browser, bahasanya mengikuti pengaturan pengunjung, dan posisinya selalu satu gelembung pada kolom pertama yang tidak sah. Bagian penutup proyek ini mengambil alih penyajian pesan dengan JavaScript: setiap kontrol mendapat kalimat kustom berbahasa Indonesia, ditampilkan tepat di bawah kolomnya, dan formulir menutup dengan ringkasan keberhasilan. Pola ini merangkum event submit, checkValidity(), dan ValidityState dari section HTML dan JavaScript menjadi satu alur utuh.

File formulir-umpan-balik.html #

Simpan file berikut sebagai formulir-umpan-balik.html di folder bagian-4/, bersama umpan-balik.js dan logo pada assets/:

<!doctype html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pendaftaran Seminar Web Komunitas</title>
  <link rel="stylesheet" href="../styles/style.css">
  <script src="umpan-balik.js" defer></script>
</head>
<body>
  <main>
    <h1>Pendaftaran Seminar Web Komunitas</h1>
    <p><img src="../assets/logo-acara.svg" alt="Logo Seminar Web Komunitas" width="96" height="96"></p>
    <p>Isi formulir berikut untuk mengikuti acara pada Sabtu, 8 Maret 2025, di Gedung Belajar Kota.</p>
    <form id="form-pendaftaran" action="www.example.com/daftar" method="post" novalidate>
      <fieldset>
        <legend>Data diri</legend>
        <label for="nama">Nama lengkap</label>
        <input class="kolom" type="text" id="nama" name="nama" autocomplete="name" required minlength="3" maxlength="40" pattern="[A-Za-z .]+" title="Hanya huruf, spasi, dan titik">
        <p id="pesan-nama" class="pesan-galat" hidden></p>
        <label for="tanggal-lahir">Tanggal lahir</label>
        <input class="kolom" type="date" id="tanggal-lahir" name="tanggal-lahir" autocomplete="bday" required min="1950-01-01" max="2010-12-31">
        <p id="pesan-tanggal-lahir" class="pesan-galat" hidden></p>
        <label for="kota">Kota domisili</label>
        <select class="kolom" id="kota" name="kota" autocomplete="address-level2" required>
          <option value="">-- Pilih kota --</option>
          <option value="bandung">Bandung</option>
          <option value="jakarta">Jakarta</option>
          <option value="surabaya">Surabaya</option>
          <option value="yogyakarta">Yogyakarta</option>
        </select>
        <p id="pesan-kota" class="pesan-galat" hidden></p>
      </fieldset>
      <fieldset>
        <legend>Kategori peserta</legend>
        <p>
          <input type="radio" id="kategori-pelajar" name="kategori" value="pelajar" required>
          <label for="kategori-pelajar">Pelajar</label>
        </p>
        <p>
          <input type="radio" id="kategori-mahasiswa" name="kategori" value="mahasiswa">
          <label for="kategori-mahasiswa">Mahasiswa</label>
        </p>
        <p>
          <input type="radio" id="kategori-profesional" name="kategori" value="profesional">
          <label for="kategori-profesional">Profesional</label>
        </p>
        <p id="pesan-kategori" class="pesan-galat" hidden></p>
      </fieldset>
      <fieldset>
        <legend>Akun</legend>
        <label for="email">Email</label>
        <input class="kolom" type="email" id="email" name="email" autocomplete="email" required>
        <p id="pesan-email" class="pesan-galat" hidden></p>
        <label for="kata-sandi">Kata sandi</label>
        <input class="kolom" type="password" id="kata-sandi" name="kata-sandi" autocomplete="new-password" required minlength="8">
        <p id="pesan-kata-sandi" class="pesan-galat" hidden></p>
      </fieldset>
      <p>
        <input type="checkbox" id="persetujuan" name="persetujuan" value="setuju" required>
        <label for="persetujuan">Saya menyetujui penggunaan data untuk keperluan acara ini</label>
      </p>
      <p id="pesan-persetujuan" class="pesan-galat" hidden></p>
      <button type="submit">Kirim pendaftaran</button>
      <p id="ringkasan" class="ringkasan" hidden></p>
    </form>
  </main>
</body>
</html>

Tiga perubahan menonjol dibanding Bagian 2. Atribut novalidate kini terpasang pada form — pemeriksaan native dimatikan agar JavaScript kita yang menentukan kapan dan bagaimana pesan tampil; aturan atribut (required, minlength, dan seterusnya) tetap dipertahankan karena justru menjadi sumber kebenaran bagi ValidityState. Setiap kontrol kemudian didampingi paragraf pesan berclass pesan-galat dengan atribut hidden bawaan, dan satu paragraf ringkasan menunggu di ujung form.

Logika umpan-balik.js #

javascript umpan-balik.js
// umpan-balik.js — pesan kustom per kontrol untuk proyek Formulir Pendaftaran.
// Dipakai bersama formulir-umpan-balik.html pada folder bagian-4/.

const formulir = document.querySelector("#form-pendaftaran");
const ringkasan = document.querySelector("#ringkasan");

const PESAN = {
  nama: {
    valueMissing: "Nama lengkap wajib diisi.",
    patternMismatch: "Nama hanya boleh memuat huruf, spasi, dan titik."
  },
  "tanggal-lahir": {
    valueMissing: "Tanggal lahir wajib dipilih.",
    rangeUnderflow: "Tanggal lahir berada di bawah rentang yang diterima.",
    rangeOverflow: "Tanggal lahir berada di atas rentang yang diterima."
  },
  kota: {
    valueMissing: "Pilih kota domisili dari daftar."
  },
  kategori: {
    valueMissing: "Pilih salah satu kategori peserta."
  },
  email: {
    valueMissing: "Email wajib diisi.",
    typeMismatch: "Format email belum benar, misalnya [email protected]."
  },
  "kata-sandi": {
    valueMissing: "Kata sandi wajib diisi.",
    tooShort: "Kata sandi minimal delapan karakter."
  },
  persetujuan: {
    valueMissing: "Centang persetujuan untuk melanjutkan."
  }
};

function pesanUntuk(kontrol) {
  const aturan = PESAN[kontrol.name] || {};
  for (const jenis of Object.keys(aturan)) {
    if (kontrol.validity[jenis]) {
      return aturan[jenis];
    }
  }
  return "Isian pada kolom ini belum sesuai.";
}

function tandai(kontrol, teks) {
  kontrol.setAttribute("aria-invalid", teks === "" ? "false" : "true");
  const wadah = document.querySelector("#pesan-" + kontrol.name);
  if (!wadah) {
    return;
  }
  if (teks === "") {
    wadah.textContent = "";
    wadah.hidden = true;
  } else {
    wadah.textContent = teks;
    wadah.hidden = false;
  }
}

formulir.addEventListener("submit", function (event) {
  event.preventDefault();
  ringkasan.hidden = true;

  const kontrolDiperiksa = [
    formulir.elements.namedItem("nama"),
    formulir.elements.namedItem("tanggal-lahir"),
    formulir.elements.namedItem("kota"),
    formulir.elements.namedItem("kategori")[0],
    formulir.elements.namedItem("email"),
    formulir.elements.namedItem("kata-sandi"),
    formulir.elements.namedItem("persetujuan")
  ];

  let seluruhnyaSah = true;
  for (const kontrol of kontrolDiperiksa) {
    const sah = kontrol.checkValidity();
    tandai(kontrol, sah ? "" : pesanUntuk(kontrol));
    if (!sah) {
      seluruhnyaSah = false;
    }
  }

  if (seluruhnyaSah) {
    ringkasan.textContent =
      "Terima kasih, " +
      formulir.elements.namedItem("nama").value +
      ". Data pendaftaran untuk " +
      formulir.elements.namedItem("email").value +
      " siap diproses (simulasi, tanpa pengiriman nyata).";
    ringkasan.hidden = false;
  }
});

Output yang Dihasilkan:

text
Saat tombol dikirim dengan kolom nama kosong:
  - Teks merah "Nama lengkap wajib diisi." tampil tepat di bawah kolom nama
  - Pesan kolom lain tidak tampil jika isian mereka sah
Saat nama diisi "Bu" dan kata sandi "abc" lalu dikirim:
  - "Nama hanya boleh memuat huruf, spasi, dan titik." tampil
  - "Kata sandi minimal delapan karakter." tampil
  - Halaman tidak berpindah dan tidak ada gelembung browser
Saat seluruh isian sah lalu dikirim:
  - Kartu hijau muncul: "Terima kasih, Budi. Data pendaftaran untuk
    [email protected] siap diproses (simulasi, tanpa pengiriman nyata)."

Mari kita bedah alurnya:

  1. Objek PESAN: peta dari nama kontrol ke daftar kalimat, berdasarkan jenis pelanggaran pada ValidityState — valueMissing untuk kolom kosong, patternMismatch untuk pola, tooShort untuk panjang, rangeUnderflow/rangeOverflow untuk rentang tanggal, dan typeMismatch untuk format email. Kuncinya memakai nilai atribut name, bukan id, karena grup radio dinilai per name.
  2. pesanUntuk(kontrol): menelusuri anggota validity satu per satu dan mengembalikan kalimat pertama yang cocok; kalimat cadangan menutup jenis pelanggaran yang belum dipetakan.
  3. tandai(kontrol, teks): menuliskan pesan pada paragraf pendamping dan mengatur atribut aria-invalid — true saat isian tidak sah, false saat sah. Atribut ini memberi tahu pembaca layar bahwa kolom sedang bermasalah; penyempurnaannya dengan aria-describedby termasuk materi ARIA lanjutan.
  4. Listener submit (baris terakhir blok): preventDefault() menahan pengiriman, lalu daftar kontrolDiperiksa memuat satu perwakilan tiap kolom — untuk grup radio cukup elemen pertamanya karena validitas dinilai per grup.
  5. Pemeriksaan per kontrol: checkValidity() menguji isian tanpa menampilkan gelembung; tandai() lalu menampilkan atau menyembunyikan pesan pendamping, dan seluruhnyaSah baru bernilai true bila seluruh kontrol lolos.
  6. Ringkasan keberhasilan: bila seluruh isian sah, paragraf ringkasan dirangkai dari nilai nama dan email, lalu ditampilkan. Pengiriman nyata ke server tidak terjadi — contoh ini simulasi, dan data tetap perlu divalidasi ulang di sisi server.

Langkah Pemeriksaan #

Tuntaskan Bagian ini sekaligus proyek dengan urutan berikut:

  1. Buka formulir-umpan-balik.html di browser. Tekan kirim dengan formulir kosong dan pastikan pesan kustom tampil di bawah setiap kolom yang bermasalah — bukan gelembung browser.
  2. Isi seluruh kolom dengan data fiktif (misalnya Budi, [email protected] ) lalu kirim; kartu ringkasan hijau harus muncul dengan nama yang diketik.
  3. Jalankan ../tests/cek-struktur.html dan centang seluruh butir tersisa, termasuk kelompok Umpan balik JavaScript. Checklist menyimpan progresnya otomatis.

Proyek Formulir Pendaftaran tuntas: struktur yang berlabel benar, validasi berlapis, gaya yang serasi, dan umpan balik yang berbahasa sendiri. Pola formulir ini siap dipakai ulang untuk hampir semua kebutuhan pengumpulan data berikutnya.

Referensi resmi untuk pendalaman:

Berkas Sumber

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

formulir-umpan-balik.html

Unduh berkas
html
<!doctype html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pendaftaran Seminar Web Komunitas</title>
  <link rel="stylesheet" href="../styles/style.css">
  <script src="umpan-balik.js" defer></script>
</head>
<body>
  <main>
    <h1>Pendaftaran Seminar Web Komunitas</h1>
    <p><img src="../assets/logo-acara.svg" alt="Logo Seminar Web Komunitas" width="96" height="96"></p>
    <p>Isi formulir berikut untuk mengikuti acara pada Sabtu, 8 Maret 2025, di Gedung Belajar Kota.</p>
    <form id="form-pendaftaran" action="www.example.com/daftar" method="post" novalidate>
      <fieldset>
        <legend>Data diri</legend>
        <label for="nama">Nama lengkap</label>
        <input class="kolom" type="text" id="nama" name="nama" autocomplete="name" required minlength="3" maxlength="40" pattern="[A-Za-z .]+" title="Hanya huruf, spasi, dan titik">
        <p id="pesan-nama" class="pesan-galat" hidden></p>
        <label for="tanggal-lahir">Tanggal lahir</label>
        <input class="kolom" type="date" id="tanggal-lahir" name="tanggal-lahir" autocomplete="bday" required min="1950-01-01" max="2010-12-31">
        <p id="pesan-tanggal-lahir" class="pesan-galat" hidden></p>
        <label for="kota">Kota domisili</label>
        <select class="kolom" id="kota" name="kota" autocomplete="address-level2" required>
          <option value="">-- Pilih kota --</option>
          <option value="bandung">Bandung</option>
          <option value="jakarta">Jakarta</option>
          <option value="surabaya">Surabaya</option>
          <option value="yogyakarta">Yogyakarta</option>
        </select>
        <p id="pesan-kota" class="pesan-galat" hidden></p>
      </fieldset>
      <fieldset>
        <legend>Kategori peserta</legend>
        <p>
          <input type="radio" id="kategori-pelajar" name="kategori" value="pelajar" required>
          <label for="kategori-pelajar">Pelajar</label>
        </p>
        <p>
          <input type="radio" id="kategori-mahasiswa" name="kategori" value="mahasiswa">
          <label for="kategori-mahasiswa">Mahasiswa</label>
        </p>
        <p>
          <input type="radio" id="kategori-profesional" name="kategori" value="profesional">
          <label for="kategori-profesional">Profesional</label>
        </p>
        <p id="pesan-kategori" class="pesan-galat" hidden></p>
      </fieldset>
      <fieldset>
        <legend>Akun</legend>
        <label for="email">Email</label>
        <input class="kolom" type="email" id="email" name="email" autocomplete="email" required>
        <p id="pesan-email" class="pesan-galat" hidden></p>
        <label for="kata-sandi">Kata sandi</label>
        <input class="kolom" type="password" id="kata-sandi" name="kata-sandi" autocomplete="new-password" required minlength="8">
        <p id="pesan-kata-sandi" class="pesan-galat" hidden></p>
      </fieldset>
      <p>
        <input type="checkbox" id="persetujuan" name="persetujuan" value="setuju" required>
        <label for="persetujuan">Saya menyetujui penggunaan data untuk keperluan acara ini</label>
      </p>
      <p id="pesan-persetujuan" class="pesan-galat" hidden></p>
      <button type="submit">Kirim pendaftaran</button>
      <p id="ringkasan" class="ringkasan" hidden></p>
    </form>
  </main>
</body>
</html>

umpan-balik.js

Unduh berkas
javascript
// umpan-balik.js — pesan kustom per kontrol untuk proyek Formulir Pendaftaran.
// Dipakai bersama formulir-umpan-balik.html pada folder bagian-4/.

const formulir = document.querySelector("#form-pendaftaran");
const ringkasan = document.querySelector("#ringkasan");

const PESAN = {
  nama: {
    valueMissing: "Nama lengkap wajib diisi.",
    patternMismatch: "Nama hanya boleh memuat huruf, spasi, dan titik."
  },
  "tanggal-lahir": {
    valueMissing: "Tanggal lahir wajib dipilih.",
    rangeUnderflow: "Tanggal lahir berada di bawah rentang yang diterima.",
    rangeOverflow: "Tanggal lahir berada di atas rentang yang diterima."
  },
  kota: {
    valueMissing: "Pilih kota domisili dari daftar."
  },
  kategori: {
    valueMissing: "Pilih salah satu kategori peserta."
  },
  email: {
    valueMissing: "Email wajib diisi.",
    typeMismatch: "Format email belum benar, misalnya [email protected]."
  },
  "kata-sandi": {
    valueMissing: "Kata sandi wajib diisi.",
    tooShort: "Kata sandi minimal delapan karakter."
  },
  persetujuan: {
    valueMissing: "Centang persetujuan untuk melanjutkan."
  }
};

function pesanUntuk(kontrol) {
  const aturan = PESAN[kontrol.name] || {};
  for (const jenis of Object.keys(aturan)) {
    if (kontrol.validity[jenis]) {
      return aturan[jenis];
    }
  }
  return "Isian pada kolom ini belum sesuai.";
}

function tandai(kontrol, teks) {
  kontrol.setAttribute("aria-invalid", teks === "" ? "false" : "true");
  const wadah = document.querySelector("#pesan-" + kontrol.name);
  if (!wadah) {
    return;
  }
  if (teks === "") {
    wadah.textContent = "";
    wadah.hidden = true;
  } else {
    wadah.textContent = teks;
    wadah.hidden = false;
  }
}

formulir.addEventListener("submit", function (event) {
  event.preventDefault();
  ringkasan.hidden = true;

  const kontrolDiperiksa = [
    formulir.elements.namedItem("nama"),
    formulir.elements.namedItem("tanggal-lahir"),
    formulir.elements.namedItem("kota"),
    formulir.elements.namedItem("kategori")[0],
    formulir.elements.namedItem("email"),
    formulir.elements.namedItem("kata-sandi"),
    formulir.elements.namedItem("persetujuan")
  ];

  let seluruhnyaSah = true;
  for (const kontrol of kontrolDiperiksa) {
    const sah = kontrol.checkValidity();
    tandai(kontrol, sah ? "" : pesanUntuk(kontrol));
    if (!sah) {
      seluruhnyaSah = false;
    }
  }

  if (seluruhnyaSah) {
    ringkasan.textContent =
      "Terima kasih, " +
      formulir.elements.namedItem("nama").value +
      ". Data pendaftaran untuk " +
      formulir.elements.namedItem("email").value +
      " siap diproses (simulasi, tanpa pengiriman nyata).";
    ringkasan.hidden = false;
  }
});