Как применить фильтры к директиве *ngFor в Angular?
- {{ user.name }} - {{ user.email }}
Проблема с 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. Для простых случаев достаточно геттера из первого ответа.