Master HTML5 Input Types, Custom Validation & Dynamic Forms
💡 Tip: Xem video trước khi làm bài tập để hiểu rõ hơn về các khái niệm!
Trong bài nâng cao này, bạn sẽ học:
💡 Mục tiêu: Sau khi hoàn thành bài này, bạn sẽ có thể xây dựng các form phức tạp với validation nâng cao, tương tác realtime, và trải nghiệm người dùng chuyên nghiệp. Các kỹ năng này rất quan trọng trong phỏng vấn và dự án thực tế!
| Thuộc tính | Mô tả | User có thể | Submit? | Use case |
|---|---|---|---|---|
disabled |
Vô hiệu hóa hoàn toàn | ❌ Không tương tác | ❌ Không gửi | Tính năng chưa khả dụng |
readonly |
Chỉ đọc, không chỉnh sửa | ✅ Có thể select/copy | ✅ Được gửi | Hiển thị data hệ thống |
required |
Bắt buộc nhập | ✅ Phải điền | ✅ Nếu có giá trị | Fields bắt buộc |
pattern |
Regex validation | ✅ Phải match pattern | ✅ Nếu hợp lệ | Format cụ thể (phone, code) |
minlength/maxlength |
Độ dài ký tự | ✅ Trong giới hạn | ✅ Nếu hợp lệ | Username, password |
min/max |
Giá trị min/max (number, date) | ✅ Trong khoảng | ✅ Nếu hợp lệ | Age, date range |
step |
Bước nhảy (number, range) | ✅ Theo bước | ✅ | Slider, số lượng |
autocomplete |
Tự động điền | ✅ Browser gợi ý | ✅ | Email, address, phone |
autofocus |
Focus tự động khi load | ✅ | ✅ | First field trong form |
placeholder |
Gợi ý nhập liệu | ✅ | ✅ | Hướng dẫn format |
<!-- DISABLED - Không tương tác được, không submit -->
<input type="text" name="userId" value="USER_123" disabled>
<!-- READONLY - Có thể copy, được submit -->
<input type="text" name="createdDate" value="2025-01-15" readonly>
<!-- PATTERN - Validation với Regex -->
<input type="text" name="phone"
pattern="[0-9]{4}-[0-9]{3}-[0-9]{3}"
placeholder="0123-456-789"
title="Format: 0123-456-789">
<!-- MINLENGTH/MAXLENGTH -->
<input type="text" name="username"
minlength="3"
maxlength="20"
required>
<!-- MIN/MAX/STEP -->
<input type="number" name="age"
min="18"
max="100"
step="1">
<!-- AUTOCOMPLETE -->
<input type="email" name="email"
autocomplete="email">
<input type="text" name="address"
autocomplete="street-address">
<!-- AUTOFOCUS -->
<input type="text" name="search" autofocus>
// Get/Set disabled
const input = document.getElementById('myInput');
console.log(input.disabled); // true/false
input.disabled = true; // Disable
input.disabled = false; // Enable
// Get/Set readonly
input.readOnly = true; // Readonly
input.readOnly = false; // Editable
// Get/Set required
input.required = true;
// Get/Set pattern
input.pattern = '[A-Za-z0-9]{3,20}';
// Get/Set min/max/step
input.min = '18';
input.max = '100';
input.step = '5';
// Get/Set minLength/maxLength
input.minLength = 3;
input.maxLength = 20;
// Check validity
console.log(input.validity.valid); // true/false
console.log(input.validationMessage); // Error message
// Custom validation
input.setCustomValidity('Lỗi tùy chỉnh!');
input.setCustomValidity(''); // Clear error
| Input Type | 🎯 Mục đích | ⏰ Dùng khi nào | 💡 Ví dụ thực tế |
|---|---|---|---|
date |
Chọn ngày (YYYY-MM-DD) | Cần chọn ngày sinh, deadline, booking | Ngày sinh, ngày đặt phòng, ngày hẹn |
time |
Chọn giờ (HH:MM) | Cần chọn giờ cụ thể | Giờ hẹn bác sĩ, giờ đặt bàn |
datetime-local |
Chọn ngày + giờ | Cần cả ngày và giờ | Lịch meeting, event, reminder |
week |
Chọn tuần trong năm | Báo cáo theo tuần, lịch làm việc | Report tuần, schedule tuần |
month |
Chọn tháng/năm | Thẻ tín dụng, báo cáo tháng | Expiry date thẻ, budget tháng |
range |
Slider chọn giá trị | Volume, brightness, price range | Âm lượng, độ ưu tiên, filter giá |
color |
Chọn màu sắc | Theme, highlight, design tool | Chọn màu chủ đạo, màu text |
file |
Upload file | Avatar, documents, images | Upload CV, ảnh đại diện, tài liệu |
search |
Ô tìm kiếm | Search box, filter | Tìm sản phẩm, tìm bài viết |
url |
Nhập URL | Website, social links | Facebook URL, portfolio link |
tel |
Số điện thoại | Contact, registration | Hotline, SĐT khách hàng |
<!-- Date input - Chọn ngày -->
<input type="date" name="birthdate" min="1950-01-01" max="2005-12-31">
<!-- Time input - Chọn giờ -->
<input type="time" name="appointment" min="08:00" max="18:00">
<!-- Datetime-local input - Chọn ngày + giờ -->
<input type="datetime-local" name="event_time">
<!-- Week input - Chọn tuần -->
<input type="week" name="work_week">
<!-- Month input - Chọn tháng/năm (dùng cho thẻ tín dụng) -->
<input type="month" name="card_expiry">
<!-- Range slider - Âm lượng, độ ưu tiên -->
<input type="range" name="volume" min="0" max="100" value="50">
<!-- Color picker - Chọn màu chủ đạo -->
<input type="color" name="theme_color" value="#667eea">
<!-- File upload - Avatar (single file) -->
<input type="file" name="avatar" accept="image/*">
<!-- File upload - Documents (multiple files) -->
<input type="file" name="documents" multiple accept=".pdf,.doc,.docx">
<!-- Search input - Tìm kiếm -->
<input type="search" name="query" placeholder="Tìm kiếm...">
<!-- URL input - Website link -->
<input type="url" name="website" placeholder="https://example.com">
<!-- Tel input - Số điện thoại -->
<input type="tel" name="phone" placeholder="0123456789" pattern="[0-9]{10}">
| Input Type | Thuộc tính | Mô tả | Ví dụ |
|---|---|---|---|
| date, time | min, max |
Giới hạn ngày/giờ | min="2024-01-01" |
| range | min, max, step |
Giá trị slider | step="5" |
| file | accept |
Loại file chấp nhận | accept="image/*" |
| file | multiple |
Cho phép nhiều file | multiple |
| Event | Input Type | Khi nào xảy ra | Data truy cập |
|---|---|---|---|
input |
range | Realtime khi kéo slider | e.target.value |
change |
color, date, file | Khi chọn xong | e.target.value |
change |
file | Khi chọn file | e.target.files |
// Date input - Tính tuổi
dateInput.addEventListener('change', function(e) {
const birthDate = new Date(e.target.value);
const today = new Date();
const age = Math.floor((today - birthDate) / (365.25 * 24 * 60 * 60 * 1000));
console.log('Age:', age);
});
// Range input - Realtime value
rangeInput.addEventListener('input', function(e) {
const value = e.target.value;
document.getElementById('displayValue').textContent = value;
});
// Color picker
colorInput.addEventListener('change', function(e) {
const color = e.target.value;
document.body.style.setProperty('--theme-color', color);
});
// File input - Đọc thông tin files
fileInput.addEventListener('change', function(e) {
const files = e.target.files;
Array.from(files).forEach(file => {
console.log('File:', file.name);
console.log('Size:', file.size, 'bytes');
console.log('Type:', file.type);
});
});
<form>
<fieldset>
<legend>Thông tin cá nhân</legend>
<label for="firstName">Họ:</label>
<input type="text" id="firstName" name="firstName" required>
<label for="lastName">Tên:</label>
<input type="text" id="lastName" name="lastName" required>
</fieldset>
<fieldset disabled>
<legend>Thông tin hệ thống (chỉ đọc)</legend>
<label>User ID:</label>
<input type="text" value="USER_12345" readonly>
</fieldset>
</form>
| Thuộc tính | Mô tả | Ví dụ |
|---|---|---|
disabled |
Disable toàn bộ inputs trong fieldset | <fieldset disabled> |
name |
Tên fieldset (optional) | name="personalInfo" |
// Enable/Disable fieldset
function toggleFieldset(fieldsetId, enable) {
const fieldset = document.getElementById(fieldsetId);
fieldset.disabled = !enable;
}
// Validate từng fieldset
function validateFieldset(fieldset) {
const inputs = fieldset.querySelectorAll('input[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('error');
isValid = false;
} else {
input.classList.remove('error');
}
});
return isValid;
}
❓ new FormData(form) là gì?
new FormData() là một constructor (hàm khởi tạo) tạo ra một object đặc biệtform là DOM element của form (lấy bằng getElementById, querySelector, v.v.)name và value của các input trong form🔍 FormData lưu dữ liệu như thế nào?
// Ví dụ form HTML:
<input name="username" value="john_doe">
<input name="email" value="john@mail.com">
<input name="age" value="25">
// FormData sẽ lưu như sau:
FormData {
"username" => "john_doe",
"email" => "john@mail.com",
"age" => "25"
}
⚠️ Lưu ý: Tất cả giá trị đều là string (kiểu chuỗi), kể cả số!
❓ Object.fromEntries() là gì?
Object.fromEntries() là một phương thức built-in của JavaScriptdata.username, data.email🔄 Quy trình hoạt động:
1️⃣ const formData = new FormData(form);
→ Tạo FormData object từ form
→ Lấy tất cả input có name attribute
2️⃣ const data = Object.fromEntries(formData);
→ Chuyển FormData thành object thông thường
→ Bây giờ có thể dùng: data.username, data.email
3️⃣ console.log(data);
→ Kết quả: {username: "john_doe", email: "john@mail.com", age: "25"}
💡 Tại sao không truy cập trực tiếp?
❌ Cách cũ (dài dòng):
const data = {
username: form.username.value,
email: form.email.value,
age: form.age.value,
phone: form.phone.value,
address: form.address.value
};
✅ Cách mới (ngắn gọn):
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Tự động lấy tất cả!
// Không cần viết từng field
🎯 Khi nào dùng FormData?
// Cách đơn giản nhất - FormData API
const form = document.getElementById('myForm');
// Method 1: Get all data as object
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
console.log(data);
// {username: "john", email: "john@mail.com", age: "25"}
});
// Method 2: Iterate FormData
formData.forEach((value, key) => {
console.log(key, value);
});
// Method 3: Get specific field
const username = formData.get('username');
const files = formData.getAll('photos'); // Multiple files
// Cách cũ - truy cập từng input
const form = document.getElementById('myForm');
const data = {
username: form.elements.username.value,
email: form.elements.email.value,
age: form.elements.age.value
};
// Hoặc dùng querySelector
const username = form.querySelector('[name="username"]').value;
// Hoặc getElementById
const email = document.getElementById('email').value;
// Function serialize form to JSON
function serializeForm(form) {
const formData = new FormData(form);
const data = {};
for (let [key, value] of formData.entries()) {
// Handle multiple values (checkboxes)
if (data[key]) {
if (!Array.isArray(data[key])) {
data[key] = [data[key]];
}
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
// Usage
const jsonData = serializeForm(form);
console.log(JSON.stringify(jsonData, null, 2));
// Get all checked checkboxes
const hobbies = Array.from(
form.querySelectorAll('input[name="hobbies"]:checked')
).map(cb => cb.value);
// Get selected radio
const gender = form.querySelector('input[name="gender"]:checked')?.value;
// Get select value
const country = form.querySelector('select[name="country"]').value;
// Get select text (not value)
const select = form.querySelector('select[name="country"]');
const selectedText = select.options[select.selectedIndex].text;
// Get file(s) from input
const fileInput = form.querySelector('input[type="file"]');
const files = fileInput.files; // FileList
// Single file
const file = files[0];
console.log('Name:', file.name);
console.log('Size:', file.size, 'bytes');
console.log('Type:', file.type);
// Multiple files
Array.from(files).forEach(file => {
console.log(file.name);
});
// Read file content
const reader = new FileReader();
reader.onload = function(e) {
console.log('File content:', e.target.result);
};
reader.readAsText(file); // or readAsDataURL() for images
// Validity State API
const input = document.getElementById('email');
// Check validity
console.log(input.validity.valid); // true/false
// Validity properties
console.log(input.validity.valueMissing); // required field empty
console.log(input.validity.typeMismatch); // wrong type (email)
console.log(input.validity.patternMismatch); // pattern không match
console.log(input.validity.tooLong); // vượt maxlength
console.log(input.validity.tooShort); // dưới minlength
console.log(input.validity.rangeUnderflow); // dưới min
console.log(input.validity.rangeOverflow); // trên max
console.log(input.validity.stepMismatch); // không đúng step
// Get validation message
console.log(input.validationMessage);
// Set custom error
input.setCustomValidity('Email này đã tồn tại!');
// Clear custom error
input.setCustomValidity('');
// Check form validity
const form = document.getElementById('myForm');
console.log(form.checkValidity()); // Validate toàn bộ form
// 1. Validate Required Field
function validateRequired(input) {
if (!input.value.trim()) {
input.setCustomValidity('Field này là bắt buộc!');
return false;
}
input.setCustomValidity('');
return true;
}
// 2. Validate Email
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
return { valid: false, message: 'Email không đúng định dạng!' };
}
return { valid: true };
}
// 3. Validate Phone (Vietnam)
function validatePhone(phone) {
const regex = /^(0|\+84)[0-9]{9}$/;
if (!regex.test(phone)) {
return { valid: false, message: 'SĐT phải 10 số, bắt đầu bằng 0' };
}
return { valid: true };
}
// 4. Validate Age Range
function validateAge(age, min = 18, max = 100) {
const numAge = parseInt(age);
if (isNaN(numAge)) {
return { valid: false, message: 'Tuổi phải là số!' };
}
if (numAge < min) {
return { valid: false, message: `Tuổi phải >= ${min}` };
}
if (numAge > max) {
return { valid: false, message: `Tuổi phải <= ${max}` };
}
return { valid: true };
}
// 5. Validate Password Match
function validatePasswordMatch(password, confirmPassword) {
if (password !== confirmPassword) {
return { valid: false, message: 'Mật khẩu không khớp!' };
}
return { valid: true };
}
// 6. Validate File (type & size)
function validateFile(file, allowedTypes, maxSize) {
// Check type
if (!allowedTypes.includes(file.type)) {
return {
valid: false,
message: `Chỉ chấp nhận: ${allowedTypes.join(', ')}`
};
}
// Check size (maxSize in MB)
const maxBytes = maxSize * 1024 * 1024;
if (file.size > maxBytes) {
return {
valid: false,
message: `File quá lớn! Max: ${maxSize}MB`
};
}
return { valid: true };
}
// 7. Show/Hide Error Message
function showError(input, message) {
// Remove existing error
const existingError = input.parentElement.querySelector('.error-message');
if (existingError) {
existingError.remove();
}
// Add new error
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.style.color = 'red';
errorDiv.style.fontSize = '0.85em';
errorDiv.style.marginTop = '5px';
errorDiv.textContent = message;
input.parentElement.appendChild(errorDiv);
input.style.borderColor = 'red';
}
function clearError(input) {
const errorDiv = input.parentElement.querySelector('.error-message');
if (errorDiv) {
errorDiv.remove();
}
input.style.borderColor = 'green';
}
╔══════════════════════════════════════════════╗ ║ 🎫 FORM ĐẶT VÉ SỰ KIỆN ONLINE ║ ╚══════════════════════════════════════════════╝ ┌─────────────────────────────────────────────┐ │ 👤 Họ tên: [Nguyễn Văn A ] * │ │ 📧 Email: [nguyenvana@mail.com ] * │ │ 📅 Ngày sự kiện: [2025-12-25 ] * │ │ (Không được chọn ngày quá khứ) │ │ ⏰ Giờ bắt đầu: [14:00 ] * │ │ (Chỉ từ 09:00 - 18:00) │ │ │ │ 🎟️ Loại vé: │ │ ○ VIP (500,000đ) │ │ ● Standard (200,000đ) ← Selected │ │ ○ Student (100,000đ) │ │ │ │ 🔢 Số lượng vé: [━━━━●━━━━] 5 │ │ Tổng tiền: 1,000,000đ │ │ (Cập nhật realtime khi kéo) │ │ │ │ 📄 Upload thông tin (PDF): │ │ [📁 Choose File] resume.pdf (2.3 MB) │ │ ✅ File hợp lệ │ │ │ │ [🎯 ĐẶT VÉ NGAY] │ └─────────────────────────────────────────────┘ 💾 Form tự động lưu vào LocalStorage!
📋 Yêu cầu HTML:
⚡ JavaScript nâng cao:
✅ Checklist hoàn thành:
💡 Code gợi ý:
// Tính tổng tiền realtime
const prices = { vip: 500000, standard: 200000, student: 100000 };
const quantityInput = document.getElementById('quantity');
const totalDisplay = document.getElementById('total');
function updateTotal() {
const ticketType = document.querySelector('input[name="ticketType"]:checked').value;
const quantity = quantityInput.value;
const total = prices[ticketType] * quantity;
totalDisplay.textContent = total.toLocaleString('vi-VN') + 'đ';
}
quantityInput.addEventListener('input', updateTotal);
document.querySelectorAll('input[name="ticketType"]').forEach(radio => {
radio.addEventListener('change', updateTotal);
});
// File validation
const fileInput = document.getElementById('file');
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
// Check type
if (file.type !== 'application/pdf') {
alert('Chỉ chấp nhận file PDF!');
this.value = '';
return;
}
// Check size (5MB = 5 * 1024 * 1024 bytes)
if (file.size > 5 * 1024 * 1024) {
alert('File không được vượt quá 5MB!');
this.value = '';
return;
}
console.log('File hợp lệ:', file.name);
});
// LocalStorage auto-save
const form = document.getElementById('eventForm');
form.addEventListener('input', function() {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
localStorage.setItem('eventForm', JSON.stringify(data));
console.log('✅ Đã lưu tự động');
});
// Load saved data
window.addEventListener('load', function() {
const saved = localStorage.getItem('eventForm');
if (saved) {
const data = JSON.parse(saved);
Object.keys(data).forEach(key => {
const input = form.elements[key];
if (input) input.value = data[key];
});
updateTotal();
}
});
╔══════════════════════════════════════════════╗ ║ 💳 FORM THANH TOÁN THẺ TÍN DỤNG ║ ╚══════════════════════════════════════════════╝ ┌─────────────────────────────────────────────┐ │ 👤 Tên chủ thẻ: [NGUYEN VAN A ] * │ │ (Tự động chuyển UPPERCASE) │ │ │ │ 💳 Số thẻ: [1234 5678 9012 3456 ] * │ │ (Tự động format 4 số mỗi nhóm) │ │ ✅ Số thẻ hợp lệ (Luhn algorithm) │ │ │ │ 📅 Ngày hết hạn: [2025-12 ] * │ │ ✅ Thẻ còn hiệu lực │ │ │ │ 🔒 CVV: [***] * │ │ (3-4 số, type=password) │ │ │ │ 🎨 Màu thẻ: [🟦] #2196F3 │ │ │ │ ┌──────────── PREVIEW THẺ ────────────┐ │ │ │ ╔════════════════════════════════╗ │ │ │ │ ║ [🟦 BLUE CARD] ║ │ │ │ │ ║ ║ │ │ │ │ ║ 💳 1234 5678 9012 3456 ║ │ │ │ │ ║ ║ │ │ │ │ ║ NGUYEN VAN A 12/25 ║ │ │ │ │ ╚════════════════════════════════╝ │ │ │ └──────────────────────────────────────┘ │ │ │ │ [💰 THANH TOÁN NGAY] │ └─────────────────────────────────────────────┘ ⚡ Real-time validation và preview!
📋 Yêu cầu HTML:
⚡ JavaScript nâng cao:
✅ Checklist hoàn thành:
💡 Code gợi ý:
// Auto-format card number (4 số mỗi nhóm)
const cardInput = document.getElementById('cardNumber');
cardInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\s/g, ''); // Remove spaces
let formatted = value.match(/.{1,4}/g)?.join(' ') || value;
e.target.value = formatted;
// Update preview
document.getElementById('previewNumber').textContent = formatted || '•••• •••• •••• ••••';
});
// Luhn Algorithm - Credit card validation
function validateLuhn(cardNumber) {
const digits = cardNumber.replace(/\s/g, '').split('').reverse();
let sum = 0;
for (let i = 0; i < digits.length; i++) {
let digit = parseInt(digits[i]);
if (i % 2 === 1) { // Every second digit
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
}
return sum % 10 === 0;
}
// CVV validation
const cvvInput = document.getElementById('cvv');
cvvInput.addEventListener('input', function(e) {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 4);
});
// Name uppercase
const nameInput = document.getElementById('cardName');
nameInput.addEventListener('input', function(e) {
e.target.value = e.target.value.toUpperCase();
document.getElementById('previewName').textContent = e.target.value || 'YOUR NAME';
});
// Color picker - Update card preview
const colorInput = document.getElementById('cardColor');
colorInput.addEventListener('input', function(e) {
document.getElementById('cardPreview').style.background =
`linear-gradient(135deg, ${e.target.value} 0%, ${e.target.value}dd 100%)`;
});
// Expiry validation
const expiryInput = document.getElementById('expiry');
expiryInput.addEventListener('change', function(e) {
const selected = new Date(e.target.value + '-01');
const now = new Date();
now.setDate(1); // First day of month
if (selected < now) {
alert('Thẻ đã hết hạn!');
e.target.value = '';
}
});
📝 HTML5 Features:
⚡ JavaScript Advanced:
🎨 UX/UI Features:
💾 Data Management:
🎓 Khi hoàn thành 2 bài tập, bạn đã thành thạo HTML Forms nâng cao!
Những kỹ năng này rất quan trọng trong thực tế và phỏng vấn!
💡 Tip: Tập trung vào validation và UX để tạo forms chuyên nghiệp!