Stev0x
Stev0x

Reputation: 62

Angular rxjs filter observable

I'm trying to filter an observable of products with an array of filters but i really don't know how..

Let me explain, I would like to set the result of my filtering to filteredProducts. For filtering i have to check, for each filter, if the product's filter array contains the name of the filter and if the products values array's contains filter id.

For the moment, the filter works but only with the last selected filter and i'd like to filter products list with all filters in my selectedFilters array. I can have one or multiple filters.

StackBlitz

export class ProductsFilterComponent extends BaseComponent implements OnInit {
    @Select(FiltersState.getAllFilters) filters$: Observable<any>;
    @Input() products$: Observable<Product[]>;
    filteredProducts$: Observable<Product[]>;
    public selectedFilters = [];

    constructor(
        private store: Store) { super(); }

    ngOnInit() {
        this.store.dispatch(new GetAllFilters());
    }

    private filterProducts() {
        this.filteredProducts$ = this.products$.pipe(
            map(
                productsArr => productsArr.filter(
                    p =>
                        p.filters.some(f => this.selectedFilters.some(([selectedF]) => selectedF === f.name.toLowerCase()) // Filter name
                            && f.values.some(value => this.selectedFilters.some(([, filterId]) => filterId === value)) // Filter id
                        )
                )
            )
        );
        this.filteredProducts$.subscribe(res => console.log('filtered:', res));
    }
}

Here's the structure of a product object Here's the structure of a product object

Here's the structure of selectedFilters enter image description here

A big thank you in advance :-).

Upvotes: 1

Views: 2654

Answers (1)

Marian
Marian

Reputation: 4079

Here's a stackblitz with an example that works.

To sum it up:

  1. We listen to changes in both products and filters, and combine them into a single observable using combineLatest:
    const productsAndFilters: Observable<[Product[], ProductFilter[]]> = combineLatest([
      this.products$,
      this.filters$.pipe(startWith(initialFilters)),
    ]);
  1. Then we pipe the combined observable into a function that actually does the filtering job
    this.filteredProducts$ = productsAndFilters.pipe(
      map(([products, filters]) => {
        return AppComponent.filterProducts(products, filters);
      }),
    )
  1. Every time products$ emits a new value, we will update filteredProducts$. Every time filters$ emits a new value, we update them too.

  2. Note that combineLatest will NOT emit until EACH observable has emitted at least once. For this reason, if your "selectedFilters" do not emit anything in the beginning, you can use startWith operator to create an observable with a starting value. This makes sure that combineLatest will emit a value as soon as products$ emits a value.


I've also moved the filtering code into a static function:

  private static filterProducts(products: Product[], filters: ProductFilter[]) {
    // we can combine filters into a single function
    // so that the code a tiny bit more readable
    const matchesAllFilters = (product: Product) => filters.every(
        ([filterName, filterValue]) => product.filters
          .some(f => f.name === filterName &&
                     f.values.some(value => value === filterValue))
    );

    return products.filter(matchesAllFilters);
  }

In my example I assume that you want ALL filters to be satisfied. If you instead want "AT LEAST ONE" filter to be satisfied, you can change filters.every to filters.some.

Upvotes: 1

Related Questions