Функционально приложение уже работает. Осталось довести UX до уровня, на котором не стыдно показать кому-то. Это всё мелочи, но их сумма — разница между «хм, это что-то странное» и «нормальное приложение».
Индикатор первичной загрузки
Когда страница только открылась, список ещё не пришёл. Пользователь видит пустоту. Простая статусная строка «Загружаю...» покрывает 90% случаев — уже сделана в loadTodos из 9.2. Можно сделать чуть приятнее: добавить «скелетон» — серые плейсхолдеры в форме итоговых элементов.
function showLoadingSkeleton() {
listEl.innerHTML = Array.from({ length: 5 }, () => `
<li class="skeleton">
<div class="skel-checkbox"></div>
<div class="skel-text"></div>
</li>
`).join('');
}
В CSS добавляем серые блоки с лёгкой анимацией мерцания — пользователь сразу понимает, что что-то грузится, и видит ожидаемую структуру.
Сообщения об ошибках
В предыдущих главах мы писали ошибки прямо в statusEl. Это работает, но у нас все сообщения сливаются с «Нет задач» и «Загружаю». Сделаем отдельный визуальный приём для ошибок:
function showError(message) {
statusEl.textContent = message;
statusEl.classList.add('error');
setTimeout(() => {
statusEl.classList.remove('error');
statusEl.textContent = '';
}, 4000);
}
В CSS — красный фон, белый текст, лёгкий padding. Через четыре секунды само исчезает.
Все statusEl.textContent = '...' на ошибках из 9.2–9.3 меняем на showError('...').
Disabled во время отправки
Чтобы пользователь не дважды добавил одну и ту же задачу, форма блокируется на время запроса. Уже было в демке из 5.2 — повторим тот же приём:
formEl.addEventListener('submit', async (e) => {
e.preventDefault();
const title = inputEl.value.trim();
if (!title) return;
const submitBtn = formEl.querySelector('button[type="submit"]');
submitBtn.disabled = true;
// ...остальной код добавления...
submitBtn.disabled = false;
});
Для оптимистичного UI блокировка нужна меньше (UI уже обновился), но без неё пользователь может за секунду нажать «Добавить» десять раз и создать дубли. С блокировкой — нет.
Сокращаем тройную нажатую задачу
Если пользователь напечатал что-то и быстро трижды нажал кнопку — даже с submitBtn.disabled = true между нажатиями могут пройти миллисекунды. Защита — очистка инпута первой строкой:
const title = inputEl.value.trim();
if (!title) return;
inputEl.value = ''; // ← мгновенно
// дальше — добавление, async/await
Пустой инпут — следующий trim() вернёт пустую строку, и обработчик выйдет на ранний return.
Что включает финальная демка
Всё, что мы написали в этом модуле, плюс несколько мелочей:
- Скелетон при первичной загрузке.
- Пустое состояние «Нет задач».
- Disabled на кнопке отправки во время запроса.
- Красная плашка с автоисчезновением для ошибок.
- Оптимистичные добавление, удаление, переключение с откатом.
- Защита от XSS через escapeHtml.
Что мы сделали
За один модуль написали маленькое, но настоящее веб-приложение, в котором задействованы:
- Модуль 3 — четыре HTTP-метода (GET, POST, PATCH, DELETE).
- Модуль 4 — fetch и async/await.
- Модуль 5 — query-параметры (?_limit=10), POST-тело с JSON, PATCH с одним полем.
- Модуль 6 — обёртка api() с проверкой response.ok, обработка ошибок, защита от 204 No Content.
Это и есть REST-фронт в миниатюре. Дальше можно бесконечно дорабатывать — добавить роуты, фильтры, drag’n’drop, синхронизацию между вкладками. Но фундамент — вот он.