Angular search autosuggest with Observables

Simple and clean Angular search autosuggest

In this post, I’ll show you how to implement a search auto suggest feature, with Angular and the secret power of Observables.

Source code on github, and plunker example (just add your spotify api client ID to make it works).

AS you may already now, Angular massively use rxjs behind the scene to handle all sort of asynchronous tasks like Http request, reactive forms, emitting events…etc. You can make the most of the Observables to write less code, and make your workflow easier.

What we are building

We will build a simple and clean Angular search component. When the user starts tipping his search query, results that match should be displayed below the search input inside a <ul> tags. Now let’s write some code!

Bootstrapping the application

I use the angular-cli to bootstrap the application.

> npm install -g angular-cli
> ng new angular-search
> cd angular-search & npm install
> ng serve

At this point your console’s output should look like this :

Sering the application on localhost:4200

A local development server will start, you can navigate to in your browser on http://localhost:4200.

Creating the search component

Now we are going to create our search component and add to it the simple html and css code.

> ng new component search

this command will create for us all the component related file and put them into the newly created search folder, in addition to register the component in the AppModule so we can us it in the hole application.

├── src
│   ├── app
│   │   ├── app.component.css
│   │   ├── app.component.html
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── search
│   │       ├── search.component.html
├── search.component.css
│   │       └── search.component.ts

Adding our html and css

In the search.component.html file, add the HTML code :

<section class="filter-wrapper">
<div class="keyword-wrapper">
<input type="text" id="keyword" placeholder="Search for artists..." autofocus/>
</div>
<ul class="filter-select">
<li class="filter-select-list"><img src="" alt="" width="50" height="50"><p class="artist-name"></p>
<span class="tags"></span>
</ul>
</section>

it’s just a simple text input for search and a <ul> element which will host the search result suggestions. then, add the CSS code in your style.css file, which is located in the root of your applications.

*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: border-box
}
html,
body {
height: calc(100% + 1px)
}
body {
font: 100% 'Arimo', sans-serif;
}
.filter-wrapper,
.keyword-wrapper {
display: flex;
justify-content: center;
}
.filter-wrapper {
min-height: 100%;
flex-flow: column wrap;;
position: relative
}
.keyword-wrapper {
width: 100%;
position: relative;
}
#keyword {
border: 1px solid #ccc;
padding: 10px;
font: 1.5em 'Arimo', sans-serif;
width: 50%;
outline: none;
transition: border 0.5s ease-in-out
}
#keyword:focus {
border-color : rgba(81, 203, 238, 1);;
}
#keyword-button {
position: absolute;
right: 26%;
top: 50%;
transform: translateY(-50%);
font-size: 1.7em;
color: #8DB9ED
}
#keyword-button:hover {
color: #ccc
}
.filter-select {
width: 50%;
list-style: none;
font-size: 1.1em;
color: rgb(105, 105, 105);
border: 1px solid #ccc;
border-top: none;
/*so things don't jump around*/
position: absolute;
left: 25%;
top: calc(50% + 25px);
max-height: calc(50% - 15px);
overflow-y: auto;
background: #fff
}
.filter-select-list img {
margin-right: 30px;
}
.tags {
font-size: 12px;
font-style: italic;
color: #c6c6c6;
margin-right: 10px;
position: relative;
top: -10px;
}
.filter-select-list:hover .tags {
color: #fff;
}
.filter-select-list {
cursor: pointer;
padding: 5px 10px;
}
.artist-name {
display: inline-block;
position: absolute;
}
.filter-select-list:hover {
background: #C0C0C0;
color: #fff
}
.list-highlight,
.list-highlight:hover {
background: rgb(55, 55, 55);
color: #fff
}
@media only screen and (max-width: 768px) {
.filter-select,
#keyword {
width: 80%;
}
#keyword {
font-size: 1.3em
}
.filter-select {
font-size: 0.9em;
left: 10%;
top: calc(50% + 23px);
}
#keyword-button {
right: 11%
}
}
@media only screen and (max-width: 480px) {
.filter-select,
#keyword {
width: 95%;
}
.filter-select {
left: 2.5%;
}
#keyword-button {
right: 3.5%
}
}

Before we can see the search input in action, we need to the search component inside the AppComponent template:

// [app.component.html]
<app-search></app-search>

If you navigate to localhost:4200 you should see the form input which does nothing at for moment.

Bringing some data

For the purpose of this tutorial, I’ll use the Spotify API to have a real database for the search. In order to use the Spotify API, you have to create a developer account and create an application here, you’ll be asked to give a name and a description to your app.

spotify application

Your client ID and client Secret have been generated, you need the client ID for making the search requests which I’ll talk about in a moment.

The spotify search endpoint is : https://api.spotify.com/v1/search

The api have a lot of parameters that we can use, but w’ll only need 4 of them which are :

type : artist, playlist, album, or track, we’ill use the artist type.

limit : the number of artists the api should return.

client_id : the client ID generated in your spotify account.

q : the search query tapped by the user in the search input.

Creating our search service

lets create our search service :

> ng generate service ./search/search

This command will create a search.service.ts file in the search folder, and register this service as a provider in app.module.ts :

