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 #
// 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:
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:
- Objek
PESAN: peta dari nama kontrol ke daftar kalimat, berdasarkan jenis pelanggaran padaValidityState—valueMissinguntuk kolom kosong,patternMismatchuntuk pola,tooShortuntuk panjang,rangeUnderflow/rangeOverflowuntuk rentang tanggal, dantypeMismatchuntuk format email. Kuncinya memakai nilai atributname, bukanid, karena grup radio dinilai pername. pesanUntuk(kontrol): menelusuri anggotavaliditysatu per satu dan mengembalikan kalimat pertama yang cocok; kalimat cadangan menutup jenis pelanggaran yang belum dipetakan.tandai(kontrol, teks): menuliskan pesan pada paragraf pendamping dan mengatur atributaria-invalid—truesaat isian tidak sah,falsesaat sah. Atribut ini memberi tahu pembaca layar bahwa kolom sedang bermasalah; penyempurnaannya denganaria-describedbytermasuk materi ARIA lanjutan.- Listener
submit(baris terakhir blok):preventDefault()menahan pengiriman, lalu daftarkontrolDiperiksamemuat satu perwakilan tiap kolom — untuk grup radio cukup elemen pertamanya karena validitas dinilai per grup. - Pemeriksaan per kontrol:
checkValidity()menguji isian tanpa menampilkan gelembung;tandai()lalu menampilkan atau menyembunyikan pesan pendamping, danseluruhnyaSahbaru bernilaitruebila seluruh kontrol lolos. - Ringkasan keberhasilan: bila seluruh isian sah, paragraf
ringkasandirangkai 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:
- Buka
formulir-umpan-balik.htmldi browser. Tekan kirim dengan formulir kosong dan pastikan pesan kustom tampil di bawah setiap kolom yang bermasalah — bukan gelembung browser. - Isi seluruh kolom dengan data fiktif (misalnya Budi, [email protected] ) lalu kirim; kartu ringkasan hijau harus muncul dengan nama yang diketik.
- Jalankan
../tests/cek-struktur.htmldan 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:
- MDN Web Docs — ValidityState
- MDN Web Docs — HTMLInputElement.checkValidity()
- MDN Web Docs — Atribut aria-invalid
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
formulir-umpan-balik.html
Unduh berkas<!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// 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;
}
});