How To Create A Filter Pipe In Angular

Understand why Angular doesn’t ship with filtering and sorting pipes and how you can create a custom filter pipe

Photo by Erlend Ekseth on Unsplash

Angular offers several built-in pipes for the most common scenarios.

One of the big absences is a filter pipe. There is a very good reason why Angular doesn’t offer a built-in filter pipe.

Pipes for filtering and sorting lists are not available in Angular, even though AngularJS used to offer filterand orderBy.

This isn’t a mistake.

Because such pipelines perform badly and prohibit aggressive minification, Angular does not offer them out of the box. Filtering is costly.

Even though computing power grows every year, data and information also grow constantly!

Image of CPUs
CPUs. Photo by Laura Ockel on Unsplash

Generally speaking, parameters that correspond to object attributes are required for filtering. However, some scenarios require pipes to be impure, meaning that Angular calls impure pipes practically every time a change-detection cycle occurs.

As a consequence, filtering and sorting become costly operations, especially when the amount of data is significant.

When Angular invokes these pipes multiple times per second, the user experience for even moderate-sized lists might suffer significantly.

First of all, let’s have a look at the template and the class.

AppComponent and Data

In the template, we have an input element that uses two-way binding to store the user input in a property called filterBy. Remember to import FormsModule in AppModule to use two-way binding in your app.

Below, we use NgFor to list the name of some users from usersList. Here’s the code:

AppComponent including template and class

The list of users usersList is an array of ten items, eg, users. It is stored in the class, and you can find it on jsonplaceholder or in the StackBlitz of this app.

Each user is an object with several properties. For our purpose, it suffices to know that one of the properties is name.

We want to filter users by name.

The initial app looks like this:

Angular app using FilterPipe
Angular app using FilterPipe

Let’s start by using the Angular CLI command ng generate pipe filterwhere filter is the pipe name that will be used in template bindings.

As a side note, you can also use ng g p pipe.

Angular will take care of creating a file, populating some fields, and importing it correctly in app.module.ts.

In the transform method, value is the value on the left side of the pipe in the template while args?is an optional argument that we will use to filter value.

To use this filter, we can simply add it to the template as follows:

<div *ngFor="let user of usersList | filter">

Naturally, nothing happens. However, note that usersList is the property that goes through the pipe and is associated with thevalue argument in the transform method.

We will now work inside the transform method in FilterPipe.

The arguments of the transform method

First, we update the arguments of the transform method with this code:

transform(value: User[], filterString: string, property: string)

We discussed value above. The type of value is an array of objects that are shaped like the User interface. You can see that in model.ts in StackBlitz.

The second argument is filterString of type string. The value of filterString has to come from the template. Therefore, we will add a parameter to the pipe. The parameter will be filterBy that is the user input, eg, the string the user wants to use as a filter.

The third and last argument is property. Since our user objects contain several properties, we need to specify which property we want to use. For the sake of simplicity, I will hard code the string name as the second parameter of the filter pipe in the template.

This is an example. Usually, you shouldn’t hard code values ​​in your code.

In the template, the pipe becomes:

...
<div *ngFor="let user of usersList | filter: filterBy:'name'">
<ul>
<li>{{ user.name }}</li>
</ul>
</div>

where filteris the name of the pipe, filterByis the property we get from the input element thanks to two-way binding, and 'name' is the hardcoded property in the user objects.

The number of parameters you need is optional, according to your needs.

Filtering logic in the transform method

Now we work inside the transform method.

We start by adding some code to return every value when there is no filter.

if (value.length === 0 || !filterString) {
return value;
}

Then, we add the logic to filter based on the user’s string.

let filteredUsers: User[] = [];for (let user of value) {  if (user[property].includes(filterString)) {
filteredUsers.push(user);
}
}return filteredUsers;

Remember that property is hardcoded and it means name. value is the list of users from usersListand filterString is the string that a person wants to use as a filter, eg, filterBy.

Therefore, you can read this code as “for each user from userListif the user’s name includes the filterBystring, add this user to the filteredUsersarray” that is eventually returned.

This is FilterPipe so far:

At this point, the pipe allows us to filter names just fine.

Note that I added the JavaScript toLowerCase() method so that the filter is case insensitive. This is totally optional of course.

What about the expensive filtering operations?

Let’s take it one step further.

We add a simple button that adds a new user to usersList with this code:

<button (click)="onAddUser()">Add user</button>

The method looks like this:

onAddUser() {
this.usersList.push({
id: Math.floor(Math.random() * 10000),
name: 'Leanne Graham',
username: 'Bret',
...
});
}

On every click, one user gets pushed to usersList.

Here is the problem!

If you filter for Leanne and then you click on the button, the user is added to usersList but the filtered list is not rendered. Therefore, you won’t see the new users in the filtered list, even though you will see them as soon as you remove the filter.

Angular doesn’t rerun the pipe on the data every time composite arrays or objects change.

Pure and impure pipes

To be more precise, we need to talk about pure and impure pipes.

We have a pure pipe when there is a pure change to the input value. A pure change can be one of the following:

  • a change to a primitive input value (String, Number, Boolean, Symbol)
  • a change to an object reference (Date, Array, Function, Object)

A pure pipe uses pure functions. Given the same input, pure functions should always return the same output.

By default, pipes are pure because Angular ignores changes inside objects or arrays. Therefore, it won’t call a pure pipe when we add to an input array, like in our case, or update an input object property.

As you can guess, an object reference check is faster than a deep check for differences. Furthermore, when Angular skips the pipe execution, it also skips the view update.

For this reason, a pure pipe is preferable when the regular change detection strategy is fine.

When we need a deep check, like in the current example, we need to use impure pipes.

An impure pipe runs during every component change detection cycle. For instance, it could run for every keystroke or mouse move.

As you can guess, this become very expensive fast.

Having said that, implementing an impure pipe is as simple as declaring pure: false in the Pipe decorator.

@Pipe({
name: 'filter',
pure: false,
})

By adding the pure property to the Pipe decorator, we force FilterPipe to update every time data changes.

Add a log inside the pipe to visualize the frequency.

Using impure pipes might lead to performance and aggressive minimization issues. So use it carefully and only when really needed.

Find the code on StackBlitz or clone the app from GitHub.

As a side note, it is worth mentioning that the async pipe is impure. Roughly speaking, we could argue that “a pipe is impure when it is always checking for new input data.”

While the Angular async pipe optimizes change detection, it also removes the necessity to use subscribe and unsubscribe manually.

Furthermore, it is a good way to use an RxJS Declarative Pattern in Angular.

Leave a Comment