Category: guide
HTML Forms & Native Validation
Native HTML5 form validation, Constraint Validation API, custom error messages, และ patterns สำหรับ accessible forms
สารบัญ
HTML5 Validation Attributes
<!-- required — ต้องมีค่า -->
<input type="text" name="name" required />
<!-- type — validate format อัตโนมัติ -->
<input type="email" name="email" /> <!-- ต้องมี @ -->
<input type="url" name="url" /> <!-- ต้องขึ้นต้นด้วย http:// -->
<input type="number" name="age" /> <!-- ตัวเลขเท่านั้น -->
<input type="tel" name="phone" /> <!-- keyboard เปลี่ยน, ไม่ validate format -->
<!-- min / max -->
<input type="number" name="qty" min="1" max="100" />
<input type="date" name="birthday" max="2024-12-31" />
<input type="range" name="rating" min="1" max="5" step="1" />
<!-- minlength / maxlength -->
<input type="text" name="username" minlength="3" maxlength="20" />
<textarea name="bio" minlength="10" maxlength="500"></textarea>
<!-- pattern — regex -->
<input type="text" name="zip" pattern="[0-9]{5}" title="รหัสไปรษณีย์ 5 หลัก" />
<input type="password" name="pw"
pattern="(?=.*[A-Z])(?=.*[0-9]).{8,}"
title="ต้องมีตัวพิมพ์ใหญ่ ตัวเลข และยาวอย่างน้อย 8 ตัว" />
Form Element Types
<form id="register" novalidate> <!-- novalidate: ปิด browser UI แต่ API ยังทำงาน -->
<!-- Text inputs -->
<input type="text" name="name" autocomplete="name" />
<input type="email" name="email" autocomplete="email" />
<input type="password" name="password" autocomplete="new-password" />
<!-- Select -->
<select name="role" required>
<option value="">-- เลือก --</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<!-- Checkboxes -->
<input type="checkbox" name="agree" id="agree" required />
<label for="agree">ยอมรับเงื่อนไข</label>
<!-- Radio group -->
<fieldset>
<legend>ประเภทบัญชี</legend>
<input type="radio" name="type" value="personal" id="personal" required />
<label for="personal">Personal</label>
<input type="radio" name="type" value="business" id="business" />
<label for="business">Business</label>
</fieldset>
<button type="submit">สมัครสมาชิก</button>
</form>
Constraint Validation API
const input = document.querySelector<HTMLInputElement>('input[name="email"]')!;
// Check validity
input.validity.valid // ถูกต้องทุก constraint หรือไม่
input.validity.valueMissing // required แต่ว่าง
input.validity.typeMismatch // type ไม่ตรง (email, url)
input.validity.patternMismatch // pattern ไม่ตรง
input.validity.tooShort // น้อยกว่า minlength
input.validity.tooLong // มากกว่า maxlength
input.validity.rangeUnderflow // น้อยกว่า min
input.validity.rangeOverflow // มากกว่า max
// Error message จาก browser
input.validationMessage // 'Please fill out this field.'
// Custom error message
input.setCustomValidity('Email นี้ถูกใช้งานแล้ว');
input.reportValidity(); // แสดง error bubble
// ล้าง custom error
input.setCustomValidity('');
// Check element
input.checkValidity(); // boolean, ไม่แสดง UI
// Check form ทั้งหมด
const form = document.querySelector<HTMLFormElement>('form')!;
form.checkValidity(); // boolean
form.reportValidity(); // boolean + แสดง UI
Custom Validation UI
const form = document.querySelector<HTMLFormElement>('#register')!;
form.addEventListener('submit', (event) => {
event.preventDefault();
if (!form.checkValidity()) {
// แสดง error สำหรับทุก invalid fields
const invalidFields = form.querySelectorAll<HTMLInputElement>(':invalid');
invalidFields.forEach((field) => showError(field));
invalidFields[0].focus();
return;
}
// submit...
});
// Validate ทุก blur
form.addEventListener('focusout', (event) => {
const field = event.target as HTMLInputElement;
if (field.tagName === 'INPUT' || field.tagName === 'SELECT') {
if (!field.validity.valid) showError(field);
else clearError(field);
}
});
function showError(field: HTMLInputElement) {
const errorEl = field.parentElement?.querySelector('.field-error');
if (errorEl) {
errorEl.textContent = getErrorMessage(field);
errorEl.removeAttribute('hidden');
}
field.setAttribute('aria-invalid', 'true');
}
function clearError(field: HTMLInputElement) {
const errorEl = field.parentElement?.querySelector('.field-error');
if (errorEl) errorEl.setAttribute('hidden', '');
field.removeAttribute('aria-invalid');
}
function getErrorMessage(field: HTMLInputElement): string {
if (field.validity.valueMissing) return 'กรุณากรอกข้อมูลนี้';
if (field.validity.typeMismatch) {
if (field.type === 'email') return 'รูปแบบอีเมลไม่ถูกต้อง';
if (field.type === 'url') return 'รูปแบบ URL ไม่ถูกต้อง';
}
if (field.validity.tooShort) return `ต้องมีอย่างน้อย ${field.minLength} ตัวอักษร`;
if (field.validity.patternMismatch) return field.title || 'รูปแบบไม่ถูกต้อง';
return field.validationMessage;
}
Accessible Form Pattern
<!-- ✓ Label ผูกกับ input ด้วย for/id เสมอ -->
<div class="field">
<label for="email" class="field-label">
อีเมล
<span class="required-mark" aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
aria-describedby="email-hint email-error"
aria-required="true"
/>
<p id="email-hint" class="field-hint">เราจะส่ง OTP ไปยังอีเมลนี้</p>
<p id="email-error" class="field-error" hidden aria-live="polite"></p>
</div>
/* Visual invalid state */
input:invalid:not(:placeholder-shown) {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.03);
}
/* ✓ ใช้ :user-invalid แทน :invalid เพื่อ avoid flash ก่อน user touch */
input:user-invalid {
border-color: #ef4444;
}
/* Focus state ต้อง visible */
input:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
Server-side Validation Match
// ✓ Client validation = UX ปรับปรุง
// ✓ Server validation = Security ต้องมีเสมอ
// Pattern: validate client + show server errors
async function handleSubmit(formData: FormData) {
const res = await fetch('/api/register', {
method: 'POST',
body: formData,
});
if (!res.ok) {
const { errors } = await res.json();
// errors = { email: 'อีเมลนี้ถูกใช้งานแล้ว', ... }
for (const [field, message] of Object.entries(errors)) {
const input = form.querySelector<HTMLInputElement>(`[name="${field}"]`);
if (input) {
input.setCustomValidity(message as string);
showError(input);
// ล้าง custom validity เมื่อ user แก้ไข
input.addEventListener('input', () => {
input.setCustomValidity('');
clearError(input);
}, { once: true });
}
}
}
}