Avatar ·

Как применить фильтры к директиве *ngFor в Angular?

Привет всем! Я новичок в Angular и TypeScript, и у меня возникла проблема с фильтрацией данных в шаблоне. У меня есть компонент, который отображает список пользователей с помощью *ngFor. Мне нужно отфильтровать этот список по имени (например, показать только пользователей, чьё имя начинается с определённой буквы или содержит подстроку). Я пытался использовать pipe, но что-то пошло не так. Вот мой код: ```typescript // user.model.ts export interface User { id: number; name: string; email: string; } ``` ```typescript // users.component.ts import { Component } from '@angular/core'; import { User } from './user.model'; @Component({ selector: 'app-users', templateUrl: './users.component.html' }) export class UsersComponent { users: User[] = [ { id: 1, name: 'Анна', email: 'anna@example.com' }, { id: 2, name: 'Борис', email: 'boris@example.com' }, { id: 3, name: 'Виктор', email: 'viktor@example.com' } ]; } ``` ```html
  • {{ user.name }} - {{ user.email }}
``` Я хочу добавить поле ввода, чтобы пользователь мог ввести текст, и список фильтровался по имени. Я слышал, что можно использовать `pipe`, но когда я попробовал, получил ошибки. Подскажите, как правильно реализовать фильтрацию в *ngFor? Может, есть другие способы, кроме pipe? Заранее спасибо! <<>>

Проблема с pipe и правильное решение

Использование pipe для фильтрации в Angular — распространённая ошибка новичков. Во-первых, чистые пайпы (pure pipes) не пересчитываются при изменении входных данных, если ссылка на массив не меняется. Во-вторых, Angular рекомендует избегать пайпов для фильтрации в шаблонах, так как это может привести к проблемам с производительностью и нарушению принципа "умный компонент — глупый шаблон".

Лучший способ — фильтровать данные в самом компоненте, используя геттер или метод, который возвращает отфильтрованный массив. Это даёт полный контроль над логикой и производительностью.

Пошаговое решение

Сначала добавим поле для ввода фильтра и привяжем его к свойству компонента через [(ngModel)]:

// users.component.ts
import { Component } from '@angular/core';
import { User } from './user.model';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html'
})
export class UsersComponent {
  users: User[] = [
    { id: 1, name: 'Анна', email: 'anna@example.com' },
    { id: 2, name: 'Борис', email: 'boris@example.com' },
    { id: 3, name: 'Виктор', email: 'viktor@example.com' }
  ];

  filterText: string = '';

  get filteredUsers(): User[] {
    if (!this.filterText.trim()) {
      return this.users; // если фильтр пуст, показываем всех
    }
    const lowerFilter = this.filterText.toLowerCase();
    return this.users.filter(user => 
      user.name.toLowerCase().includes(lowerFilter)
    );
  }
}

Теперь обновим шаблон, чтобы использовать геттер filteredUsers и добавить поле ввода:

<!-- users.component.html -->
<div>
  <label>Фильтр по имени:</label>
  <input type="text" [(ngModel)]="filterText" placeholder="Введите имя...">
</div>

<ul>
  <li *ngFor="let user of filteredUsers">
    {{ user.name }} - {{ user.email }}
  </li>
</ul>

Почему это работает

  • Реактивность: Изменение filterText через ngModel автоматически перерисовывает список, так как Angular отслеживает изменения свойств компонента.
  • Производительность: Фильтрация происходит только при изменении filterText или массива users, а не на каждой проверке изменений.
  • Чистота кода: Логика фильтрации находится в компоненте, а не в шаблоне, что упрощает тестирование и поддержку.

Дополнительные улучшения

Если фильтр должен быть регистронезависимым, мы уже это реализовали. Для более сложной фильтрации (например, по нескольким полям) можно расширить метод:

get filteredUsers(): User[] {
  if (!this.filterText.trim()) return this.users;
  const lowerFilter = this.filterText.toLowerCase();
  return this.users.filter(user => 
    user.name.toLowerCase().includes(lowerFilter) ||
    user.email.toLowerCase().includes(lowerFilter)
  );
}

Также можно вынести фильтрацию в отдельный сервис, если логика сложная или используется в нескольких компонентах.

Предостережение против pipe

Хотя технически можно создать кастомный пайп для фильтрации, это не рекомендуется:

  • Чистые пайпы не сработают, если вы мутируете массив (добавляете/удаляете элементы без изменения ссылки).
  • Нечистые пайпы (impure) вызываются на каждой проверке изменений, что убивает производительность.
  • Angular документация явно предупреждает: "Avoid filtering and sorting in pipes, especially for large lists".

Используйте подход с геттером — это стандартная практика в Angular-сообществе.

<<>>

Альтернативный подход: использование RxJS с BehaviorSubject

Если вы работаете с асинхронными данными (например, из HTTP-запроса) или хотите более продвинутую реактивность, можно использовать BehaviorSubject и оператор combineLatest из RxJS. Этот подход особенно полезен, когда фильтр и данные меняются динамически.

Пример реализации:

// users.component.ts
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from './user.model';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html'
})
export class UsersComponent implements OnInit {
  private usersSubject = new BehaviorSubject<User[]>([
    { id: 1, name: 'Анна', email: 'anna@example.com' },
    { id: 2, name: 'Борис', email: 'boris@example.com' },
    { id: 3, name: 'Виктор', email: 'viktor@example.com' }
  ]);
  
  private filterSubject = new BehaviorSubject<string>('');
  
  filteredUsers$ = combineLatest([
    this.usersSubject,
    this.filterSubject
  ]).pipe(
    map(([users, filter]) => {
      if (!filter.trim()) return users;
      const lowerFilter = filter.toLowerCase();
      return users.filter(user => 
        user.name.toLowerCase().includes(lowerFilter)
      );
    })
  );

  setFilter(value: string) {
    this.filterSubject.next(value);
  }
}

В шаблоне используем асинхронный пайп:

<!-- users.component.html -->
<div>
  <label>Фильтр:</label>
  <input type="text" (input)="setFilter($event.target.value)">
</div>

<ul>
  <li *ngFor="let user of filteredUsers$ | async">
    {{ user.name }} - {{ user.email }}
  </li>
</ul>

Плюсы: Отличная реактивность, легко комбинировать с HTTP-запросами, автоматическая отписка через async pipe. Минусы: Сложнее для новичков, требует понимания RxJS. Для простых случаев достаточно геттера из первого ответа.