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.3.0

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...

  • Pie Cy

    since you done soundcloud tutorial on ionic 2 can you also have a tutorial for Paypal Integration in Ionic 2? i am headache about the paypa mobile integration thing

  • blazs

    On section 2 you mistake on second code example. It says “modify home.html” instead “home.ts”

    Good work!

  • giri

    Wow this is amazing….Keep up the good work

  • Edouard Jubert

    Hey boss! Great tutorial like always! I applied this to a firebase list of email, but when I load the page, nothing appears, even after a while. Then I close and open the page, and all email show perfectly… Do you have any idea where does that error could come from?

    • Lynette Winkel-Kelly

      Hi did you figure this out?

      • Edouard Jubert

        At the time, my workaround was to charge the email list on the previous page. Now I switched for AngularFire2 with Ionic RC0, it is still a little bit edgy, but really cool.
        You have the same problem?

  • Julio Cesar Rodrigues

    Great,
    Just one update…
    In the last version of Angular 2 is necessary to import FormControl from “@angular/forms” instead of Control from ‘@angular/common’
    thank you so much

  • Karen Di Stefano

    Hi Josh, what’s the different between use the “debounce” option of “ion-searchbar” https://ionicframework.com/docs/v2/api/components/searchbar/Searchbar/ and this method ?

    thank you for all. Your tutorials are very helpfull

    • The example in this tutorial sets up its own observable to watch for changes rather than using the ionInput listener. There wouldn’t really be any difference if you instead used a combination of ionInput and the debounce setting Ionic provides. The method in this tutorial might be a bit easier if you want to chain more things onto the observable, like distinctUntilChanged.

  • Anton

    Hi Josh,Can I use with .filter data of type – Array ? I am trying filtering data from database answer.

    • Akmal

      Hi @disqus_c9RTHrzmz7:disqus. Did you able to filter data from database?

  • Anton

    Hi Josh! Is it possible to download big file on deviсe memory ?

  • terrycollinson1

    Firstly these tutorials are excellent. Thanks so much. On this one I am getting the following error though – debounceTime does not exist on type Observable. Any suggestions?

    • Sarah Tully

      You need to add the import for it: import ‘rxjs/add/operator/debounceTime’;

  • Paulo

    Nice, thanks for the tutorial, I think there’s a problem with when the items are changed within the data service. The component will not pick up the changes until the user searches for something again, am I right? How would you address that?

  • Foued

    i think you may remove (ionInput)=”onSearchInput()” for better performance

  • Damiano Salvemini

    I have this error: Cannot find control with unspecified name attribute
    Any suggestions?

  • mansoor

    i have this eror plz help me
    Error in ./HomePage class HomePage – inline template:11:0 caused by: Failed to execute ‘setAttribute’ on ‘Element’: ‘)’ is not a valid attribute name.

  • Ayyanan Gopalakrishnan

    Plz help me
    i have this error: Property ‘dataService’ does not exist on type ‘SearchPage’

  • dimitri

    HI Josh,
    Any idea how you would implement an autocomplete functionality component in such a list. Not a search bar but a drop down filter list as the user types… This could be a great and useful component that is still lacking in the framework

    • Yasir Panjsheri

      hi, I am looking for the same thing…have you found anything ?

      • dimitri

        No not yet I am afraid.I am still looking into it . For now I am using a modal with a search bar. The ionic framework has not implemented it either which is strange since this would be a cool feature. https://github.com/driftyco/ionic/issues/6921

  • narayana petla

    if i select some element it will not selected from list how could i solve this one pls help me

  • Manish Sankari

    Thank you so much Josh.

  • Manish Sankari

    Can you help to solve search box in ionic 2 like this : http://codepen.io/calendee/pen/pwcGk

  • Yasir Panjsheri

    hi, what if the input has no match in the list… but I still want to select that input item and added it ! how to do we approach this

  • Shivansh Subnani

    hii i followed ur code and i am facing an error that Error: No provider for Http!

    • Gregor Srdic

      in app.module.ts, you probably need to add HttpModule int your module imports:
      import { HttpModule } from ‘@angular/http’;
      @NgModule({
      …,
      imports: [
      …,
      HttpModule
      ],

      })

  • Damien Storm

    Thank you, Josh, for an amazing series of ionic tutorial! I completed the first part of your tutorial without any problems, and then I switched to loading remote data as opposed to the hard coded data used in the tutorial. The data loads fine, however when I try to apply filtering to the data I get a Runtime Error ‘this.items is undefined’. The only change I made to the code is using the http.get command in the constructor on data.ts to load the remote data. I have included screenshots of both the data.ts and the home.ts files (I edited the url to my json file – it works fine). Thank you very much for your help on this!! https://uploads.disquscdn.com/images/73f49c30c495d87d0ff63609018c51718417c118934ce15ff2cce572799111c9.png https://uploads.disquscdn.com/images/30c3c8cb0a1669380f5b672965db768958bf52aec72cbca66100ff17e40b1987.png https://uploads.disquscdn.com/images/ce40e4d20c84dd98671acd7fa552775ea1adb2bf93482431b05040fc62b82e34.png https://uploads.disquscdn.com/images/e66dd9fa9159e6b97b4cdde98df9decd75b9300479a4b723f2a5eeac8d8d038f.png

  • Sharanagouda K

    I am getting Error here title.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1;
    like IndexOf method not found

  • Michal Nosil

    Hi, I try make my results from http. But I have data.ts function dataInitItems that get all my results.
    I need from this method return all results, but I dont know regulary syntax. My URL get good results, I changed to this post.
    export class Data {
    items: any;
    islogin : boolean = false;
    lastdata : any ;
    temp: any;
    constructor(public storage: Storage, public http: Http){
    }

    dataInitItems(){
    this.http.get(‘https://www.xxxx.cz/?getproducts=1&data=1’).map(res => res.json()).subscribe(data => {
    this.items = data.products;
    });
    };

    I tried this, but it didnt work.
    dataInitItems(){
    return this.http.get(‘https://www.xxxx.cz/?getproducts=1&onlykavovars=1’).map(res => res.json()).subscribe(data => {
    this.items = data.products;
    });

    THX

  • itolduco

    Hi, guys!

    i’m followed the tutorial, and it’s work perfect.

    But i have no idea how can i implement the search over http call,
    when the json or data is coming from Database.

    Can someone help me please.

    this what i got on my service

    import { Injectable, OnInit } from ‘@angular/core’;
    import { Http } from ‘@angular/http’;
    import {List} from ‘./editor-list’;
    import ‘rxjs/add/operator/map’;

    @Injectable()
    export class EditorListProvider implements OnInit{
    items:List[];
    constructor(public http: Http) {
    console.log(‘Hello EditorListProvider Provider’);

    this.items = [];
    }

    getJsonData(){
    return this.http.get(‘http://deveditor.dev:8000/api/v1/treetable’).map(res => res.json());
    }

    filterItems(searchTerm){

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

    }

    }

    export interface List {
    uuid:string;
    name:string;
    Parent_ID:number;
    ContentStart_ID:number
    }

    and i have no idea how i can pass this.items on constructor to replace with the data coming from api

    • Prateek Kumar

      ;ppp

  • Yasir

    onSearchInput() triggers when closing keyboard !!! leading to show spinner , how to solve this?

  • George Nasis

    Faboulus!Straight forward!It run with the first shot!Keep up the good work Josh!Thank you!

  • paola belli

    Thank you Josh for your explanation! If I would filter json file data, who can I do?
    To be precise, I have an rss file, I used api rss2json to fetch data and putting thm in a ionic list.
    Now, where I have to edit?