Tutorial: Membuat Formulir Kontak Dinamis dan Validasi Real-time dengan JavaScript (Tanpa Library)
Formulir kontak adalah jembatan penting antara pengunjung dan pemilik website. Formulir yang dirancang dengan baik tidak hanya memudahkan pengguna mengirim pesan, tetapi juga memberikan pengalaman yang positif. Salah satu cara untuk meningkatkan pengalaman tersebut adalah dengan validasi real-time, di mana pengguna mendapatkan umpan balik instan tentang input mereka, bahkan sebelum menekan tombol kirim.
Dalam tutorial ini, kita akan membangun formulir kontak yang responsif, dinamis, dan memberikan validasi langsung saat pengguna mengetik, semuanya menggunakan HTML, CSS, dan JavaScript murni (Vanilla JS).
1. Pendahuluan
Pentingnya Formulir Kontak
Formulir kontak memungkinkan pengunjung untuk bertanya, memberikan masukan, meminta layanan, atau sekadar berinteraksi. Tanpanya, Anda mungkin kehilangan peluang berharga.
Mengapa Validasi Real-time?
Tradisionalnya, validasi formulir terjadi setelah pengguna menekan tombol “Kirim”. Jika ada kesalahan, pengguna harus memperbaiki dan mengirim ulang. Validasi real-time mengubah ini dengan:
- Umpan Balik Instan: Pengguna tahu seketika jika input mereka valid atau tidak.
- Mengurangi Frustrasi: Menghindari pengguna mengisi seluruh formulir hanya untuk menemukan kesalahan di akhir.
- Meningkatkan Tingkat Penyelesaian: Pengguna lebih cenderung menyelesaikan formulir jika prosesnya mulus.
- Mengurangi Beban Server: Kesalahan dasar bisa ditangani di sisi klien sebelum data dikirim ke server.
Tujuan Tutorial
Kita akan membuat formulir kontak yang:
- Memiliki struktur HTML yang semantik.
- Responsif dan terlihat baik di berbagai perangkat.
- Memberikan validasi real-time untuk setiap field (Nama, Email, Subjek, Pesan).
- Menampilkan pesan error yang jelas dan membantu.
- Menonaktifkan tombol kirim jika masih ada error.
2. Struktur HTML Dasar
Kita akan mulai dengan membuat kerangka HTML untuk formulir kontak kita.
Buat file index.html
dengan struktur berikut:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formulir Kontak Dinamis</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="form-container">
<h1>Hubungi Kami</h1>
<p>Ada pertanyaan atau masukan? Jangan ragu untuk mengirim pesan.</p>
<form id="contactForm" novalidate>
<div class="form-group">
<label for="name">Nama Lengkap:</label>
<input type="text" id="name" name="name" required minlength="3" placeholder="Masukkan nama Anda">
<div class="error-message" id="nameError"></div>
</div>
<div class="form-group">
<label for="email">Alamat Email:</label>
<input type="email" id="email" name="email" required placeholder="contoh@email.com">
<div class="error-message" id="emailError"></div>
</div>
<div class="form-group">
<label for="subject">Subjek Pesan:</label>
<input type="text" id="subject" name="subject" required minlength="5" placeholder="Tuliskan subjek pesan">
<div class="error-message" id="subjectError"></div>
</div>
<div class="form-group">
<label for="message">Pesan Anda:</label>
<textarea id="message" name="message" rows="5" required minlength="10" placeholder="Tuliskan pesan Anda di sini..."></textarea>
<div class="error-message" id="messageError"></div>
</div>
<button type="submit" id="submitButton" class="submit-btn">Kirim Pesan</button>
</form>
<div id="formStatus" class="form-status-message"></div>
</div>
<script src="script.js"></script>
</body>
</html>
Penjelasan Struktur HTML:
<div class="form-container">
: Wrapper utama untuk formulir dan judulnya.<form id="contactForm" novalidate>
:id="contactForm"
: Untuk targeting JavaScript.novalidate
: Atribut ini penting untuk menonaktifkan validasi HTML5 bawaan browser, sehingga kita bisa mengontrol validasi sepenuhnya dengan JavaScript.
<div class="form-group">
: Mengelompokkan setiap label, input, dan pesan error.<label for="...">
: Menghubungkan label dengan inputnya untuk aksesibilitas.<input type="...">
dan<textarea>
: Field input standar.id
danname
: Penting untuk JavaScript dan pengiriman data ke server.required
: Menandakan field wajib diisi (JavaScript kita akan menghormati ini).minlength
: Atribut HTML5 untuk panjang minimal (JavaScript kita juga bisa menggunakannya).placeholder
: Teks bantuan di dalam field.
<div class="error-message" id="...">
: Elemen untuk menampilkan pesan error spesifik untuk setiap field. Awalnya kosong.<button type="submit" id="submitButton">
: Tombol untuk mengirim formulir.<div id="formStatus">
: Untuk menampilkan pesan status global setelah submit (misalnya, “Pesan terkirim!” atau “Gagal mengirim.”).
3. Styling Formulir dengan CSS
Kita akan memberikan tampilan yang bersih, modern, dan responsif pada formulir.
Buat file style.css
:
/* style.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f4f8;
color: #333;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: flex-start; /* Agar form tidak terlalu ke tengah jika konten sedikit */
min-height: 100vh;
}
.form-container {
background-color: #fff;
padding: 30px 40px;
border-radius: 10px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 600px;
}
.form-container h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 10px;
}
.form-container p {
text-align: center;
color: #7f8c8d;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
position: relative; /* Untuk positioning pesan error atau ikon */
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #34495e;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group textarea {
width: 100%;
padding: 12px 15px;
border: 1px solid #bdc3c7;
border-radius: 6px;
font-size: 1rem;
color: #2c3e50;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group textarea:focus {
border-color: #3498db;
outline: none;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
.form-group textarea {
resize: vertical; /* Izinkan resize vertikal */
min-height: 100px;
}
/* Styling untuk indikasi valid dan invalid */
.form-group input.valid,
.form-group textarea.valid {
border-color: #2ecc71; /* Hijau untuk valid */
}
.form-group input.valid:focus,
.form-group textarea.valid:focus {
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
}
.form-group input.invalid,
.form-group textarea.invalid {
border-color: #e74c3c; /* Merah untuk invalid */
}
.form-group input.invalid:focus,
.form-group textarea.invalid:focus {
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2);
}
.error-message {
color: #e74c3c;
font-size: 0.875rem; /* 14px */
margin-top: 5px;
display: none; /* Awalnya disembunyikan */
/* Animasi sederhana untuk pesan error */
opacity: 0;
max-height: 0;
overflow: hidden;
transition: opacity 0.3s ease, max-height 0.3s ease, margin-top 0.3s ease;
}
.error-message.show {
display: block;
opacity: 1;
max-height: 50px; /* Sesuaikan jika pesan error panjang */
margin-top: 5px;
}
.submit-btn {
display: block;
width: 100%;
padding: 15px;
background-color: #3498db;
color: white;
border: none;
border-radius: 6px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease, opacity 0.3s ease;
}
.submit-btn:hover:not(:disabled) {
background-color: #2980b9;
}
.submit-btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
opacity: 0.7;
}
.form-status-message {
margin-top: 20px;
padding: 15px;
border-radius: 6px;
text-align: center;
font-weight: 500;
display: none; /* Awalnya disembunyikan */
}
.form-status-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.form-status-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Responsif */
@media (max-width: 480px) {
.form-container {
padding: 20px;
}
.form-container h1 {
font-size: 1.8rem;
}
}
Penjelasan Styling:
- Layout dasar dibuat dengan
max-width
pada.form-container
dan menengahkannya. - Setiap
.form-group
diberimargin-bottom
untuk spasi. - Styling untuk
input
dantextarea
, termasuk efek:focus
. - Kelas
.valid
dan.invalid
akan ditambahkan oleh JavaScript ke input untuk mengubah warna border. .error-message
: Awalnya disembunyikan (display: none
,opacity: 0
,max-height: 0
). Kelas.show
akan ditambahkan oleh JS untuk menampilkannya dengan animasi..submit-btn
: Styling tombol kirim, termasuk state:disabled
..form-status-message
: Untuk pesan setelah submit.
4. Menambahkan Validasi Real-time dengan JavaScript
Ini adalah inti dari tutorial. Kita akan menangkap event input pengguna dan memberikan umpan balik langsung.
Buat file script.js
:
// script.js
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contactForm');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const subjectInput = document.getElementById('subject');
const messageInput = document.getElementById('message');
const submitButton = document.getElementById('submitButton');
const formStatus = document.getElementById('formStatus');
// Objek untuk menyimpan status validasi setiap field
const validationStatus = {
name: false,
email: false,
subject: false,
message: false
};
// Fungsi untuk menampilkan error
const showError = (inputElement, errorElementId, message) => {
const errorElement = document.getElementById(errorElementId);
inputElement.classList.remove('valid');
inputElement.classList.add('invalid');
errorElement.textContent = message;
errorElement.classList.add('show');
};
// Fungsi untuk menghilangkan error (menandai valid)
const showSuccess = (inputElement, errorElementId) => {
const errorElement = document.getElementById(errorElementId);
inputElement.classList.remove('invalid');
inputElement.classList.add('valid');
errorElement.textContent = '';
errorElement.classList.remove('show');
};
// Fungsi untuk memeriksa apakah semua field valid
const checkFormValidity = () => {
const allValid = Object.values(validationStatus).every(status => status === true);
submitButton.disabled = !allValid;
};
// Validasi Nama
const validateName = () => {
const nameValue = nameInput.value.trim();
if (nameValue === '') {
showError(nameInput, 'nameError', 'Nama tidak boleh kosong.');
validationStatus.name = false;
} else if (nameValue.length < 3) {
showError(nameInput, 'nameError', 'Nama minimal 3 karakter.');
validationStatus.name = false;
} else {
showSuccess(nameInput, 'nameError');
validationStatus.name = true;
}
checkFormValidity();
};
// Validasi Email
const validateEmail = () => {
const emailValue = emailInput.value.trim();
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Pola regex sederhana untuk email
if (emailValue === '') {
showError(emailInput, 'emailError', 'Email tidak boleh kosong.');
validationStatus.email = false;
} else if (!emailPattern.test(emailValue)) {
showError(emailInput, 'emailError', 'Format email tidak valid.');
validationStatus.email = false;
} else {
showSuccess(emailInput, 'emailError');
validationStatus.email = true;
}
checkFormValidity();
};
// Validasi Subjek
const validateSubject = () => {
const subjectValue = subjectInput.value.trim();
if (subjectValue === '') {
showError(subjectInput, 'subjectError', 'Subjek tidak boleh kosong.');
validationStatus.subject = false;
} else if (subjectValue.length < 5) {
showError(subjectInput, 'subjectError', 'Subjek minimal 5 karakter.');
validationStatus.subject = false;
} else {
showSuccess(subjectInput, 'subjectError');
validationStatus.subject = true;
}
checkFormValidity();
};
// Validasi Pesan
const validateMessage = () => {
const messageValue = messageInput.value.trim();
if (messageValue === '') {
showError(messageInput, 'messageError', 'Pesan tidak boleh kosong.');
validationStatus.message = false;
} else if (messageValue.length < 10) {
showError(messageInput, 'messageError', 'Pesan minimal 10 karakter.');
validationStatus.message = false;
} else {
showSuccess(messageInput, 'messageError');
validationStatus.message = true;
}
checkFormValidity();
};
// Tambahkan event listener untuk validasi real-time
nameInput.addEventListener('input', validateName);
nameInput.addEventListener('blur', validateName); // Validasi juga saat field kehilangan fokus
emailInput.addEventListener('input', validateEmail);
emailInput.addEventListener('blur', validateEmail);
subjectInput.addEventListener('input', validateSubject);
subjectInput.addEventListener('blur', validateSubject);
messageInput.addEventListener('input', validateMessage);
messageInput.addEventListener('blur', validateMessage);
// Validasi awal saat halaman dimuat (jika ada nilai dari autocomplete browser)
validateName();
validateEmail();
validateSubject();
validateMessage();
// checkFormValidity(); // Dipanggil di akhir setiap fungsi validasi
// Handle submit form
form.addEventListener('submit', (event) => {
event.preventDefault(); // Mencegah submit form standar
// Lakukan validasi sekali lagi untuk semua field sebelum submit
validateName();
validateEmail();
validateSubject();
validateMessage();
if (Object.values(validationStatus).every(status => status === true)) {
// Jika semua valid, simulasi pengiriman form
submitButton.disabled = true;
submitButton.textContent = 'Mengirim...';
formStatus.textContent = '';
formStatus.className = 'form-status-message'; // Reset kelas
formStatus.style.display = 'none';
// Simulasi AJAX request dengan setTimeout
setTimeout(() => {
// Asumsikan pengiriman berhasil
formStatus.textContent = 'Pesan Anda telah berhasil terkirim! Terima kasih.';
formStatus.classList.add('success');
formStatus.style.display = 'block';
form.reset(); // Reset form
// Reset status validasi dan tampilan field
Object.keys(validationStatus).forEach(key => validationStatus[key] = false);
[nameInput, emailInput, subjectInput, messageInput].forEach(input => {
input.classList.remove('valid', 'invalid');
document.getElementById(input.id + 'Error').classList.remove('show');
document.getElementById(input.id + 'Error').textContent = '';
});
submitButton.textContent = 'Kirim Pesan';
// Tombol submit tetap disabled sampai user mulai mengetik lagi (dihandle oleh checkFormValidity)
checkFormValidity();
}, 2000); // Delay 2 detik untuk simulasi
} else {
// Jika ada yang tidak valid (seharusnya tidak terjadi jika tombol submit aktif)
formStatus.textContent = 'Harap perbaiki error pada formulir sebelum mengirim.';
formStatus.classList.add('error');
formStatus.style.display = 'block';
submitButton.disabled = false; // Pastikan tombol bisa diklik lagi jika ada error tak terduga
}
});
});
Penjelasan JavaScript:
- Seleksi Elemen DOM.
validationStatus
: Objek untuk melacak status validasi setiap field.showError()
danshowSuccess()
: Fungsi helper untuk menampilkan/menyembunyikan pesan error dan mengubah tampilan border input.checkFormValidity()
: Memeriksa apakah semua field divalidationStatus
bernilaitrue
. Jika ya, tombol submit diaktifkan; jika tidak, dinonaktifkan.- Fungsi Validasi Per Field (
validateName
,validateEmail
, dll.):- Mengambil nilai input dan membersihkannya (
trim()
). - Melakukan pengecekan berdasarkan aturan (kosong, panjang minimal, format email).
- Memanggil
showError()
ataushowSuccess()
sesuai hasil. - Mengupdate
validationStatus
untuk field tersebut. - Memanggil
checkFormValidity()
di akhir.
- Mengambil nilai input dan membersihkannya (
- Event Listener (
input
danblur
):input
: Terpicu setiap kali pengguna mengetik atau mengubah nilai field. Ini memberikan validasi real-time.blur
: Terpicu saat field kehilangan fokus. Berguna untuk menangkap error jika pengguna langsung pindah field.
- Validasi Awal: Memanggil semua fungsi validasi saat halaman dimuat untuk menangani kasus di mana browser mengisi form secara otomatis (autocomplete) dan tombol submit mungkin perlu dinonaktifkan dari awal.
- Handle Submit Form:
event.preventDefault()
: Mencegah halaman reload saat form disubmit.- Validasi ulang semua field (sebagai jaring pengaman).
- Jika semua valid:
- Simulasi pengiriman data (menggunakan
setTimeout
). Di aplikasi nyata, di sini Anda akan menggunakanfetch
atauXMLHttpRequest
untuk mengirim data ke server. - Menampilkan pesan sukses.
- Mereset formulir dan status validasi.
- Simulasi pengiriman data (menggunakan
- Jika tidak valid (seharusnya tombol sudah disabled, tapi ini untuk jaga-jaga): tampilkan pesan error global.
5. Fitur Dinamis Tambahan (Opsional)
A. Loader Saat Form Dikirim
Kita sudah melakukan simulasi “Mengirim…” pada teks tombol submit. Anda bisa menambahkan ikon loader CSS jika ingin lebih visual.
B. Reset Otomatis Setelah Sukses
Formulir sudah di-reset (form.reset()
) setelah simulasi pengiriman berhasil.
C. Validasi Server-Side Simulatif (Mockup dengan Timeout)
Di dalam setTimeout
pada event submit, Anda bisa menambahkan logika if/else
untuk secara acak mensimulasikan kegagalan dari server:
// Di dalam setTimeout pada form submit event listener
setTimeout(() => {
const isSuccess = Math.random() > 0.3; // 70% kemungkinan sukses
if (isSuccess) {
formStatus.textContent = 'Pesan Anda telah berhasil terkirim! Terima kasih.';
formStatus.classList.add('success');
form.reset();
// ... reset validasi ...
} else {
formStatus.textContent = 'Maaf, terjadi kesalahan di server. Silakan coba lagi.';
formStatus.classList.add('error');
}
formStatus.style.display = 'block';
submitButton.disabled = false; // Aktifkan lagi tombol setelah response server (simulasi)
submitButton.textContent = 'Kirim Pesan';
if(isSuccess) checkFormValidity(); // Nonaktifkan jika form sudah kosong
}, 2000);
Ini membantu Anda melihat bagaimana aplikasi bereaksi terhadap berbagai respons server.
6. Aksesibilitas dan UX
- Atribut ARIA:
- Pada setiap input, Anda bisa menambahkan
aria-describedby
yang menunjuk ke ID elemen pesan errornya. Ini membantu screen reader mengumumkan pesan error yang terkait dengan input. Contoh:<input type="text" id="name" ... aria-describedby="nameError">
- Saat input menjadi tidak valid, tambahkan
aria-invalid="true"
ke elemen input, danaria-invalid="false"
atau hapus atribut saat valid.
// Di dalam showError: inputElement.setAttribute('aria-invalid', 'true'); // Di dalam showSuccess: inputElement.setAttribute('aria-invalid', 'false'); // Atau inputElement.removeAttribute('aria-invalid');
- Pada setiap input, Anda bisa menambahkan
- Fokus Otomatis ke Input yang Error: Saat form disubmit dan ada error, Anda bisa memfokuskan ke field pertama yang error.
// Di dalam event listener submit, jika validasi gagal: // const firstInvalidField = Object.keys(validationStatus).find(key => !validationStatus[key]); // if (firstInvalidField) { // document.getElementById(firstInvalidField).focus(); // }
- Placeholder vs Label: Kita sudah menggunakan keduanya. Label (
<label>
) penting untuk aksesibilitas dan selalu terlihat. Placeholder bisa memberikan petunjuk tambahan tapi menghilang saat pengguna mulai mengetik. Jangan mengandalkan placeholder sebagai pengganti label.
7. Testing dan Troubleshooting
- Tes di Berbagai Browser: Pastikan formulir berfungsi dan terlihat baik di Chrome, Firefox, Edge, dan Safari (jika memungkinkan).
- Simulasi Keyboard-Only User: Coba navigasi dan isi formulir hanya menggunakan keyboard (Tab, Shift+Tab, Space, Enter). Pastikan semua interaktif dan indikator fokus jelas.
- Validasi Data Salah dan Benar: Uji semua skenario validasi:
- Biarkan field kosong.
- Masukkan data yang terlalu pendek.
- Masukkan format email yang salah.
- Masukkan data yang benar.
- Pastikan pesan error muncul dan hilang dengan benar, dan tombol submit aktif/nonaktif sesuai.
- Cek Konsol Browser: Perhatikan error JavaScript yang mungkin muncul di konsol Developer Tools.
8. Penutup
Luar biasa! Anda telah berhasil membangun formulir kontak yang tidak hanya fungsional tetapi juga memberikan pengalaman pengguna yang jauh lebih baik dengan validasi real-time.
Rekap Fitur yang Telah Dibangun:
- Formulir kontak HTML yang semantik dan responsif.
- Styling CSS modern dengan indikasi visual untuk status validasi.
- Validasi input real-time menggunakan JavaScript untuk nama, email, subjek, dan pesan.
- Pesan error dinamis yang muncul dan hilang dengan animasi.
- Tombol kirim yang aktif/nonaktif berdasarkan validitas formulir.
- Simulasi pengiriman formulir dengan umpan balik status.
- Pertimbangan dasar untuk aksesibilitas (UX).
Keuntungan Validasi Real-time: Dengan memberikan umpan balik instan, Anda membuat proses pengisian formulir menjadi lebih intuitif, mengurangi kesalahan, dan meningkatkan kemungkinan pengguna berhasil mengirim pesan. Ini adalah investasi kecil dalam kode yang memberikan dampak besar pada pengalaman pengguna.
Tantangan Tambahan:
- Integrasi Backend Nyata: Saat ini kita hanya mensimulasikan pengiriman. Langkah selanjutnya adalah mengirim data formulir ke skrip backend (misalnya, PHP) menggunakan
fetch
API atauXMLHttpRequest
(AJAX) untuk diproses (misalnya, dikirim sebagai email atau disimpan ke database). - Validasi Server-Side (Wajib): Ingat, validasi JavaScript di sisi klien adalah untuk UX, bukan keamanan. Validasi sisi server selalu diperlukan karena JavaScript bisa dimatikan atau dilewati.
- Perlindungan dari Spam: Implementasi CAPTCHA (seperti reCAPTCHA) atau teknik lain untuk mencegah bot spam.
- Validasi Asinkron: Misalnya, mengecek apakah username atau email sudah terdaftar di database secara real-time (memerlukan panggilan AJAX).
Teruslah bereksperimen dan tingkatkan formulir ini. Kemampuan membuat formulir yang baik dan interaktif adalah aset berharga bagi setiap developer web. Semoga tutorial ini bermanfaat!