Deborah's Developer MindScape






         Tips and Techniques for Web and .NET developers.

August 29, 2017

Filtering in Angular

Filed under: Angular @ 1:09 pm
Tags:

One common requirement is to filter a list of information based on user entered criteria. This post covers several techniques for filtering in Angular.

The content of this post is based on Angular version >= 2.x unless explicitly stated otherwise.

UPDATE: 2/22/18 Added techniques for a more generic filter method that can filter any list.

Filter Pipe

In AngularJS (Angular v1) we could easily filter information for display using a filter pipe. However, these often caused performance issues, so a built in filter pipe was not provided in Angular (v2+). We could, of course, build a custom pipe to perform our filtering. But that could have the same performance implications, so is not recommended.

From the Angular documentation:

Angular doesn’t offer such pipes because they perform poorly and prevent aggressive minification.

So what’s the alternative? Again from the Angular documentation:

The Angular team and many experienced Angular developers strongly recommend moving filtering and sorting logic into the component itself. The component can expose a filteredHeroes or sortedHeroes property and take control over when and how often to execute the supporting logic. Any capabilities that you would have put in a pipe and shared across the app can be written in a filtering/sorting service and injected into the component.

Filtering in the Component Code

The key to filtering in code is to provide a property for the filtered list and bind to that property. And to ensure that the filtering is performed every time the user changes the filter criteria, create a setter for the filter criteria property.

Let’s walk through some sample code:

import { Component, OnInit } from '@angular/core';

import { IProduct } from './product';
import { ProductService } from './product.service';

