Практическая работа к уроку 6 (занятие 2)

Сервисы, модель Task и DI

Выделите состояние задач в сервис-хранилище, опишите модели данных и подключите их через Dependency Injection.

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

Коротко о главном. Представьте, что вы строите приложение-список дел (TaskFlow) вроде записной книжки задач.

Задание 1. Модели данных

Контекст. Прежде чем писать код, договоримся, из каких частей состоит задача. Эти «чертежи» помогают Angular понимать, какие данные вы храните, и подсказывать ошибки заранее.

Аналогия. Модель — это анкета. Task — как готовый билет: у него есть свой номер (id), название, отметка completed («сделано ли») и важность. А TaskDraft — это черновик анкеты без номера: вы ещё не отдали его, поэтому номер сервер выдаст сам позже.
interface Task {
  id: number;            // номер задачи (его выдаёт сервер)
  title: string;         // название задачи
  completed: boolean;    // true = сделано, false = ещё нет
  priority: 'low' | 'medium' | 'high';  // важность
}

// Черновик — без id, его выдаёт сервер
// Omit<Task, 'id'> значит: «возьми всё из Task, но убери поле id»
type TaskDraft = Omit<Task, 'id'>;
Пошаговый разбор:
  1. interface Task — описываем, из чего состоит готовая задача.
  2. completed: boolean — поле статуса. Именно completed, а не done (слово done мы оставим только для фильтра «показать выполненные»).
  3. type TaskDraft = Omit<Task, 'id'> — делаем «урезанную» копию без номера. Когда пользователь создаёт задачу, номер ей присвоит сервер.
Что увидит студент. Пока ничего на экране — это просто описание данных. Но теперь Angular «знает» структуру задачи, и при ошибке подсветит её красным.

Задание 2. Сервис-хранилище

Контекст. Место, где живёт список задач и правила его изменения. Компоненты (страницы) будут обращаться сюда, а не хранить задачи у себя.

Аналогия из жизни. Сервис — это общая кофеварка на этаже. Все сотрудники пользуются одной кофеваркой (providedIn: 'root' — значит «поставь её в холле, видно всем»). Внутри — закрытый бак с кофе (приватный signal): открыть его руками нельзя, но есть кнопки «налить» и «включить» (методы). Снаружи — только смотровое окно (asReadonly()) и табло со счётчиком чашек (computed).
import { Injectable, signal, computed, inject } from '@angular/core';

// @Injectable + providedIn: 'root' = «одна кофеварка на этаже»
@Injectable({ providedIn: 'root' })
export class TaskService {
  // Приватный сигнал — закрытый бак. Менять можно только изнутри класса.
  private readonly _tasks = signal<Task[]>([
    { id: 1, title: "Создать сервис", completed: true,  priority: "high" },
    { id: 2, title: "Подключить DI",  completed: false, priority: "low" }
  ]);

  // Наружу — только для чтения (смотровое окно)
  readonly tasks = this._tasks.asReadonly();

  // computed — табло, пересчитывается само при изменении списка
  readonly doneCount = computed(() =>
    this._tasks().filter(t => t.completed).length
  );

  // Метод: переключить статус задачи (сделано <-> не сделано)
  toggle(id: number) {
    this._tasks.update(list =>
      list.map(t =>
        t.id === id ? { ...t, completed: !t.completed } : t
      )
    );
  }

  // Метод: добавить новую задачу из черновика
  add(draft: TaskDraft) {
    const nextId = Math.max(0, ...this._tasks().map(t => t.id)) + 1;
    this._tasks.update(list => [...list, { id: nextId, ...draft }]);
  }
}
Пошаговый разбор:
  1. @Injectable({ providedIn: 'root' }) — Angular создаёт один экземпляр сервиса на всё приложение.
  2. private _tasks = signal(...) — список задач лежит внутри, снаружи его не перезаписать.
  3. tasks = asReadonly() — компоненты читают, но не портят данные.
  4. doneCount — считает, сколько задач с completed: true. Пересчёт идёт автоматически.
  5. toggle(id) — находит задачу по id и переворачивает её completed.
  6. add(draft) — берёт черновик без номера, сам вычисляет новый id и добавляет в список.
Почему asReadonly(). Компоненты видят данные, но не могут напрямую перезаписать сигнал — меняют состояние только через методы сервиса (как кофе наливают через кнопку, а не лезут в бак).
Что увидит студент. Сам по себе этот код не рисует ничего. Но теперь у вас есть «кофеварка», из которой страницы будут брать задачи.

