Search code examples
angularrxjsinput-filtering

How to Filter and Handle Input Character Removal


I'm working on a application where I need to filter tasks based on their title or description. The filter works well when characters are added to the input, but it fails when characters are removed. For example, if I type "create" and then delete the "reate", the filter does not update correctly.

When I start typing in the input field, the filtering works as expected. However, if I backspace to remove characters from the query, the list of tasks does not update properly.

Are there any best practices when implementing this type of filtering?

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

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

Solution

  • From the getter as below:

    public get tasks(): Task[] {
      return this._tasks$.getValue() as Task[];
    }
    

    You are getting the value from the observable. While in your filterTasks, you have updated the task$ observable value during filtering. Thus, you can't get back the original data for the tasks.

    Instead, you may look to store the tasks data in the tasks variable and not update it.

    And remove the tasks getter.

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

    Demo @ StackBlitz