@Component({
    templateUrl: './product-list.component.html',
    styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
    pageTitle: string = 'Product List';
    errorMessage: string;

    filteredProducts: IProduct[];
    products: IProduct[] = [];
    
    _listFilter: string;
    get listFilter(): string {
        return this._listFilter;
    }
    set listFilter(value: string) {
        this._listFilter = value;
        this.filteredProducts = this.listFilter ? this.performFilter(this.listFilter) : this.products;
    }

    constructor(private _productService: ProductService) {

    }

    performFilter(filterBy: string): IProduct[] {
        filterBy = filterBy.toLocaleLowerCase();
        return this.products.filter((product: IProduct) =>
              product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1);
    }

    ngOnInit(): void {
        this._productService.getProducts()
                .subscribe(products => {
                    this.products = products;
                    this.filteredProducts = this.products;
                },
                    error => this.errorMessage = <any>error);
    }
}

In addition to a products property that retains the complete list of products, we also have a filteredProducts property. In the HTML, we bind the displayed table to the filteredProducts property so only the filtered products are displayed in the list.

The listFilter property uses a getter and setter. A getter and setter provide a way to work with a property as a property, but still execute code when the property is retrieved (getter) or when the property is set (setter). In this example, we use the _listFilter property to hold the filter criteria value. We simply return that value in the getter. In the setter, we set the _listFilter property to the value passed into the setter and then perform the filter.

The x ? y : z syntax is a JavaScript conditional operator. It evaluates x and if x is true, it executes y. If x is false, it executes z. You can find out more about it here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator. In this specific example, the code is checking the listFilter property. If the property has a value, it calls the performFilter method. If the listFilter property has no value (meaning the user did not define filter criteria), then the filteredProducts property is set to the full list of products by assigning it to the products property.

In the HTML, we bind an input element to the listFilter property. As the user changes the filter criteria value, the setter is called and the filtering is re-executed.

We have a data service (ProductService) which retrieves the data. We inject that service into the constructor as we do any Angular service.

The performFilter method is where the filtering actually takes place.

The first line of this method converts the filterBy argument to lower case. When comparing strings, converting to lower case ensures that the matching is case insensitive.

The second line is using the JavaScript filter function (you can find our more about that here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)

We want to filter the full list of products, so we call the filter function on this.products, which is the array to filter. The filter processes each element in the array and compares it using the provided arrow function. If the arrow function returns true, the item is included in the filtered list. If the arrow function returns false, the item is not included in the filtered list.

Notice, however, that we don’t have a return statement here in our arrow function!

(product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1

That’s because a one-line arrow function implicitly returns the function’s value. If the arrow function was multiple lines, then we would need both curly braces AND a return statement:

(product: IProduct) => {
    console.log(product.productName);
    return product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1;
}

Looking closer at the arrow function, we want to filter the list of products to only those products with a product name containing the user-defined filter text. So in the arrow function, we check the productName property of the product. We first convert the property value to lower case so that we are comparing both the property value and the filter string as lower case values.

We then use the indexOf function to locate the location of the filterBy text within the productName property. The indexOf function returns –1 if it does not find the defined text. We don’t really care where it finds the text, only that it does not return a –1.

You can download a complete example that uses this filter in the APM-Final folder of this github repo: https://github.com/DeborahK/Angular-GettingStarted

Or to see this filter built step-by-step in a video, check out my course: http://bit.ly/Angular-GettingStarted

Filtering on Multiple Criteria

Filtering on multiple criteria is just a matter of modifying the perform filter method. For example, say we want to filter based on either the product name or the product description. The performFilter code would then look like this:

    performFilter(filterBy: string): IProduct[] {
        filterBy = filterBy.toLocaleLowerCase();
        return this.products.filter((product: IProduct) =>
              product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1 || 
              product.description.toLocaleLowerCase().indexOf(filterBy) !== -1);
    }

We’ve simply added an OR conditional operator. So if the text is found in the productName or in the description, the item will be included in the filtered list.

Filtering on Criteria Matching any Object Properties

There may be times that you want to generically filter based on the value of any of an object’s properties. For example, find a particular string in *any* property of a product. In this case, the performFilter code would look something like this:

    performFilter(filterBy: string): IProduct[] {
        filterBy = filterBy.toLocaleLowerCase();
        return this.products.filter((product: IProduct) => 
            Object.keys(product).some(prop => {
                let value = product[prop];
                if (typeof value === "string") {
                    value = value.toLocaleLowerCase();
                } 
                return value.toString().indexOf(filterBy) !== -1;
            })    
        );
    }

This code uses Object.keys(product) to access each of the product object properties. It then uses the JavaScript some function (detailed here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) which tests whether at least one element in the array of properties passes the test defined by the arrow function. So as soon as the filter text is found in any property, it stops iterating through the remaining properties and simply returns true to include the item in the filtered list.

In the some function’s arrow function, we first access the actual value of the property using product[prop]. In JavaScript, we can access the properties of an object as a set of key and value pairs. Unless we know for sure that all of our properties are strings, we’ll have to do some additional processing based on the type. So we’ll handle our property strings different from our numbers.

NOTE: This code was not tested with complex object graphs where a property of the object is itself another object. For that scenario, some additional code is required.

If the property value is a string, we convert it to lower case so that we can compare the lower case value to the lower case filter criteria. We then use the toString function to ensure the value is converted to a string before calling the indexOf function since indexOf only works on strings.

More Generic Filtering on Criteria Matching any Object Properties

Taking it another step further, you can make your filter method even more generic by allowing it to filter *any* list. For that, we pass in the desired list:

    performFilter(list: any[], filterBy: string | null): any[] {
        if (filterBy) {
            filterBy = filterBy.toLocaleLowerCase();
            return list.filter((item) =>
                Object.keys(item).some(prop => {
                    let value = item[prop];
                    if (typeof value === "string") {
                        value = value.toLocaleLowerCase();
                    }
                    return value.toString().indexOf(filterBy) !== -1;
                })
            );
        } else {
            return list;
        }
    }

This code is similar to the prior example, but passes in the list to sort. It has an extra check that ensures the filterBy has a value. If not, it simply returns the full list.

Note that this code uses the *any* type to work with any type of array. To strongly type the array, you can use a generic parameter as shown below:

    performFilter<T>(list: any[], filterBy: string | null): T[] {
        if (filterBy) {
            filterBy = filterBy.toLocaleLowerCase();
            return list.filter((item: T) =>
                Object.keys(item).some(prop => {
                    let value = item[prop];
                    if (typeof value === "string") {
                        value = value.toLocaleLowerCase();
                    }
                    return value.toString().indexOf(filterBy) !== -1;
                })
            );
        } else {
            return list;
        }
    }

This method is called as follows:

this.filteredProducts = this.performFilter<IProduct>(this.products, this.listFilter);

Generic Filtering on Criteria Matching a Specific Property

You may instead want a generic filter function that only matches one specific property as shown here:

    performFilter<T>(list: any[], prop: string, filterBy: string | null): T[] {
        if (filterBy) {
            filterBy = filterBy.toLocaleLowerCase();
            return list.filter((item: T) => {
                if (item.hasOwnProperty(prop)) {
                    let value = item[prop];
                    if (typeof value === "string") {
                        value = value.toLocaleLowerCase();
                    }
                    return value.toString().indexOf(filterBy) !== -1;
                }
            });
        } else {
            // No filter was provided so return the list
            return list;
        }
    }

This code requires the following parameters:

  • The list to filter
  • The string name of the property to use for the filtering
  • The string defining the filter criteria

The calling code would then look like this:

this.filteredProducts = this.performFilter<IProduct>(this.products, 'productName', this.listFilter);

The above code is filtering the list of products on the productName property using the defined filter criteria.

Filtering at the Server

If, however, you have hundreds or thousands of rows, downloading all of that data and filtering on the client may be just to inefficient. In that case, consider requesting the filter criteria first, then passing that criteria to your server, returning only the rows that match the criteria. Then you no longer need filtering on the client.

Enjoy!

RSS feed for comments on this post. TrackBack URI

Leave a comment

© 2023 Deborah's Developer MindScape   Provided by WPMU DEV -The WordPress Experts   Hosted by Microsoft MVPs