Практическая лаборатория 2 из 5

Три состояния UI

Интерфейс не просто «есть» или «нет». Он находится в одном из нескольких состояний: загружается, показывает данные или сообщает об ошибке. В этой лаборатории мы построим список задач, который умеет показывать все три ситуации.

Что вы узнаете

Что такое «состояние» интерфейса

Аналогия из жизни. Представьте статус посылки в интернет-магазине. Посылка может быть в трёх положениях: «в пути» (ещё едет), «доставлена» (вы её получили) или «потеряна» (произошла ошибка). В каждом случае вы видите разную картинку на сайте.

Суть. Состояние (state) — это текущее положение дел в программе. От него зависит, что именно видит пользователь на экране. Меняется состояние — меняется и интерфейс.

В нашем списке задач (он называется TaskFlow) всё точно так же: пока данные грузятся — показываем «Загрузка…», когда пришли — показываем список, если что-то сломалось — показываем ошибку и кнопку «Повторить».

Схема состояний

Контекст. Это «карта» нашей программы. Она показывает, в каких состояниях может находиться экран и как из одного состояния можно перейти в другое.

Аналогия из жизни. Это как правила доставки посылки: из статуса «в пути» она может прийти в «доставлена» или «потеряна», а из «потеряна» — снова отправиться в «в пути» (повторная попытка). Автомат состояний (state machine) — это просто правило: «в какой момент какой экран показывать».

idle → loading → success
             → error → loading (повтор)

Пошаговый разбор схемы:

Что увидит студент. На бумаге — стрелочную схему выше. В браузере — тот экран, который соответствует текущему состоянию.

Пошаговая инструкция

Шаг 1. Создайте файл

✍️ Действие: создайте файл lab2.html рядом с предыдущим (в той же папке, где лежат ваши лабораторные).

Зачем. В этом файле мы напишем весь код списка задач целиком — и HTML, и стили, и логику. Это самодостаточная страничка, которую можно просто открыть в браузере.

Шаг 2. Вставьте код

Контекст. Ниже — полный код страницы lab2.html. Он строит список задач TaskFlow и умеет показывать три состояния: загрузка, успех и ошибка. Весь «мозг» программы — в функции render(), которая рисует экран в зависимости от переменной status.

