Formuláře a validace v HTML a JavaScriptu bez zbytečných knihoven
Nativní HTML validace, Constraint Validation API, vlastní chybové hlášky, přístupné chyby a proč musíte vždy validovat i na serveru.
Dobře udělaný formulář uživatele neotravuje, srozumitelně řekne, co je špatně, a funguje i na mobilu. Velkou část práce přitom zvládne samotné HTML. JavaScript přidáte jen tam, kde nativní chování nestačí.
Začněte správnými typy polí
<form id="signup" novalidate>
<label for="email">E-mail</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label for="phone">Telefon</label>
<input id="phone" name="phone" type="tel" autocomplete="tel"
pattern="(\+420)? ?[0-9]{3} ?[0-9]{3} ?[0-9]{3}">
<label for="age">Věk</label>
<input id="age" name="age" type="number" min="15" max="120" inputmode="numeric">
<label for="password">Heslo</label>
<input id="password" name="password" type="password"
autocomplete="new-password" minlength="10" required>
<button type="submit">Registrovat</button>
</form>
Co tím získáte zadarmo:
type="email"/tel/number– mobil ukáže vhodnou klávesnici,autocomplete– prohlížeč a správce hesel vyplní údaje (seznam hodnot je ve specifikaci HTML:email,tel,given-name,postal-code,new-password,one-time-code…),required,minlength,min,max,pattern– validace bez řádku JavaScriptu.
Každé pole potřebuje <label> propojený přes for/id. Placeholder label nenahrazuje – zmizí při psaní a čtečky obrazovky ho nemusí přečíst.
Proč novalidate?
Nativní bubliny s chybami vypadají v každém prohlížeči jinak a nedají se stylovat. Atribut novalidate vypne jejich automatické zobrazení, ale pravidla zůstávají aktivní – můžeme se jich ptát z JavaScriptu přes Constraint Validation API a chyby zobrazit po svém.
Constraint Validation API
Každé pole má vlastnost validity s přehledem, co je špatně:
| Vlastnost | Kdy je true |
|---|---|
valueMissing |
prázdné povinné pole |
typeMismatch |
neplatný e-mail nebo URL |
patternMismatch |
neodpovídá pattern |
tooShort / tooLong |
porušení minlength / maxlength |
rangeUnderflow / rangeOverflow |
menší než min / větší než max |
customError |
nastaveno přes setCustomValidity() |
valid |
vše v pořádku |
Vlastní hlášky v češtině
const form = document.querySelector('#signup');
const messages = {
valueMissing: 'Toto pole je povinné.',
typeMismatch: 'Zadejte platný e-mail.',
patternMismatch: 'Zadejte telefon ve tvaru 777 123 456.',
tooShort: f => `Zadejte alespoň ${f.minLength} znaků.`,
rangeUnderflow: f => `Minimální hodnota je ${f.min}.`,
rangeOverflow: f => `Maximální hodnota je ${f.max}.`,
};
function getMessage(field) {
for (const key in messages) {
if (field.validity[key]) {
const m = messages[key];
return typeof m === 'function' ? m(field) : m;
}
}
return field.validationMessage; // záložní text prohlížeče
}
function showError(field) {
const errorId = `${field.id}-error`;
let error = document.getElementById(errorId);
if (!error) {
error = document.createElement('p');
error.id = errorId;
error.className = 'field-error';
field.insertAdjacentElement('afterend', error);
}
if (field.validity.valid) {
error.textContent = '';
field.removeAttribute('aria-invalid');
field.removeAttribute('aria-describedby');
} else {
error.textContent = getMessage(field);
field.setAttribute('aria-invalid', 'true');
field.setAttribute('aria-describedby', errorId);
}
}
Atributy aria-invalid a aria-describedby zajistí, že čtečka obrazovky u pole přečte i chybovou hlášku. Text chyby vkládejte přes textContent, ne innerHTML.
Kdy validovat
Validace při každém stisku klávesy je otravná – uživatel ještě nedopsal e-mail a už vidí chybu. Osvědčený postup:
- první kontrola při opuštění pole (
blur), - jakmile pole jednou ukázalo chybu, kontrolovat průběžně při psaní, aby chyba zmizela hned po opravě,
- kontrola všeho při odeslání.
form.addEventListener('blur', e => {
if (e.target.matches('input, select, textarea')) {
e.target.dataset.touched = 'true';
showError(e.target);
}
}, true); // blur nebublá, proto capture fáze
form.addEventListener('input', e => {
if (e.target.dataset.touched) showError(e.target);
});
form.addEventListener('submit', async e => {
e.preventDefault();
const fields = [...form.elements].filter(el => el.willValidate);
fields.forEach(showError);
const firstInvalid = fields.find(f => !f.validity.valid);
if (firstInvalid) {
firstInvalid.focus();
return;
}
await submit(new FormData(form));
});
Po neúspěšném odeslání přesuňte fokus na první chybné pole – uživatel s klávesnicí nebo čtečkou jinak neví, kde je problém.
Vlastní pravidla: potvrzení hesla
const confirm = document.querySelector('#password-confirm');
const password = document.querySelector('#password');
function checkMatch() {
confirm.setCustomValidity(
confirm.value && confirm.value !== password.value ? 'Hesla se neshodují.' : ''
);
}
password.addEventListener('input', checkMatch);
confirm.addEventListener('input', checkMatch);
Nezapomeňte nastavit prázdný řetězec, když je hodnota v pořádku – jinak pole zůstane navždy nevalidní. Do getMessage pak přidejte i customError: f => f.validationMessage.
Stylování podle stavu
input:user-invalid {
border-color: #c0392b;
}
.field-error {
color: #c0392b;
font-size: 0.875rem;
min-height: 1.25em; /* rezervuje místo, formulář neposkakuje */
}
Pseudotřída :user-invalid (podporovaná v aktuálních prohlížečích) se na rozdíl od :invalid aktivuje až po interakci uživatele, takže prázdný formulář nesvítí červeně hned po načtení.
Odeslání a ochrana proti dvojkliku
async function submit(data) {
const button = form.querySelector('[type="submit"]');
button.disabled = true;
try {
const res = await fetch('/api/signup', { method: 'POST', body: data });
if (!res.ok) throw new Error();
form.reset();
} catch {
alert('Odeslání se nepovedlo, zkuste to znovu.');
} finally {
button.disabled = false;
}
}
Server validuje vždy
Validace v prohlížeči je pohodlí pro uživatele, ne zabezpečení. Kdokoli ji obejde pomocí DevTools nebo pošle požadavek přímo přes curl. Server musí stejná pravidla zkontrolovat znovu a chyby vrátit ve strukturované podobě (např. { "errors": { "email": "Tento e-mail už je registrovaný." } }), aby je frontend mohl zobrazit u správných polí.
Ve větších React aplikacích se osvědčuje kombinace React Hook Form a schématu v knihovně Zod – stejné schéma můžete použít na klientu i na serveru.