Search code examples
angularrxjsrxjs-observables

Dynamically filter array based on async user input and content of another array


I want to display a filtered list of strings for display to the user where the filter is a function of input from the user and another (dynamically changing) array which contains strings that should not be displayed. For context, the goal is to have an autocomplete list that is filtered by the user input and doesn't included strings that the user did already select previously.

Currently I have the following which works to update the filtered list in case the user input changes but doesn't update it when the content of the already selected strings changes. And also not when the allStrings array changes.

allStrings: string[] = ["Lemon", "Apple", "Orange"];
selectedStrings: string[] = [];
userInput = new FormControl<string>('');
filteredStrings!: Observable<string[]>();

ngOnInit() {
  this.filteredStrings = this.userInput.valueChanges.pipe(
    startWith(""),
    map(input => {
      return this.allStrings.filter(item => {
        return input == "" || (item.includes(input) && !this.selectedStrings.includes(item));
      });
    }),
  );
}

How can I achieve what I want? Convert allStrings and selectedStrings to observables (or subjects?) and use combineLatest or similar to trigger on changes of all inputs? Or is there are more idiomatic way of doing this kind of thing in Angular/RxJS?

allStrings!: Observable<string[]>;
selectedStrings!: Observable<string[]>;
userInput = new FormControl<string>('');
filteredStrings!: Observable<string[]>();

ngOnInit() {
  this.filteredItems = combineLatest(this.allStrings, this.selectedStrings, this.userInput.valueChanges).pipe(
    startWith([[], [], ""]),
    map([all, selected, input] => {
      return all.filter(item => {
        return input == "" || (item.includes(input) && !selected.includes(item));
      });
    }),
  );
}

Solution

  • Your approach of using combineLatest looks promising. It allows you to react to changes in all three inputs: allStrings, selectedStrings, and userInput. This way, the filteredItems observable will be updated whenever any of these inputs change.

    Here's the adjusted code:

    allStrings$: Observable<string[]>;
    selectedStrings$: Observable<string[]>;
    userInput = new FormControl<string>('');
    filteredItems$: Observable<string[]>;
    
    ngOnInit() {
      this.filteredItems$ = combineLatest([this.allStrings$, this.selectedStrings$, this.userInput.valueChanges]).pipe(
        startWith([[], [], ""]),
        map(([all, selected, input]) => {
          return all.filter(item => {
            return input == "" || (item.includes(input) && !selected.includes(item));
          });
        }),
      );
    }
    

    This code assumes that allStrings and selectedStrings are observables. If they are regular arrays that can change over time, you'll need to convert them to observables using BehaviorSubject or ReplaySubject.

    Remember to replace this.allStrings and this.selectedStrings with actual observables or subjects.

    This setup should work for dynamically updating the filtered list based on changes in user input, allStrings, and selectedStrings. Keep in mind that you'll need to subscribe to filteredItems$ in your template or component logic to consume the filtered list.