Phil Palmieri
Phil Palmieri

Reputation: 313

RxJS Observables and the Right Way to architect filtered lists

I have gone through a bunch of tutorials, done demos etc.. but i'm still not able to wrap my head around the right way to do this with Observables...

Basically what i have (in angular) is 2 mutable arrays in my component, a displayList and a fullList of items... The template does an ngFor on the displayList.

Filters on the screen call a filter function in the component that clears displayList, then loops through fullList and if the filter applies, it pushes it to display list - giving a real time list filtering effect on the screen...

I know this is the wrong way to approach this, but i cannot wrap my head around the architecture/pattern needed to do this with observables. Do i make a main list and run a .filter() on it based on a component private property? do i do the ngFor on a method that returns the observable list with the .filter in place?

Thanks in advance.

Upvotes: 4

Views: 449

Answers (2)

martin
martin

Reputation: 96889

In RxJS 5 the best candidate for this is the combineLatest() operator (it has both static and instance method variants) that calls its selector function when any of its source Observables emit a value.

let userInput$ = Observable.from(['a', 'ac', 'aca', 'acarp'])
  .concatMap(char => Observable.of(char).delay(500))
  .startWith(null);

let list$ = Observable.of(['abstemious', 'abstentious', 'abulia', 'abut', 'aby', 'acalculia', 'acarophobia', 'acarpous', 'accidence', 'accismus', 'acclamation']);

let filteredList$ = Observable.combineLatest(list$, userInput$, (list, filterString) => {
    if (filterString) {
      // Note that this is Array.filter() method, not an RxJS operator
      return list.filter(name => name.indexOf(filterString) === 0);
    }
    return list;
  });


filteredList$.subscribe(val => console.log(val));

See live demo: https://jsbin.com/jihuxu/2/edit?js,console

This simulates a situation where the user types one character every 500ms and filters the list$ accordingly. Note that also the list$ can emit a new array that'll be filtered right away.

One important thing to be aware of is that each source Observable has to emit at least one value before .combineLatest() is able to emit on every change. This is why I have also the startWith(null), to be sure that the selected filter is null at the beginning.

Upvotes: 2

Olaf Horstmann
Olaf Horstmann

Reputation: 16882

Yes, yes, no, yes, no... in other words: There is no correct answer, it depends. (On your personal, preference, on the general use-case, on the existing architecture of your application, ect. ect...)


Filtering in the controller

In your case, there might not be the need for rxjs, though you should avoid mutable objects/data if possible, so your idea with using .filter would be the way to go here.


Custom Pipe

Another way to go would be to implement a custom pipe and filter the data directly in your template:

<div *ngFor="let item of fullList | customFilterPipe:filterSettings">...</div>

The RxJS-Way

Since you asked for the rxjs-way, here is how I would do it:

filterSettings$: BehaviorSubject<IFilterData> = new BehaviorSubject(INITIAL_FILTER_SETTINGS); // this is updated with filterSettings$.next(newFilterSettings)
fullList$: BehaviorSubject<IData[]> = new BehaviorSubject([]); // updated through fullList$.next(newFullList);
displayList$ = Observable.combineLatest(this.fullList$, this.filterSettings$)
    .map(([list, filterSettings]) => {
        return list.filter(/* your custom filter based on the filterSettings... */);
    });

This will automatically update the displayList$ whenever the filterSettings$ or the fullList$ changes.

To use it in the templare you can use the async-Pipe:

<div *ngFor="let item of displayList$ | async">...</div>

But again: Any of those solutions, as well as your current implementation could be a perfectly valid implementation for a given case.

Upvotes: 2

Related Questions