Build an Ecommerce Product Filter with Angular and Cosmic
Tags: angular, cosmicjs, javascript, typescript
Published: 2020-10-13 15:50:36

---
title: Build an Ecommerce Product Filter with Angular and Cosmic
published: true
canonical: https://www.cosmicjs.com/articles/build-a-headless-ecommerce-product-filter-using-angular-and-cosmic
description: 
tags: angular, cosmicjs, javascript, typescript
---
![Build an Ecommerce Product Filter with Angular and Cosmic](https://imgix.cosmicjs.com/6febf530-12d9-11eb-8570-4b38bb821e19-Screenshot-2020-10-20-at-14.37.28.png?w=1000)

_\* This article will assume some basic [knowledge of Angular and CMS](https://www.cosmicjs.com/knowledge-base/angularjs-cms) so it can focus on the specific task at hand. Feel free to ask me about any specifics on the implementation you may find unclear_

  
### **TL; DR**

Take a look at:
* [The GitHub repo](https://github.com/cosmicjs/cosmic-commerce-filters)
* [Install the app](https://www.cosmicjs.com/apps/ecommerce-personalization-with-filters)
* [View a demo](https://cosmic-commercefilters.netlify.app/)



## What are we going to build?

This site will be based on a previous example: an ecommerce website which purpose is to show how we can offer a customized experience for everyone. I strongly recommend that you [read the first article](https://www.cosmicjs.com/articles/build-an-ecommerce-app-with-personalization-using-angular-and-cosmic-js), as we will work on top of what was built there. This time, we will add filtering functionality to showcase the [Cosmic Advanced Queries](https://www.cosmicjs.com/changelog/introducing-advanced-queries) feature. Our data will be stored and served by Cosmic and we will use Angular for our Front-End.

## Preparing our bucket

The first thing we'll do is prepare our Cosmic bucket. We already have the following three object types:
* Categories
* Products
* Users

Each `product` now will include a `color` attribute, and each `category` will include a `isRoot` attribute. These attributes will give us more to work with when building the filters.

We will also create a new type:
* Price filters

Each `price filter` will have a `min` and `max` attribute. This new type will allow us to define price ranges to then use in the filter. There are other options to do this, as we could directly filter by all the different prices contained in the products, but this approach will give us (and the potential editor/merchandiser setting everything up) more flexibility on what we want to show the customer.

If you are as lazy as I am, you can always **replicate the demo bucket** by [installing the app](https://www.cosmicjs.com/apps/ecommerce-personalization-with-filters).

## Updating the models

We need to reflect the changes to the bucket into our models. This will be the model for the price filters:
```javascript
export class PriceFilter {
  _id: string;
  slug: string;
  title: string;
  max: number;
  min: number;

  constructor(obj) {
    this._id = obj._id;
    this.slug = obj.slug;
    this.title = obj.title;
    this.max = obj.metadata.max;
    this.min = obj.metadata.min;
  }
}
```

And, of course, we need to also update our product and category models:
```javascript
import { Category } from './category';

export class Product {
  _id: string;
  slug: string;
  title: string;
  price: string;
  categories: Category[];
  image: string;
  color: string;

  constructor(obj) {
    this._id = obj._id;
    this.slug = obj.slug;
    this.title = obj.title;
    this.price = obj.metadata.price;
    this.image = obj.metadata.image.url;
    this.color = obj.metadata.color;
    this.categories = [];

    if (obj.metadata && obj.metadata.categories) {
      obj.metadata.categories.map(category => this.categories.push(new Category(category)));
    }
  }
}
```
```javascript
export class Category {
  _id: string;
  slug: string;
  title: string;
  isRoot: boolean;

  constructor(obj) {
    this._id = obj._id;
    this.slug = obj.slug;
    this.title = obj.title;
    this.isRoot = obj.metadata ? obj.metadata.root : false;
  }
}
```
## Modifying the service

In order to take full advantage of the advanced queries, we will create a new method on our service:

```javascript
getProductsByQuery(query?: string): Observable<Product[]> {
    if (!this.products$.get(query)) {
      const querystring = query ? '&query=' + query : '';

      const response = this.http.get<Product[]>(this.productObjectsUrl + '&sort=random' + querystring).pipe(
        tap(_ => console.log('fetched products')),
        map(_ => {
          if (_['objects']) {
            return _['objects'].map(element => new Product(element));
          }
        }),
        shareReplay(1),
        catchError(this.handleError('getProducts', []))
      );
      this.products$.set(query, response);
    }
    return this.products$.get(query);
  }
```
_\*Note that the only difference with the old `getProducts()` is the inclusion of the optional `query` parameter._

Also, let's create a method to get our new price filters:
```javascript
private priceFiltersUrl = this.objectTypePath + '/pricefilters';
private priceFilters$: Observable<PriceFilter[]>;

getPriceFilters(): Observable<PriceFilter[]> {
    if (!this.priceFilters$) {
      this.priceFilters$ = this.http.get<PriceFilter[]>(this.priceFiltersUrl).pipe(
        tap(_ => console.log('fetched price filters')),
        map(_ => {
          return _['objects'].map(element => new PriceFilter(element));
        }),
        shareReplay(1),
        catchError(this.handleError('getPriceFilters', []))
      );
    }
    return this.priceFilters$;
  }
```


## Creating the filter component

Now we have a method to query products in an advanced manner, but we still need to construct the query, so let's build a component to allow the user to select the different filtering options. 

We want to allow the customer to select different categories, colors and price ranges, for that, we will subscribe to our service and assign the results to a map that will store a pair of `object, boolean`; that way we can keep track of the user selections.
```javascript
export class FilterComponent implements OnInit {
  public rootCategoryList: Map<Category, boolean> = new Map<Category, boolean>();
  public categoryList: Map<Category, boolean> = new Map<Category, boolean>();
  public colorList: Map<string, boolean> = new Map<string, boolean>();
  public priceList: Map<PriceFilter, boolean> = new Map<PriceFilter, boolean>();

  @Output() selectedFilters = new EventEmitter<string>();

  constructor(private cosmicService: CosmicService) {}

  ngOnInit() {
    forkJoin(this.cosmicService.getCategories(), this.cosmicService.getProducts(), this.cosmicService.getPriceFilters()).subscribe(
      ([categories, products, priceFilters]) => {
        // categories
        categories.forEach(cat => {
          cat.isRoot ? this.rootCategoryList.set(cat, false) : this.categoryList.set(cat, false);
        });

        // colors

        const colorSet = new Set<string>(); // Using a Set will automatically discard repeated colors
        products.forEach(p => colorSet.add(p.color));
        colorSet.forEach(c => {
          this.colorList.set(c, false);
        });

        // prices
        priceFilters.forEach(pf => this.priceList.set(pf, false));

        this.updateSelectedFilters();
      }
    );
  }
...
```
_\*The reasoning behind dividing categories between root/no-root is because I want to provide the user with a visual hint as to what this categories model looks like, but it's not relevant to the task._

Now, this is how the html will look like:

```html
<ul>
  <li class="mb-3" *ngFor="let category of rootCategoryList | keyvalue">
    <label class="radio is-size-4" >
      <input type="checkbox" value="{{category.key.slug}}" [checked]="category.value" (change)="filterRootCategory(category)">
      <span class="pl-2">{{category.key.title}}</span>
    </label>
  </li>
</ul>
<hr/>
<ul>
  <li class="mb-3" *ngFor="let category of categoryList | keyvalue">
    <label class="checkbox is-size-4" >
      <input type="checkbox" value="{{category.key.slug}}" [checked]="category.value" (change)="filterCategory(category)">
      <span class="pl-2">{{category.key.title}}</span>
    </label>
  </li>
</ul>
<hr/>
<ul>
  <li class="mb-3 color-item" *ngFor="let color of colorList | keyvalue">
      <label class="checkbox is-size-4">
        <input type="checkbox" value="{{color.key}}" [checked]="color.value" (change)="filterColor(color)">
        <span [style.background-color]="color.key"></span>
      </label>
    </li>
</ul>
<hr/>
<ul>
  <li class="mb-3" *ngFor="let price of priceList | keyvalue">
    <label class="checkbox is-size-4" >
      <input type="checkbox" value="{{price.key.slug}}" [checked]="price.value" (change)="filterPrice(price)">
      <span class="pl-2">{{price.key.title}}</span>
    </label>
  </li>
</ul>

```

All the change events look the same, they just mark the element as selected/unselected on the map (this is bound to the checkbox value, so there is no need to modify the DOM manually) and trigger a filter update:
```javascript
filterCategory(entry: { key: Category; value: boolean }) {
    this.categoryList.set(entry.key, !entry.value);
    this.updateSelectedFilters();
  }
```
_\* And so on..._

Now, let's look at `updateSelectedFilters()`. This method will review what's currently selected on the maps (thanks to the help of aux methods `setCategoryFilterSelection()`, etc. and build our query.
```javascript
updateSelectedFilters() {
    // categories
    const catInSelection: string[] = [];
    const catNotInSelection: string[] = [];

    this.setCategoryFilterSelection(this.categoryList, catInSelection, catNotInSelection);
    this.setCategoryFilterSelection(this.rootCategoryList, catInSelection, catNotInSelection);

    // colors

    const colorInSelection: string[] = this.setColorFilterSelection(this.colorList);

    // price
    const pricesInSelection: number[][] = this.setPriceFilterSelection(this.priceList);

    // query
    let jsonObj = {};
    if (catInSelection.length > 0 && catNotInSelection.length > 0) {
      jsonObj['metadata.categories'] = {
        $in: catInSelection,
        $nin: catNotInSelection
      };
    }
    if (colorInSelection.length > 0) {
      jsonObj['metadata.color'] = { $in: colorInSelection };
    }

    if (pricesInSelection.length > 0) {
      jsonObj['$or'] = [];
      pricesInSelection.forEach(price => {
        jsonObj['$or'].push({
          $and: [
            {
              'metadata.price': {
                $gte: price[0]
              }
            },
            {
              'metadata.price': {
                $lte: price[1]
              }
            }
          ]
        });
      });

      // Introducing "$or" means we need to combine with an "$and" for the other conditions
      const auxObj = { $and: [] };

      auxObj.$and.push(
        { "'metadata.categories": jsonObj['metadata.categories'], 'metadata.color': jsonObj['metadata.color'] },
        { $or: jsonObj['$or'] }
      );
      jsonObj = auxObj;
    }

    const query = encodeURIComponent(JSON.stringify(jsonObj));
    this.selectedFilters.emit(query);
  }
```

## Wrapping it all together

Did you notice we are emitting the query? Now it's time to go to our product listing and modify how it request the products to accommodate all the changes we made. First of all, let's update the HTML to include our new filter component.
```html
<div class="columns">
<div class="column is-one-fifth filters">
  <app-filter (selectedFilters)="onChangeFilters($event)"></app-filter>
</div>
<div class="column columns" *ngIf="productList && user">
  <ng-container *ngFor="let product of (productList | customSort:user.interests)">
          <div class="product-tile column is-one-third">
            <img src="{{ product.image }}" class="image"/>
            <div class="level is-size-4 is-uppercase">
                <span class="level-item">{{product.title}}</span>
                <span class="level-item has-text-weight-bold">${{product.price}}</span>
            </div>
            <app-actions [product]="product"></app-actions>
          </div>
  </ng-container>
  <div *ngIf="productList.length === 0">
    <span>There are no products that match your search, try something else.</span>
  </div>
</div>
</div>

```

Now we just need to define the method for our `selectedFilters` event, it looks like this:
```javascript
  onChangeFilters(selectedFilters: string) {
    this.cosmicService.getProductsByQuery(selectedFilters).subscribe(products => {
      this.productList = products ? products : [];
    });
  }
```

And that's all. With just a couple updates on our previous eCommerce application, we've been able to add a pretty powerful filter component that would  help our customers find the product they are looking for.

Interested in more articles like this? Check out [Cosmic articles](https://cosmicslack.herokuapp.com/) for more tutorials like this one, or [join us in the Slack community](https://cosmicslack.herokuapp.com/), where hundreds of devs like you are discussing the future of Headless websites.