Как фильтровать и обрабатывать удаление входных символов

Я работаю над приложением, в котором мне нужно фильтровать задачи по их названию или описанию. Фильтр хорошо работает, когда к вводу добавляются символы, но не работает, когда символы удаляются. Например, если я наберу «создать», а затем удалю «создать», фильтр обновится неправильно.

Когда я начинаю вводить текст в поле ввода, фильтрация работает должным образом. Однако если я вернусь, чтобы удалить символы из запроса, список задач не обновится должным образом.

Существуют ли какие-либо передовые методы реализации этого типа фильтрации?

Демо Stackblitz

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);
  }
}
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
2
0
65
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Из геттера, как показано ниже:

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);
  }

  ...
}

Демо @ StackBlitz

Вы поделились не той демо-версией stackblitz? Ваш пример не работает.

coder 02.07.2024 15:06

Привет, извините, я обновил ссылку в ответе. Спасибо.

Yong Shun 02.07.2024 15:13

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

Лучшие практики:

  • не возвращайте наблюдаемое из геттера
  • не подписываться на ваши услуги; предоставлять наблюдаемые, на которые могут подписаться потребители.
  • используйте операторы rxjs для разработки наблюдаемой, которая выдает именно те данные, которые вас интересуют.
  • по возможности используйте канал 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


Если отфильтрованный список касается только одного компонента, вероятно, имеет смысл переместить логику фильтрации из службы в компонент. Я бы, наверное, сделал что-то вроде вот этого.

Другие вопросы по теме