One of the most common functionalities any app has is showing lists of data.

A contact list, a task list, you name it.

Sometimes the list is so long that we can’t easily find what we’re looking for. For this, it’s a good idea to add filtering.

Today, we’ll learn how to integrate a search bar in our application, to filter a list of data we’ll get from Firestore. (Firestore is one of the places data can come from, you can fetch the data from your own APIs)

Let’s get coding

The View

The first thing we’ll do is to create our view, it’s going to be something simple so that we can focus on the functionality, open the home.page.html file and create the search bar component and a list to loop through the items.

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> Blank </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-searchbar [formControl]="searchField" debounce="100"></ion-searchbar>

  <ion-list lines="none">
    <ion-item *ngFor="let foodItem of foodList$ | async">
      <ion-label>{{ foodItem.name }}</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

The <ion-searchbar> is an Ionic component that gives us a really nice looking search bar at the top of our file.

The [formControl]="searchField" is using Angular’s Reactive Forms to bind this searchbar to a FormControl called searchField.

The debounce="100" is the time in miliseconds the searchbar waits to emit values on key presses, so if you set it a long time, like 2000 miliseconds, and in that span of time you press A B C, it’s going to emit the value ABC instead of each key individually.

And below it, we have a regular <ion-list>, where we’re looping through an Observable called foodList$, and we’re using the async pipe to unwrap it and subscribe to it directly in the template.

A couple of things you need to keep in mind:

formControl is part of the ReactiveFormsModule, so we need to add it to our current module’s imports array:

...
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  ...,
  imports: [
    ...,
    ReactiveFormsModule,
  ],
})
export class HomePageModule {}

And you’ll get a lot of errors in the template, because none of the properties we’re declaring exist yet 😅

Now, our job is to:

  • Create all the variables we need.
  • Get the data from Firestore.
  • Create the Observable that will merge everything together and filter the list.

Creating the variables

Go ahead and open home.page.ts, this is where our work will happen, the class doesn’t have much yet, so it should look a bit like this:

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

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  constructor() {}
}

First, we’ll create the searchField control we are binding in the template, for that, let’s import FormControl from Angular Forms, and create the class variable:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

export class HomePage {
  searchField: FormControl;

  constructor() {
    this.searchField = new FormControl('');
  }
}

Notice that we’re also initializing it in the constructor using new FormControl('') with a value of ''.

Next, we want to create the foodList$ variable, this is going to be an Observable.

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';

export class HomePage {
  searchField: FormControl;
  foodList$: Observable<FoodItem[]>;

  constructor() {
    this.searchField = new FormControl('');
  }
}

interface FoodItem {
  name: string;
}

Notice that we did 3 things here:

  • We imported Observable from rxjs.
  • We created foodList$ and gave it a type of Observable<FoodItem[]>, meaning that Observable contains a list of food items.
  • We created the FoodItem interface (Which is optional, I just like having it typed like that).

Now all the work is going to happen inside of the ngOnInit() lifecycle hook, but before we get into that, we need to make 2 things to make ngOnInit() work.

We need to import OnInit, and tell our class it needs to implement it:

import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { Firestore } from '@angular/fire/firestore';

export class HomePage implement OnInit{
  searchField: FormControl;
  foodList$: Observable<FoodItem[]>;

  constructor(private readonly firestore: Firestore) {
    this.searchField = new FormControl('');
  }

  ngOnInit() {}
}

interface FoodItem {
  name: string;
}

We also injected our instance of Firestore into the constructor, but this is optional, Firestore is where I’m getting the data right now, but you could have gotten the data from anywhere you want.

Inside of our ngOnInit() method, the first thing we want to do is to create an observable for the value of the searchterm we’re getting from our searchbar.

We can do that with ReactiveForms, listening to an Observable that streams the value of our FormControl.

For the next few code snippets, I’m going to only work on the OnInit function, and I’ll be adding the relevant import for rxjs and firestore.

import { Firestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';

async ngOnInit() {
  const searchTerm$ = this.searchField.valueChanges;
}

The form control has a property called valueChanges which is an observable that streams whatever we have for the form control’s value.

There’s one small issue here, it won’t emit any value until we start typing in the searchbar, and we want it to emit an initial value of '' so that we can show our unfiltered list.

For that, we can use an rxjs operator called startWith:

import { Firestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { startWith } from 'rxjs/operators';

async ngOnInit() {
  const searchTerm$ = this.searchField.valueChanges.pipe(
    startWith(this.searchField.value)
  );
}

We’re letting our Observable know, that the first value it’s going to emit is the value we used to initialize our form control, in this case, ''.

Then, we’re going to get the list of items from Firestore:

import { Firestore, collectionData, query, collection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { startWith } from 'rxjs/operators';

async ngOnInit() {
  const searchTerm$ = this.searchField.valueChanges.pipe(
    startWith(this.searchField.value)
  );

  const foodList$ = collectionData(query(collection(this.firestore, 'foodList')));
}

Let’s zoom into this line:

const foodList$ = collectionData(query(collection(this.firestore, 'foodList')));

In this line, collectionData() is returning an Observable from a Firestore query. Our query is only passing the reference to the collection we want to get, without any conditions, this means it will bring all of the documents we have in the foodList collection.

And lastly, we want to plug everything together to feed the foodList$ observable with some real data.

import { Firestore, collectionData, query, collection } from '@angular/fire/firestore';
import { combineLatest, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

async ngOnInit() {
  const searchTerm$ = this.searchField.valueChanges.pipe(
    startWith(this.searchField.value)
  );

  const foodList$ = collectionData(query(collection(this.firestore, 'foodList')));

  this.foodList$ = combineLatest([foodList$, searchTerm$]).pipe(
    map(([foodList, searchTerm]) =>
      foodList.filter(
        (foodItem) =>
          searchTerm === '' ||
          foodItem.name.toLowerCase().includes(searchTerm.toLowerCase())
      )
    )
  );
}

Let’s zoom in this last change, because it is doing a lot of things:

this.foodList$ = combineLatest([foodList$, searchTerm$]).pipe(
  map(([foodList, searchTerm]) =>
    foodList.filter(
      foodItem =>
        searchTerm === '' || foodItem.name.toLowerCase().includes(searchTerm.toLowerCase())
    )
  )
);

Here’s the breakdown:

First, we’re telling our app that this.foodList$ = combineLatest([foodList$, searchTerm$]);

Reading from the docs, combineLatest works this way: “When any observable emits a value, emit the last emitted value from each”.

Which means that, when any of the observables we have in there (foodList$, searchTerm$) emits a value, combine latest is going to create a new Observable with the values emited from the observables it’s getting as parameters.

Since that’s not what we need, we need to use those values and transform them a little bit into something that fits our use case, that’s where .pipe() comes into play.

We can pipe() operators together to access the interval values of our observables, modify them, and then return new Observables out of those changes.

In this case we’re using the map() operator, which is taking the internal values of those Observables, so the actual foodList array, and the actual string value we get from our searchbar.

this.foodList$ = combineLatest([foodList$, searchTerm$]).pipe(map(([foodList, searchTerm]) => {}));

What the map() operator does is that it let’s you take that combination of values, and return one modified value instead.

In our case, we’re telling it we want to return the actual food list, but not with all of the items, we’re using the Array.filter() method to tell it to only return an item if the item’s name includes the string we’re passing in our searchbar.

this.foodList$ = combineLatest([foodList$, searchTerm$]).pipe(
  map(([foodList, searchTerm]) =>
    foodList.filter(
      foodItem =>
        searchTerm === '' || foodItem.name.toLowerCase().includes(searchTerm.toLowerCase())
    )
  )
);

Oh, and we’re also telling it to return ALL of the items, if the searchTerm === '', which, if you recall correctly is the initial value we’re emitting for the searchTerm$ Observable.

By the way, filter() is an array function, if like me, you jumped into Ionic development without a JS background, I recommend you take either Beginner JavaScript or ES6 for Everyone from Wes Bos, the ES6 course helped me learn enough JS that I started realizing how many things JS already does I was trying to re-invent, allowing me to remove a lot of code I didn’t need in the first place.

And that’s it', every time you type something it’s going to try and match it against the food’s name property.

If you have any questions or think I might be able to help with anything don’t hesitate to reach out, you can easily find me on Twitter as @javebratt

Also, the entire source code is available at Github, you’ll find 2 branches there, main contains this one, and there’s another branch that contains the previous promise-based code.