Я работаю над приложением, в котором мне нужно фильтровать задачи по их названию или описанию. Фильтр хорошо работает, когда к вводу добавляются символы, но не работает, когда символы удаляются. Например, если я наберу «создать», а затем удалю «создать», фильтр обновится неправильно.
Когда я начинаю вводить текст в поле ввода, фильтрация работает должным образом. Однако если я вернусь, чтобы удалить символы из запроса, список задач не обновится должным образом.
Существуют ли какие-либо передовые методы реализации этого типа фильтрации?
main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<input type = "text" name = "task-filter" (keyup) = "onKeyUp($event)" [(ngModel)] = "searchValue">
@for (task of tasks ;track task.id) {
<div class = "task">
<div>{{task.title}}</div>
<div>{{task.description}}</div>
</div>
}
`,
})
export class App implements OnInit {
name = 'Angular';
tasks!: Task[];
tasksSubscription!: Subscription;
searchValue: string = '';
constructor(public taskService: TaskService) {}
ngOnInit() {
this.tasksSubscription = this.taskService.tasks$.subscribe((tasks) => {
this.tasks = tasks;
});
}
public onKeyUp(event: KeyboardEvent): void {
this.taskService.filterTasks(this.searchValue);
}
ngOnDestroy(): void {
this.tasksSubscription.unsubscribe();
}
}
задача-service.ts
export class TaskService {
private _tasks$: BehaviorSubject<Task[]> = new BehaviorSubject<Task[]>([]);
constructor() {
this.fetchTasks();
}
public fetchTasks() {
const tasks: Task[] = [
{
id: 1,
title: 'Create Project Plan',
description:
'Develop a detailed project plan for the new software project.',
category: 'Management',
status: 'In Progress',
},
{
id: 2,
title: 'Conduct Code Review',
description:
'Perform a code review for the recently implemented feature.',
category: 'Development',
status: 'Pending',
},
];
this._tasks$.next(tasks);
}
public get tasks$(): Observable<Task[]> {
return this._tasks$.asObservable() as Observable<Task[]>;
}
public get tasks(): Task[] {
return this._tasks$.getValue() as Task[];
}
public filterTasks(searchText: string): void {
if (!searchText) {
this._tasks$.next(this.tasks);
}
searchText = searchText.toLowerCase();
const filteredTasks = this.tasks.filter(
(task) =>
task.title.toLowerCase().includes(searchText) ||
task.description.toLowerCase().includes(searchText)
);
this._tasks$.next(filteredTasks);
}
}
Из геттера, как показано ниже:
public get tasks(): Task[] {
return this._tasks$.getValue() as Task[];
}
Вы получаете значение из наблюдаемого. Находясь в filterTasks
, вы обновили наблюдаемое значение task$
во время фильтрации. Таким образом, вы не сможете вернуть исходные данные для tasks
.
Вместо этого вы можете сохранить данные tasks
в переменной tasks
и не обновлять их.
И удалите геттер tasks
.
export class TaskService {
private _tasks$: BehaviorSubject<Task[]> = new BehaviorSubject<Task[]>([]);
tasks: Task[] = [];
constructor() {
this.fetchTasks();
}
public fetchTasks() {
this.tasks = [
{
id: 1,
title: 'Create Project Plan',
description:
'Develop a detailed project plan for the new software project.',
category: 'Management',
status: 'In Progress',
},
{
id: 2,
title: 'Conduct Code Review',
description:
'Perform a code review for the recently implemented feature.',
category: 'Development',
status: 'Pending',
},
];
this._tasks$.next(this.tasks);
}
...
}
Привет, извините, я обновил ссылку в ответе. Спасибо.
Проблема в том, что вы используете последнюю эмиссию BehaviorSubject в качестве источника задач. Однако вы также проталкиваете свой отфильтрованный список по той же теме. Таким образом, вы в конечном итоге используете отфильтрованный список в качестве исходного массива для фильтрации.
Лучшие практики:
async
Angular, чтобы избавиться от необходимости управлять подписками в вашем компоненте.Ваш код намного сложнее, чем необходимо, поэтому давайте упростим:
Давайте начнем с объявления двух наблюдаемых объектов в сервисе: один для представления выбранных задач, а другой для представления текущего поискового запроса, поскольку это две части информации, которые нам нужны для построения отфильтрованного списка. Критерием поиска на самом деле будет BehaviorSubject, чтобы мы могли передавать значения. Второй будет результатом вызова вашего метода fetchTasks()
(который, как мы предполагаем, вернет Observable<Task[]>).
// service
private searchText$ = new BehaviorSubject<string>('');
private allTasks$ = this.fetchTasks();
public setSearchText(text: string) {
this.searchText$.next(text);
}
private fetchTasks(): Observable<Task[]> {
// ...
}
Теперь давайте воспользуемся функцией rxjs joinLatest для создания наблюдаемой, которая выдает именно те данные, которые нас интересуют, — отфильтрованный список!
combineLatest
принимает массив исходных наблюдаемых и будет выдавать их последние значения всякий раз, когда какой-либо из них излучает (хотя первая эмиссия не произойдет, пока каждый из них не выдаст хотя бы одно значение).
Это именно то поведение, которое вам нужно: выдавать обновленное значение всякий раз, когда выдает любой наблюдаемый источник.
public tasks$ = combineLatest([this.allTasks$, this.searchText$]).pipe(
map(([tasks, searchText]) => tasks.filter(
(task) => task.title.toLowerCase().includes(searchText.toLowerCase())
|| task.description.toLowerCase().includes(searchText.toLowerCase())
))
);
Выше tasks$
объявлен как наблюдаемый объект, который будет выдавать отфильтрованный список всякий раз, когда выдает allTasks$
или searchText$
. Вероятно, allTasks$
будет выдавать только одно значение, и это нормально. Но поскольку searchText$
выдает новые значения, новый отфильтрованный список рассчитывается с использованием полного списка задач и новейшего значения поискового запроса.
Обратите внимание, что мы еще не подписались. Служба просто предоставляет наблюдаемую tasks$
, на которую могут подписаться потребители, и метод setSearchText()
, который можно использовать для «установки» текста поиска внутри службы, что заставляет наблюдаемую tasks$
выдавать обновленное значение.
Теперь в вашем компоненте, а не:
Вы можете просто связать шаблон непосредственно с наблюдаемым объектом task$
, предоставляемым сервисом, используя канал async
:
<input type = "text" (keyup) = "onKeyUp($event)">
@for (task of service.tasks$ | async; track task.id) {
<div class = "task">
<div>{{task.title}}</div>
<div>{{task.description}}</div>
</div>
}
Это значительно упрощает код вашего компонента:
export class App {
constructor(public service: TaskService) {}
public onKeyUp(event: KeyboardEvent): void {
const searchValue = (event.target as HTMLInputElement).value ?? '';
this.service.setSearchText(searchValue);
}
}
Почти ничего не осталось! Нет ngOnInit
, нет .subscribe()
, нет Subject
, нет ngOnDestroy
никаких локальных переменных для хранения состояния.
Вот рабочий StackBlitz
Если отфильтрованный список касается только одного компонента, вероятно, имеет смысл переместить логику фильтрации из службы в компонент. Я бы, наверное, сделал что-то вроде вот этого.
Вы поделились не той демо-версией stackblitz? Ваш пример не работает.