В этой главе появляются первые две функции: одна тянет список с сервера, другая рисует его в HTML. После них приложение уже что-то делает на экране.
Получаем задачи
async function loadTodos() {
const data = await api('/todos?_limit=10');
todos = data;
render();
}
Используем нашу обёртку api() из главы 9.1. Параметр _limit=10 — query-параметр JSONPlaceholder, ограничивающий количество. Без него вернётся 200 задач, нам столько не нужно.
В реальной жизни тут была бы пагинация (см. 5.1), но для мини-проекта ограничения хватит.
Рисуем список
const listEl = document.querySelector('#list');
const statusEl = document.querySelector('#status');
function render() {
if (todos.length === 0) {
listEl.innerHTML = '';
statusEl.textContent = 'Нет задач — добавьте первую.';
return;
}
listEl.innerHTML = todos.map(t => `
<li data-id="${t.id}" class="${t.completed ? 'done' : ''}">
<input type="checkbox" ${t.completed ? 'checked' : ''} />
<span>${escapeHtml(t.title)}</span>
<button type="button" class="remove" aria-label="Удалить">×</button>
</li>
`).join('');
statusEl.textContent = '';
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
Что важно в этой функции:
- Атрибут data-id — чтобы по клику на чекбокс или кнопку удаления знать, какая задача обрабатывается.
- Экранирование escapeHtml — защита от XSS. Сейчас задачи приходят с JSONPlaceholder, риска нет; но привычка экранировать пользовательский ввод спасает от уязвимостей в будущем.
- Полный re-render на каждое изменение. Неэффективно для тысяч задач, но для нашей сотни — норма и упрощает код.
Делегирование событий
Чекбоксы и кнопки удаления у нас динамические — рендерятся каждый раз заново. Вешать обработчики после каждого render() неудобно. Вместо этого — один обработчик на родительский <ul>, который смотрит, куда конкретно кликнули:
listEl.addEventListener('click', async (e) => {
const li = e.target.closest('li');
if (!li) return;
const id = Number(li.dataset.id);
if (e.target.classList.contains('remove')) {
await deleteTodo(id);
} else if (e.target.matches('input[type="checkbox"]')) {
await toggleTodo(id, e.target.checked);
}
});
Функции deleteTodo и toggleTodo мы напишем в следующей главе. e.target.closest('li') идёт вверх по DOM от точки клика и находит ближайший <li> — полезный паттерн для делегированных обработчиков.
Старт приложения
На запуске страницы один вызов:
loadTodos().catch(error => {
statusEl.textContent = 'Не удалось загрузить: ' + error.message;
});
Если сеть отвалилась или сервер ответил с ошибкой — покажем сообщение прямо в статусной строке. Подробнее про обработку ошибок — в главе 9.4.
Что получилось
На этом моменте в приложении:
- при загрузке страницы тянется 10 задач;
- они рендерятся в список с чекбоксами и кнопками удаления;
- чекбоксы и кнопки уже «слышат» клики — но пока ничего не делают, потому что deleteTodo и toggleTodo ещё не написаны.
В следующей главе подключаем POST и DELETE — и приложение начинает реагировать на действия пользователя.