Filter

High Performance List Filtering in Ionic 2 & 3



·

Displaying a list with data is perhaps one of the most common elements of a mobile application, and adding the ability to search and filter those lists is also a common feature. We need to be able to do this in our Ionic 2 applications, and it needs to be done in a way that is going to provide a high level of performance.

The obvious answer would be to use a pipe. Pipes take some data, modify it in some way, and return it to be displayed to the user in the new format – perfect for what we need. Except it’s not.

If you’ve used Ionic 1 (or Angular 1) in the past, you may have used the default filter or orderBy pipes at some point. Many apps need to filter and sort data, and you can indeed do this with a pipe in Angular 2 if you want to, so why the heck isn’t it available by default? This is an intentional decision by the Angular team, and the reason for it is quite interesting. If you’re interested in reading more about it take a look at the pipes documentation.

The short answer is that it comes down to performance. Filtering and sorting data are expensive operations and, given how change detection works in Angular 2, if we were to use a pipe to filter data in a list these operations may have to be performed on every change detection cycle. Running expensive operations frequently is obviously not a great idea. Given that, the reasoning to leave these pipes out of Angular 2 makes sense:

The filter and orderBy have often been abused in Angular 1 apps, leading to complaints that Angular itself is slow. That charge is fair in the indirect sense that Angular 1 prepared this performance trap by offering filter and orderBy in the first place.angular.io

We’ve established that using pipes to filter data can be a bad idea, but that brings up the question: how do we filter data without pipes? It’s a common feature, and we can’t just abandon something because it performs poorly.

The answer is quite simple, we move the filtering logic to the component itself. Rather than filtering the data on the fly with a pipe, we can implement a function in our component to handle filtering the data.

In this tutorial we are going to walk through how to filter list data in Ionic 2, with a focus on performance and usability. Here’s a quick look at what we’ll be building:

Ionic 2 List Searching

Before we Get Started

Last updated for Ionic 3.9.2

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Generate a new Ionic 2 Application

Let’s start out by generating a new blank Ionic 2 application. The application is just going to have a single page that will display a list with a search bar.

Run the following command to generate a new Ionic 2 application:

ionic start ionic2-search-filter blank --v2

We’re also going to require a data provider to help with filtering data, so we will set that up now too.

Run the following command to generate a Data provider:

ionic g provider Data

In order to be able to use this throughout the application, we will need to make sure it is added to the providers array in the app.module.ts file.

Modify app.module.ts to reflect the following:

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { HttpModule } from '@angular/http';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { Data } from '../providers/data';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    BrowserModule,
    HttpModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    Data,
    {provide: ErrorHandler, useClass: IonicErrorHandler} 
  ]
})
export class AppModule {}

Now we’ve got everything we need set up, so let’s get started.

2. Basic Filtering

As I described in the introduction, we’re not going to be using a pipe to filter our data. Instead, we are going to handle it in our Data provider. The Data provider will store some static data, and we will create a function that we can call to return a subset of that data based on some search criteria.

Modify src/providers/data.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class Data {

    items: any;

    constructor(public http: Http) {

        this.items = [
            {title: 'one'},
            {title: 'two'},
            {title: 'three'},
            {title: 'four'},
            {title: 'five'},
            {title: 'six'}
        ]

    }

    filterItems(searchTerm){

        return this.items.filter((item) => {
            return item.title.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1;
        });     

    }

}

We set up an items member variable that we then fill with an array of data in the constructor. This is our default set of data. We then also implement a filterItems function that will take in a searchTerm and provide us back an array containing only the elements that match the search criteria.

When working with arrays, we have two very useful functions that we can take advantage of: ‘filter’ and ‘map’. If you would like an in-depth explanation of these I would recommend going through this great tutorial. In short, a “filter” will remove elements from an array based on some function a “map” will change elements in an array based on some function.

In the code above we filter the items array by providing filter with a function that will only return true for items where the searchTerm is contained somewhere within the title string. We then return that filtered array, which will now only contain items that match the search criteria.

Modify home.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Data } from '../../providers/data';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    searchTerm: string = '';
    items: any;

    constructor(public navCtrl: NavController, public dataService: Data) {

    }

    ionViewDidLoad() {

        this.setFilteredItems();

    }

    setFilteredItems() {

        this.items = this.dataService.filterItems(this.searchTerm);

    }
}

Now that we have a way to filter the data, we just need to take advantage of it. In this class, we also have an items member variable that we will use to store the filtered array returned from the Data provider. We also have a searchTerm that the user will be able to control, and this is supplied to the setFilteredItems function which will call the filter function we just defined in the provider.

We initially call setFilteredItems with no searchTerm so that the default data will display, and then every time we call setFilteredItems() after that, it will return an array of data based on whatever the current searchTerm is set as.

Modify home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content>

    <ion-searchbar [(ngModel)]="searchTerm" (ionInput)="setFilteredItems()"></ion-searchbar>

    <ion-list>
        <ion-item *ngFor="let item of items">
            {{item.title}}
        </ion-item>
    </ion-list>

</ion-content>

