Application data filtering
Angular SPA Typescript Visual Studio Code

How to Use Pipe Transforms to Filter Lists in an Angular Application

Welcome to today’s blog post.

In today’s post I will show how to use a feature of the ngFor structural directive that allows us to filter items in a collection.

The filter I am referring to is a type of Pipe Transform. A Pipe Transform is a custom function that allows each item of the template to have one of the to following actions applied to it:

  1. Display differently with a pipe transform data transformation function.
  2. Display or hide depending on the conditions within the pipe transform filter function.

Today’s post relates to the pipe filter. To implement a pipe filter transform, we are required to derive from the PipeTransform class.

I will give an overview of three ways to implement a pipe transform filter. These are:

  1. A single-field filter.
  2. A multi-field filter.
  3. A range-field filter.

Single-field Filtering

The first example of a pipe filter is to compare a property within each item within a collection based on a string value.

Below is a sample HTML template to show content using a pipe filter matching on a string value:

<p>List of Books :)</p>

<p class="heading">Fiction Books</p>

<div *ngFor="let item of books | bookGenrePipe: 'Fiction'">
  {{ item.title }}
</div>

<p class="heading">Science Books</p>

<div *ngFor="let item of books | bookGenrePipe: 'Science'">
  {{ item.title }}
</div>

The CSS class definitions for our headings are shown below:

p {
  font-family: Lato;
}

p.heading {
  font-family: Lato;
  font-weight: bolder;
}

Below is the implementation of the pipe transform for a string-matching filter:

import { Pipe, PipeTransform } from '@angular/core';
import { BookInterface } from './book-interface';

@Pipe({
  name: 'bookGenrePipe',
})
export class BookGenrePipe implements PipeTransform {
  transform(values: BookInterface[], genre?: string): any {
    if (!genre) return values;
    return values.filter((b) => b.genre === genre);
  }
}

An interface defining the structure of each collection item is shown:

export interface BookInterface {
  id: number;
  title: string;
  genre: string;
  yearPublished: number;
}

In our host component we define the filter and sample data as shown:

import { Component } from '@angular/core';
import { BookFilterInterface } from './book-filter-interface';
import { BookFilterRangeInterface } from './book-filter-range';
import { BookInterface } from './book-interface';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular';

  books: BookInterface[] = [
    {
      id: 1,
      title: 'Book A',
      genre: 'Fiction',
      yearPublished: 2000,
    },
    {
      id: 2,
      title: 'Book B',
      genre: 'Fiction',
      yearPublished: 2004,
    },
    {
      id: 3,
      title: 'Book C',
      genre: 'Science',
      yearPublished: 2010,
    },
    {
      id: 4,
      title: 'Book D',
      genre: 'Arts',
      yearPublished: 2007,
    },
    {
      id: 5,
      title: 'Book E',
      genre: 'Economics',
      yearPublished: 2012,
    },
    {
      id: 6,
      title: 'Book F',
      genre: 'Science',
      yearPublished: 2014,
    },
    {
      id: 7,
      title: 'Book G',
      genre: 'Arts',
      yearPublished: 2017,
    },
    {
      id: 8,
      title: 'Book H',
      genre: 'Science',
      yearPublished: 2016,
    },
  ];
}

The definition of the interface is shown below:

export interface BookFilterInterface {
  title?: string;
  genre?: string;
  yearPublished: number;
}

