Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Сервисы и внедрение зависимостей

Сервис — это обычный класс с логикой, которая нужна нескольким компонентам: хранение списка задач, загрузка данных с сервера, работа с localStorage. Компонент не создаёт сервис сам через new — он просит его у Angular, и Angular выдаёт готовый объект. Такой механизм называется внедрением зависимостей (dependency injection, DI).

Создание сервиса

ng generate service todo-store

CLI создаст файл src/app/todo-store.ts с классом TodoStore (в версиях до 20 — todo-store.service.ts и класс TodoStoreService). Перенесём в него всё, что касается задач:

// src/app/todo-store.ts
import { Injectable, computed, signal } from '@angular/core';
import { Todo } from './todo';
 
@Injectable({ providedIn: 'root' })
export class TodoStore {
  private nextId = 1;
  readonly todos = signal<Todo[]>([]);
  readonly left = computed(() => this.todos().filter((t) => !t.done).length);
 
  add(title: string) {
    const todo = { id: this.nextId++, title, done: false };
    this.todos.update((list) => [...list, todo]);
  }
 
  toggle(id: number) {
    this.todos.update((list) =>
      list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
    );
  }
 
  remove(id: number) {
    this.todos.update((list) => list.filter((t) => t.id !== id));
  }
}

Декоратор @Injectable разрешает внедрять класс. Настройка providedIn: 'root' означает, что на всё приложение будет один объект сервиса, созданный при первом запросе.

Получение сервиса в компоненте

Функция inject() возвращает объект сервиса. Её вызывают при объявлении поля:

// src/app/todo-list/todo-list.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
import { TodoItem } from '../todo-item/todo-item';
 
@Component({
  selector: 'app-todo-list',
  imports: [TodoItem],
  templateUrl: './todo-list.html',
})
export class TodoList {
  store = inject(TodoStore);
}
<!-- src/app/todo-list/todo-list.html -->
<input #field (keyup.enter)="store.add(field.value)" />
<p>Осталось: {{ store.left() }}</p>
 
<ul>
  @for (todo of store.todos(); track todo.id) {
    <li>
      <app-todo-item
        [todo]="todo"
        (toggle)="store.toggle($event)"
        (remove)="store.remove(todo.id)"
      />
    </li>
  }
</ul>

Список работает как раньше, но компонент стал тоньше: он только показывает данные и передаёт действия в сервис.

Один объект на всех

Любой другой компонент, который вызовет inject(TodoStore), получит тот же объект. Например, счётчик в шапке:

// src/app/header/header.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
 
@Component({
  selector: 'app-header',
  template: `<header>Невыполненных задач: {{ store.left() }}</header>`,
})
export class Header {
  store = inject(TodoStore);
}

Добавьте задачу в списке — число в шапке изменится сразу, потому что оба компонента читают один и тот же сигнал.

Сервис для части приложения

Если указать сервис в providers компонента, у этого компонента и его детей будет свой отдельный объект:

// src/app/todo-list/todo-list.ts
@Component({
  selector: 'app-todo-list',
  providers: [TodoStore],
  templateUrl: './todo-list.html',
})

Так два списка дел на одной странице получат независимые задачи.

Частая ошибка

Вызывать inject() внутри обычного метода, например в обработчике щелчка. Angular выдаст ошибку NG0203: inject() must be called from an injection context. Вызывайте inject() только при объявлении полей или в конструкторе.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.