Задание 3. Получение через inject()

Контекст. Страница (компонент) хочет показать задачи. Ей нужен сервис — и вот тут срабатывает Dependency Injection.

Аналогия из жизни. DI (внедрение зависимости) — это когда вам вместо «иди в кладовку, собери всё сам и подключи» кто-то приносит готовый инструмент на стол. Метод inject(TaskService) говорит: «дай мне сервис задач, я знаю, как с ним работать». Регистрировать его вручную не нужно.
@Component({
  selector: 'app-task-page',
  template: `
    <p>Выполнено: {{ service.doneCount() }}</p>
    @for (task of service.tasks(); track task.id) {
      <app-task-item [task]="task"
        (toggle)="service.toggle($event)" />
    }
  `
})
export class TaskPage {
  // inject() — нам «приносят» готовый сервис, регистрировать не надо
  protected readonly service = inject(TaskService);
}
Пошаговый разбор:
  1. inject(TaskService) — Angular сам находит сервис (тот самый, с providedIn: 'root') и отдаёт его.
  2. В шаблоне service.doneCount() показывает число выполненных, а @for рисует каждую задачу списком.
  3. Кнопка у задачи вызывает service.toggle(...) — то есть меняет состояние через метод сервиса.
Что увидит пользователь. Страница со списком задач и надписью «Выполнено: N». Нажимаешь галочку — счётчик меняется.
💡 Запомните. completed — это поле задачи (сделана или нет). А слово done мы используем только как значение фильтра, например 'all' | 'active' | 'done' — «показать все / активные / выполненные». Путать их не надо.

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

Контекст. То же самое хранилище задач, но на чистом JavaScript — чтобы вы потрогали логику руками, без Angular. Здесь «сервис» — это объект с приватным состоянием и публичными методами.

Аналогия из жизни. Это та же кофеварка, только собранная из подручных деталей. Внутри — закрытый список задач, снаружи — кнопки toggle (переключить) и add (добавить). Нажимаете — и «табло» перерисовывается.
Песочница: нажми «Запустить» — и увидишь список задач, счётчик «Выполнено» и кнопки переключения. Меняй код и запускай снова.
▶️ Песочница: сервис задач
Пошаговый разбор песочницы:
  1. Переменная tasks спрятана внутри функции — снаружи её не изменить напрямую.
  2. doneCount() считает задачи с completed: true.
  3. toggle(id) меняет статус и перерисовывает экран через render().
  4. add(draft) берёт черновик без id и добавляет новую задачу.
Что увидит студент. Список из двух задач, счётчик «Выполнено: 1», кнопки-галочки и кнопка «Добавить». Жмите — список меняется на лету.
💡 Попробуйте сами. В песочнице поменяйте completed: true на false у первой задачи и нажмите «Запустить» — счётчик станет 0.

Проверьте себя

Вопросы.
  1. Зачем состояние делать приватным, а не публичным сигналом?
  2. Чем providedIn: 'root' удобнее ручного провайдера?
  3. Что такое TaskDraft и почему в нём нет id?

Практика к уроку 6. Далее: Практика к уроку 7 →

Куда дальше: теория сервисов — в Уроке 6; модели данных — в лабораторной №3.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
СервисОбщая кофеварка на этажеМесто, где живёт список задач и правила его изменения — одно на всё приложение.
@InjectableПаспорт кофеварки: «поставь меня в холле»Пометка, что Angular может создать сервис один раз и отдавать его всем.
DI (внедрение зависимостей)Кто-то приносит готовый инструмент на столМетод inject() сам отдаёт сервис — регистрировать руками не нужно.
Локальное состояниеЛипкий листок у тебя на столеДанные живут только внутри одного компонента и не видны остальным.
Серверное состояниеДоска объявленийОбщие данные, которые приходят с сервера и видны всем пользователям.
TaskГотовый билет с номеромЗадача с id и полем completed (сделана или нет).
TaskDraftЧерновик анкеты без номераЗадача без id — номер ей выдаст сервер при сохранении.

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

1. Сервис с providedIn: 'root' создаётся один раз на всё приложение. Верно или неверно?

2. Метод inject() нужно регистрировать вручную, прежде чем использовать. Верно или неверно?

3. В модели TaskDraft уже есть поле id. Верно или неверно?

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