When the above component and pipes are implemented, we can then declare our pipe transforms within the NgModule declaration as shown:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { BookGenrePipe } from './book-genre.pipe';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [
    AppComponent,
    BookGenrePipe,
    …
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

When the application runs, the output for the single field filters within the template will look as shown:

In the next section, I will show how to filter with more than one field.

Multi-field Filtering

The next type of filter is a multi-conditional filter. Instead of filtering by one field value, we filter by multiple values. In this case, we filter by genre and year.

We use the same interface BookFilterInterface from earlier.

Next, we declare a filter that defines the properties below that we will filter by:

genre = Science

year published = 2010

The JSON filter is defined as shown:

bookFilter1: BookFilterInterface = {
  title: '',
  genre: 'Science',
  yearPublished: 2010,
};

The multi-field pipe transform filter is implemented as shown:

import { Pipe, PipeTransform } from '@angular/core';
import { BookFilterInterface } from './book-filter-interface';
import { BookInterface } from './book-interface';

@Pipe({
  name: 'bookFilterPipe',
})
export class BookFilterPipe implements PipeTransform {
  transform(values: BookInterface[], filter?: BookFilterInterface): any {

    if (!values || !filter) return values;

    if (filter && filter.genre && !filter.yearPublished)
      return values.filter((book) => book.genre === filter.genre);

    if (filter && filter.yearPublished && !filter.genre)
      return values.filter(
        (book) => book.yearPublished === filter.yearPublished
      );

    if (filter && filter.yearPublished && filter.genre)
      return values.filter(
        (book) =>
          book.yearPublished === filter.yearPublished &&
          book.genre === filter.genre
      );
  }
}

When the above component and pipes are implemented, we can then declare our multi-field pipe transforms within the NgModule declaration as shown:

import { NgModule } from '@angular/core';
…
import { BookFilterPipe } from './book-filter.pipe';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [
	…
    BookFilterPipe,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Below is our HTML template that declares the above multi-field filter:

<p class="heading">Science Books published in 2010</p>

<div *ngFor="let item of books | bookFilterPipe: bookFilter1">
  {{ item.title }}
</div>

When run, the output will resemble as follows

In the next section, I will show how to filter with a range of values within a field.

Range-field Filtering

The next type of filter is a range filter.

To implement a range filter on the year property, we need a new structure that adds a range (from and to) JSON property and modify the transform() function to filter items with year values within the range.

Our range filter interface and year range interface definitions are shown below:

export interface BookFilterRangeInterface {
  title?: string;
  genre?: string;
  yearPublishedRange?: YearRangeInterface;
}

export interface YearRangeInterface {
  from: number;
  to: number;
}

Within our host component, the range filter interface is declared as shown with a selected genre and year range:

bookFilter2: BookFilterRangeInterface = {
  title: '',
  genre: 'Science',
  yearPublishedRange: {
    from: 2000,
    to: 2014,
  },
};

The range filter pipe transformation implementation is shown below:

import { Pipe, PipeTransform } from '@angular/core';
import { BookFilterRangeInterface } from './book-filter-range';
import { BookInterface } from './book-interface';

@Pipe({
  name: 'bookFilterRangePipe',
})
export class BookFilterRangePipe implements PipeTransform {
  transform(values: BookInterface[], filter?: BookFilterRangeInterface): any {
    if (!values || !filter) return values;
    if (filter && filter.genre && !filter.yearPublishedRange)
      return values.filter((book) => book.genre === filter.genre);
    if (filter && filter.yearPublishedRange && !filter.genre)
      return values.filter(
        (book) =>
          book.yearPublished >= filter.yearPublishedRange.from &&
          book.yearPublished <= filter.yearPublishedRange.to
      );
    if (filter && filter.yearPublishedRange && filter.genre)
      return values.filter(
        (book) =>
          book.yearPublished >= filter.yearPublishedRange.from &&
          book.yearPublished <= filter.yearPublishedRange.to &&
          book.genre === filter.genre
      );
  }
}

When the above component and pipes are implemented, we will next declare our range filter pipe transforms within the NgModule declaration as shown:

import { NgModule } from '@angular/core';
…
import { BookFilterRangePipe } from './book-filter-range.pipe';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [
    …
    BookFilterRangePipe,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Below is our HTML template that declares the above range filter:

<p class="heading">Science Books published between 2010 and 2014</p>

<div *ngFor="let item of books | bookFilterRangePipe: bookFilter2">
  {{ item.title }}
</div>

When run, the output resembles as shown:

If you have combined, declared, and run the above three pipes within the same component, the following output will show:

We have seen how useful and powerful piped transforms are for providing filters for collections of items within an ngFor loop within a HTML template. I showed how to use each of the three different types of range filters.

In a future post I will discuss how to optimize performance within an ngFor loop within an Angular application.

That is all for today’s post.

I hope you have found today’s post useful and informative.

Social media & sharing icons powered by UltimatelySocial