We’ve added a search bar here so that the user can define their own search term. We have two-way data binding set up on searchTerm, so as soon as the user modifies the value in <ion-searchbar> the value in our class will also change. Then we listen for the (ionInput) event to detect when the user has modified the search, and we call setFilteredItems() to trigger the filtering of the data.

3. Filtering with Observables

We have a list set up now that filters data without using a pipe, but we can take that even further. We’re currently searching every time the user types a value into the search bar.

If I’m searching for coffee I don’t need to see the results for c, co, cof, coff, and coffe. I don’t need the search to happen until I’m done typing. Ideally, we want to give the user some time to finish typing their search before triggering a search, and we can do this quite easily with an Observable.

Angular 2 provides an Observable that we can use with inputs that can be accessed through Control. This will allow us to listen for input in a more advanced way. Let’s take a look.

Modify home.ts to reflect the following:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { NavController } from 'ionic-angular';
import { Data } from '../../providers/data';
import 'rxjs/add/operator/debounceTime';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    searchTerm: string = '';
    searchControl: FormControl;
    items: any;

    constructor(public navCtrl: NavController, public dataService: Data) {
        this.searchControl = new FormControl();
    }

    ionViewDidLoad() {

        this.setFilteredItems();

        this.searchControl.valueChanges.debounceTime(700).subscribe(search => {

            this.setFilteredItems();

        });


    }

    setFilteredItems() {

        this.items = this.dataService.filterItems(this.searchTerm);

    }
}

We are now importing FormControl and the debounceTime operator. We set up a new member variable called searchControl, and then we create a new instance of Control and assign it to that. You will see how we then tie that to the <ion-searchbar> in our template in just a moment.

With the FormControl set up, we can subscribe to the valueChanges observable that will emit some data every time that the value of the input field changes. We don’t just listen for changes though, we also chain the debounceTime operator, which allows us to specify a time that we want to wait before triggering the observable. If the value changes again before the debounceTime has expired, then it won’t be triggered. This will stop the setFilteredItems function from being spam called and, depending on how fast the user types, it should only be called once per search. In this case, we are setting a debounceTime of 700 milliseconds, but you can tweak this to be whatever you like.

Modify home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content>

    <ion-searchbar [(ngModel)]="searchTerm" [formControl]="searchControl"></ion-searchbar>

    <ion-list>
        <ion-item *ngFor="let item of items">
            {{item.title}}
        </ion-item>
    </ion-list>

</ion-content>

The final piece of the puzzle is to connect our <ion-searchbar> to the searchControl we created, which we do here with [formControl]. Also notice that we have removed the (ionInput) listener, because the filtering function will now be triggered by the Observable.

4. Improving the User Experience

Our list filtering is designed pretty nicely now, and it should perform very well. However, since we have added this debounceTime it causes a slight delay that will certainly be perceptible to the user. Whenever we do something “in the background” we should indicate to the user that something is happening, otherwise it will appear as if the interface is lagging or broken.

A delay is fine, and an artificial delay is sometimes even beneficial, but you definitely don’t want to leave the user wondering “is this just slow or is it frozen?”.

We’re going to make a change now that won’t have any effect on performance, but it will have an impact on the user’s perception of the responsiveness of the app. We’re simply going to add a loading spinner that will display when a search is in progress.

Modify home.html to reflect the following:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content>

    <ion-searchbar [(ngModel)]="searchTerm" [formControl]="searchControl" (ionInput)="onSearchInput()"></ion-searchbar>

    <div *ngIf="searching" class="spinner-container">
        <ion-spinner></ion-spinner>
    </div>

    <ion-list>
        <ion-item *ngFor="let item of items">
            {{item.title}}
        </ion-item>
    </ion-list>

</ion-content>

We’ve added an <ion-spinner> now, which simply shows a little spinner animation, and we are only displaying that when searching evaluates to be true (we will define this in a moment). Also notice that we’ve added the (ionInput) listener back in, but this time we will be using it to determine when the user is searching.

Modify home.ts to reflect the following:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { NavController } from 'ionic-angular';
import { Data } from '../../providers/data';
import 'rxjs/add/operator/debounceTime';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    searchTerm: string = '';
    searchControl: FormControl;
    items: any;
    searching: any = false;

    constructor(public navCtrl: NavController, public dataService: Data) {
        this.searchControl = new FormControl();
    }

    ionViewDidLoad() {

        this.setFilteredItems();

        this.searchControl.valueChanges.debounceTime(700).subscribe(search => {

            this.searching = false;
            this.setFilteredItems();

        });


    }

    onSearchInput(){
        this.searching = true;
    }

    setFilteredItems() {

        this.items = this.dataService.filterItems(this.searchTerm);

    }
}

Initially, we set searching to be false, and we set it to be true when the user starts searching. Once our observable triggers and we call the filter function, we set searching to be false again so that the spinner is hidden.

Now we just need to add a bit of styling to the spinner to center it.

Modify home.scss to reflect the following:

page-home {

    .spinner-container {
        width: 100%;
        text-align: center;
        padding: 10px;
    }

}

Summary

The methods described in this tutorial should help you create high-performance list filtering for just about any scenario. It’s not always necessary to use the Observable based approach when filtering for basic lists, but you will certainly see a performance improvement for filtering large lists of data.

What to watch next...