// [search.Service.ts]
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
@Injectable()
export class SearchService {
clientID: string = 'PAST YOUR CLIENT ID';
baseUrl: string = 'https://api.spotify.com/v1/search?type=artist&limit=10&client_id=' + this.clientID + '&q=';
constructor(private _http: Http) { }
search(queryString: string) {
let _URL = this.baseUrl + queryString;
return this._http.get(_URL);
}
}
// [app.module.ts]
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { SearchComponent } from './search/search.component';
import { SearchService } from './search/search.service';
@NgModule({
declarations: [
AppComponent,
SearchComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
HttpModule
],
providers: [SearchService],
bootstrap: [AppComponent]
})
export class AppModule { }

The service simply make a GET request to the Spotify api, et return the search result as an observable.

Implementing the autosuggest featur

Angular has observables behavious already availible in a number of places. One of them is inside ReactiveFormsModules, which allows you to use an Observable that is attached to a form input. To do that, we have convert our input to use FormControl which expose a valueChange Observable:

// [app.module.ts]
import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;
import { ReactiveFormsModule } from ‘@angular/forms’;
import { HttpModule } from ‘@angular/http’;
import { AppComponent } from ‘./app.component’;
import { SearchComponent } from ‘./search/search.component’;
@NgModule({
declarations: [
AppComponent,
SearchComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
// [search.component.ts]
import { Component, OnInit } from ‘@angular/core’;
import { SearchService } from ‘../search/search.service’;
import { FormControl } from ‘@angular/forms’;
import ‘rxjs/add/operator/debounceTime’;
import ‘rxjs/add/operator/distinctUntilChanged’;
import ‘rxjs/add/operator/switchMap’;
@Component({
selector: ‘app-search’,
templateUrl: ['./search.component.html',
styleUrls: [‘./search.component.css’]
})
export class SearchComponent implements OnInit {
results: any[] = [];
queryField: FormControl = new FormControl();

constructor(private _searchService: SearchService) { }
ngOnInit() {
this.queryField.valueChanges
.subscribe( result => console.log(result);

}
}

in the ngOnInit lifecycle hook, we subscribed to the values emitted by the queryField, and logged the results so that you can see the values emitted:

chrome developer console output

Making the search request

Instead of printing the values in the console, we are going to make a new search request every time a new value is emitted, then store the response object in the results variable to finally display them in our HTML with ngFor :

// [search.component.ts]
ngOnInit() {
this.queryField.valueChanges
.subscribe(queryField =>this._searchService.search(queryField)
.subscribe(response => this.results = this.response.json().artists.items);
}
}
// [search.component.html]
<section class="filter-wrapper">
<div class="keyword-wrapper">
<input [formControl]="queryField" type="text" id="keyword" placeholder="search for artists..." autofocus/>
</div>
<ul class="filter-select">
<li *ngFor="let result of results" class="filter-select-list"><img src="{{result.images['2']?.url}}" alt="" width="50" height="50"><p class="artist-name">
{{result.name}}</p>
<span class="tags" *ngFor='let genre of result?.genres | slice:0:6'>{{genre}}</span>
</ul>
</section>

In this code, we subscribe to the value emitted by the search input and pass that values to the search method located in the SearchService.

Debouncing the input

Each time the input value changes, Angular will fire off a request and handle the response as soon as it is ready. In the case where the useris querying a long term, such as ilikebananassoomuch, it may be necessary for you to only send off a single request once you think the user is done with typings.

RxJS observable have this built in. debounceTime(delay) will create a new observable tha only pass along the latest value when there haven’t been any other values for <delay> ms. In our case, 200 ms will be suitable for our purpose.

ngOnInit() {
this.queryField.valueChanges
.debounceTime(200)
.subscribe(queryField =>this._searchService.search(queryField)
.subscribe(response => this.results = this.response.json().artists.items);
}
}

Ignoring serial duplicates

Since you are reading input from a textbox, it is very possible that the user will type one character, then type another character and press backspace. From the perspective of the Observable, since it is now debounced by a delay period, it is entirely possible that the user input will be interpreted in such a way that the debounced output will emit two identical values sequentially. RxJS offers excellent protection against this, distinctUntilChanged(), whichwill discard an emission that will be a duplicate of its immediate predecessor:

ngOnInit() {
this.queryField.valueChanges
.debounceTime(200)
.distinctUntilChanged()
.subscribe(queryField =>this._searchService.search(queryField)
.subscribe(response => this.results = this.response.json().artists.items);
}
}

Flattening Observables and Handling unordered responses

When testing input now, you will surely notice that the delay intentionally introduced inside the API service will cause the responses to be returned out of order. This is a pretty effective simulation of network latency, so you’ll need a good way of handling this.

RxJS have the switchMap, which which flattens all the emissions from the inner Observables into a single outer Observable (exactly like flapMap), in adition to unsubscribing from any in-flight Observables that have not emitted any values yet.

ngOnInit() {
this.queryField.valueChanges
.debounceTime(200)
.distinctUntilChanged()
.switchMap((query) => this._searchService.search(query))
.subscribe( result => { if (result.status === 400) { return; } else { this.results = result.json().artists.items; }
});
}
}

Your autosuggest input should now be debounced and it should ignore redundant requests and return in-order results.

Conslusion

Angular and RxJS really change the way we think about single page applications since it handles events as s stream of data, on which you can make all sort of data manipulation like debouncing, mapping to values, converting to promise…etc.

if you like this article, please recommend it by clicking the ❤ button on the side .

Like what you read? Give Nacim Idjakirene a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.