Практика: работа с формами
Валидация в реальном времени
function validateField(input) {
const value = input.value.trim();
const type = input.dataset.validate;
let error = '';
if (input.required && !value) {
error = 'Поле обязательно';
} else if (type === 'email' && !/^\S+@\S+\.\S+$/.test(value)) {
error = 'Некорректный email';
} else if (type === 'phone' && !/^\+?[\d\s()-]{10,}$/.test(value)) {
error = 'Некорректный телефон';
} else if (input.minLength > 0 && value.length < input.minLength) {
error = `Минимум ${input.minLength} символов`;
}
input.classList.toggle('invalid', !!error);
input.setCustomValidity(error);
const errorEl = input.parentElement.querySelector('.error');
if (errorEl) errorEl.textContent = error;
return !error;
}
document.querySelectorAll('input[data-validate]').forEach((input) => {
input.addEventListener('blur', () => validateField(input));
input.addEventListener('input', () => {
if (input.classList.contains('invalid')) validateField(input);
});
});
Валидация формы
form.addEventListener('submit', (event) => {
event.preventDefault();
const inputs = form.querySelectorAll('[data-validate]');
const valid = [...inputs].every(validateField);
if (!valid) {
form.querySelector('.invalid')?.focus();
return;
}
// отправка
});
Отправка с состоянием
form.addEventListener('submit', async (event) => {
event.preventDefault();
const submit = form.querySelector('button[type="submit"]');
submit.disabled = true;
submit.textContent = 'Отправка...';
try {
const data = Object.fromEntries(new FormData(form));
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message);
}
form.reset();
showSuccess('Отправлено');
} catch (error) {
showError(error.message);
} finally {
submit.disabled = false;
submit.textContent = 'Отправить';
}
});
Автосохранение
let saveTimer;
form.addEventListener('input', () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(saveDraft, 1000);
});
async function saveDraft() {
const data = Object.fromEntries(new FormData(form));
localStorage.setItem('draft', JSON.stringify(data));
}
// При загрузке
const draft = localStorage.getItem('draft');
if (draft) {
const data = JSON.parse(draft);
for (const [name, value] of Object.entries(data)) {
const input = form.elements[name];
if (input) input.value = value;
}
}
Счётчик символов
textarea.addEventListener('input', () => {
const max = textarea.maxLength;
const current = textarea.value.length;
counter.textContent = `${current} / ${max}`;
counter.classList.toggle('warning', current > max * 0.9);
});
Зависимые поля
country.addEventListener('change', async () => {
city.innerHTML = '<option>Загрузка...</option>';
const res = await fetch(`/api/cities?country=${country.value}`);
const cities = await res.json();
city.innerHTML = cities
.map((c) => `<option value="${c.id}">${c.name}</option>`)
.join('');
});
Динамические поля
addButton.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'field-row';
row.innerHTML = `
<input name="items[]" placeholder="Название">
<button type="button" class="remove">×</button>
`;
container.append(row);
});
container.addEventListener('click', (event) => {
if (event.target.matches('.remove')) {
event.target.closest('.field-row').remove();
}
});
Загрузка файлов
input.addEventListener('change', () => {
const files = [...input.files];
const preview = files.map((file) => ({
name: file.name,
size: (file.size / 1024).toFixed(1) + ' KB',
url: URL.createObjectURL(file),
}));
renderPreview(preview);
});
Drag and drop файлов
dropZone.addEventListener('dragover', (event) => {
event.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (event) => {
event.preventDefault();
dropZone.classList.remove('dragover');
const files = event.dataTransfer.files;
handleFiles(files);
});
Прогресс загрузки
function upload(file) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
progressBar.style.width = `${percent}%`;
}
});
xhr.addEventListener('load', () => resolve(xhr.response));
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
const data = new FormData();
data.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(data);
});
}
Очистка черновика
form.addEventListener('submit', () => {
localStorage.removeItem('draft');
});
Итоги
- Валидация на
blur и input
- Состояние кнопки при отправке
- Автосохранение через debounce
- Зависимые поля через
change
- Динамические поля с делегированием
- Прогресс загрузки через XHR