Аналогия из жизни. Переменная status — это текущий статус заказа: «в работе» (loading) / «готов» (success) / «ошибка» (error). В зависимости от статуса посетитель (пользователь) видит разную картину. Функция render() — это работник, который, глядя на статус, расставляет нужные вещи на витрине.

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Лаборатория 2: три состояния</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px;
           margin: 40px auto; padding: 20px; }
    .btn { padding: 10px 20px; font-size: 15px; cursor: pointer;
           border: none; border-radius: 6px; color: white; margin-top: 10px; }
    .btn-primary { background: #2d5bff; }
    .btn-primary:hover { background: #1e4ad0; }
    .btn-secondary { background: #6c757d; }
    .status-loading { color: #2d5bff; font-weight: bold; }
    .status-error { color: #dc3545; }
    .status-success { color: #159957; font-weight: bold; }
    ul { padding-left: 20px; line-height: 1.8; }
  </style>
</head>
<body>
  <!-- div с id="app" — это «пустая витрина». Весь интерфейс мы будем
       рисовать внутри неё из JavaScript. -->
  <div id="app"></div>

  <script>
    // Три переменные, которые помнят текущий статус заказа:
    let status = 'idle';   // текущее состояние: idle / loading / success / error
    let tasks = [];        // сюда положим загруженный список задач
    let error = null;      // текст ошибки (пусто, если всё хорошо)

    // Главная функция рисования. Она смотрит на status и решает,
    // что именно показать пользователю.
    function render() {
      // document.getElementById('app') — находим нашу «витрину» в HTML.
      // DOM (объектная модель документа) — это дерево страницы,
      // как скелет дома: есть главный этаж и комнаты-ветки.
      const app = document.getElementById('app');
      app.innerHTML = ''; // очищаем витрину перед новой отрисовкой

      if (status === 'idle') {
        // Покой: показываем только кнопку «Загрузить задачи».
        const btn = document.createElement('button');
        btn.className = 'btn btn-primary';
        btn.textContent = 'Загрузить задачи';
        btn.onclick = handleLoad; // по клику запускаем загрузку
        app.appendChild(btn);
      }
      else if (status === 'loading') {
        // Идёт загрузка: синяя надпись «Загрузка данных...».
        const p = document.createElement('p');
        p.className = 'status-loading';
        p.textContent = 'Загрузка данных...';
        app.appendChild(p);
      }
      else if (status === 'error') {
        // Ошибка: красная надпись и кнопка «Повторить».
        const p = document.createElement('p');
        p.className = 'status-error';
        p.textContent = 'Ошибка: ' + error;
        app.appendChild(p);

        const btn = document.createElement('button');
        btn.className = 'btn btn-primary';
        btn.textContent = 'Повторить';
        btn.onclick = handleLoad; // снова пытаемся загрузить
        app.appendChild(btn);
      }
      else if (status === 'success') {
        // Успех: зелёная надпись, список задач и кнопка «Сбросить».
        const p = document.createElement('p');
        p.className = 'status-success';
        p.textContent = 'Задачи загружены!';
        app.appendChild(p);

        // Перебираем каждую задачу и делаем из неё пункт списка <li>.
        const ul = document.createElement('ul');
        tasks.forEach(function(t) {
          const li = document.createElement('li');
          li.textContent = t.title;
          ul.appendChild(li);
        });
        app.appendChild(ul);

        const btn = document.createElement('button');
        btn.className = 'btn btn-secondary';
        btn.textContent = 'Сбросить';
        // Сброс возвращает всё в начальное состояние idle.
        btn.onclick = function() { status = 'idle'; tasks = []; render(); };
        app.appendChild(btn);
      }
    }

    // Асинхронная функция загрузки. Слово async значит:
    // внутри может быть «пауза» (ожидание), не блокируя страницу.
    async function handleLoad() {
      status = 'loading'; // меняем статус заказа на «в пути» (loading)
      error = null;
      render();           // перерисовываем экран сразу

      try {
        // Имитируем запрос на сервер паузой в 1,5 секунды.
        // HTTP-запрос — это как заказ в магазине: послали заявку, ждём посылку.
        await new Promise(function(r) { setTimeout(r, 1500); });
        tasks = [
          { id: 1, title: 'Изучить state machine' },
          { id: 2, title: 'Реализовать idle → loading → success' },
          { id: 3, title: 'Добавить обработку ошибки' }
        ];
        // Чтобы увидеть состояние ошибки, раскомментируйте следующую строку:
        // throw new Error('Симуляция ошибки загрузки');
        status = 'success'; // данные пришли — переключаем на успех
      } catch (err) {
        // Сюда попадаем, если выше случилась ошибка (или раскомментировали throw).
        error = err.message;
        status = 'error';   // меняем статус заказа на «ошибка»
      }
      render(); // перерисовываем экран с новым состоянием
    }

    render(); // первый запуск: покажем начальный экран (idle)
  </script>
</body>
</html>

Пошаговый разбор того, как работает этот код:

💡 Инсайт: обратите внимание — весь интерфейс рисуется ОДНОЙ функцией render(). Мы нигде не прячем и не показываем куски вручную: меняем только status, а render() сам решает, что отрисовать. Это и есть суть реактивного UI.

Шаг 3. Откройте и нажмите кнопку

Откройте файл lab2.html в браузере (двойной клик по файлу). Нажмите «Загрузить задачи».

Что увидит студент. Сначала появится синяя надпись «Загрузка данных…», а через полторы секунды — зелёная «Задачи загружены!» и список из трёх пунктов.

Шаг 4. Проверьте ошибку

Найдите в коде закомментированную строку с throw new Error и раскомментируйте её (уберите два слэша // в начале). Обновите страницу и снова нажмите кнопку.

Что увидит студент. Вместо списка появится красная надпись «Ошибка: Симуляция ошибки загрузки» и синяя кнопка «Повторить». Нажав её, можно снова попасть в состояние загрузки.

Подсказка. Строка для раскомментирования находится внутри функции handleLoad, перед status = 'success'. Если ошибка вам надоест — снова поставьте // перед throw.

Связь с Angular и React

Аналогия из жизни. Представьте, что вы строите дом. Идея одна — нужны стены, крыша, окна. Но инструменты разные: один мастер кладёт кирпич мастерком, другой — специальным пистолетом. Результат один и тот же дом, но способы разные. Так же и тут: идея «UI зависит от состояния» одна, а фреймворки дают разные инструменты.

КонцепцияВ ReactВ Angular
Хранение состоянияuseStatesignal()
Условный рендеринг{status === 'loading' && ...}@if (status() === 'loading') { ... }
Изменение состоянияsetStatus('loading')this.status.set('loading')

Сама идея одна: UI зависит от состояния. Фреймворк лишь предоставляет инструменты для хранения и отображения. В нашей лаборатории мы обошлись чистым JavaScript, чтобы вы увидели эту механику «без обёрток».

Интерактивная песочница

▶️ Песочница: здесь можно поиграть с кодом прямо на странице. Нажмите «Запустить» — и результат появится справа.

Контекст. В песочнице — упрощённая версия той же машины состояний. Она умеет показывать только три состояния: idle, loading и success. Попробуйте изменить начальное значение status или текст внутри app.innerHTML и запустите снова.

Аналогия из жизни. Это как смена статуса заказа: вы меняете статус посылки (например, «в пути» → «доставлена») — и у покупателя на сайте сразу меняется вся картинка. Здесь меняете состояние — и экран показывает другое.

Песочница: нажми «Запустить» — и увидишь маленькую страничку со кнопкой «Загрузить». Меняй код и запускай снова.
Песочница: машина состояний

Что увидит студент. Справа откроется маленькая страничка. В начале — кнопка «Загрузить». Нажмите её: появится «Загрузка...», затем «Готово!» и список из трёх задач. 💡 Попробуйте сами заменить let status = 'idle' на let status = 'loading' и запустите — увидите, как сразу меняется стартовый экран.

Лаборатория 2 из 5. ← Лаборатория 1 | Лаборатория 3 →

Куда дальше: теория состояния — в Уроке 5 (сигналы и состояние), практика — практика к уроку 5.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
Состояние (state)Статус заказа: «в обработке / готов / ошибка»Текущее положение дел, от которого зависит экран.
loading (загрузка)Посылка «в пути»Данные ещё едут — показываем индикатор.
empty (пусто)Посылка «доставлена», но коробка пустаДанных нет — показываем заглушку «ничего нет».
error (ошибка)Посылка «потеряна»Что-то сломалось — показываем ошибку и «Повторить».
Сигнал (signal)Группа в мессенджереПеременная с подписчиками: изменилось значение — все получили сообщение.
Signal (Angular)Та же группа в мессенджереСпособ хранить состояние в Angular: this.status.set('loading').
Условный рендерингВывеска, зависящая от статуса заказаПоказываем разные блоки в зависимости от состояния.

🧠 Самопроверка

1. Весь интерфейс в этой лаборатории рисуется одной функцией render(). Верно или неверно?

2. Если в коде раскомментировать строку с throw new Error, программа попадёт в состояние error. Верно или неверно?

3. Сигнал в Angular похож на группу в мессенджере: при изменении значения все подписчики узнают об этом. Верно или неверно?

Прогресс курса