Create type ahead search using RxJS and Angular standalone components
Connie Leung
Posted on February 25, 2023
Introduction
This is day 6 of Wes Bos's JavaScript 30 challenge where I create a type ahead search box that filters out cities/states in the USA. In the tutorial, I created the components using RxJS, custom operators, Angular standalone components and removed the NgModules.
In this blog post, I define a function that injects HttpClient, retrieves USA cities from external JSON file and caches the response. Next, I create an observable that emits search input to filter out USA cities and states. Finally, I use async pipe to resolve the observable in the inline template to render the matching results.
Create a new Angular project
ng generate application day6-ng-type-ahead
Bootstrap AppComponent
First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.
// app.component.ts
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TypeAheadComponent } from './type-ahead/type-ahead/type-ahead.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [
TypeAheadComponent
],
template: '<app-type-ahead></app-type-ahead>',
styles: [`
:host {
display: block;
}
`]
})
export class AppComponent {
title = 'Day 6 NG Type Ahead';
constructor(titleService: Title) {
titleService.setTitle(this.title);
}
}
In Component decorator, I put standalone: true to convert AppComponent into a standalone component.
Instead of importing TypeAheadComponent in AppModule, I import TypeAheadComponent (that is also a standalone component) in the imports array because the inline template references it.
// main.ts
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
bootstrapApplication(AppComponent,
{
providers: [provideHttpClient()]
})
.catch(err => console.error(err));
provideHttpClient
is a function that configures HttpClient to be available for injection.
Next, I delete AppModule because it is not used anymore.
Declare Type Ahead component
I declare standalone component, TypeAheadComponent, to create a component with search box. To verify the component is a standalone, standalone: true
is specified in the Component decorator.
src/app
├── app.component.ts
└── type-ahead
├── custom-operators
│ └── find-cities.operator.ts
├── interfaces
│ └── city.interface.ts
├── pipes
│ ├── highlight-suggestion.pipe.ts
│ └── index.ts
└── type-ahead
├── type-ahead.component.scss
└── type-ahead.component.ts
find-cities.ts
is a custom RxJS operator that receives search value and filters out USA cities and states by it.
// type-ahead.component.ts
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, OnInit, ViewChild, inject } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { Observable, shareReplay } from 'rxjs';
import { findCities } from '../custom-operators/find-cities.operator';
import { City } from '../interfaces/city.interface';
import { HighlightSuggestionPipe } from '../pipes/highlight-suggestion.pipe';
const getCities = () => {
const httpService = inject(HttpClient);
const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
return httpService.get<City[]>(endpoint).pipe(shareReplay(1));
}
@Component({
selector: 'app-type-ahead',
standalone: true,
imports: [
HighlightSuggestionPipe,
FormsModule,
CommonModule,
],
template: `
<form class="search-form" #searchForm="ngForm">
<input type="text" class="search" placeholder="City or State" [(ngModel)]="searchValue" name="searchValue">
<ul class="suggestions" *ngIf="suggestions$ | async as suggestions">
<ng-container *ngTemplateOutlet="suggestions?.length ? hasSuggestions : promptFilter; context: { suggestions, searchValue }"></ng-container>
</ul>
</form>
<ng-template #promptFilter>
<li>Filter for a city</li>
<li>or a state</li>
</ng-template>
<ng-template #hasSuggestions let-suggestions="suggestions" let-searchValue="searchValue">
<li *ngFor="let suggestion of suggestions">
<span [innerHtml]="suggestion | highlightSuggestion:searchValue"></span>
<span class="population">{{ suggestion.population | number }}</span>
</li>
</ng-template>
`,
styleUrls: ['./type-ahead.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TypeAheadComponent implements OnInit {
@ViewChild('searchForm', { static: true })
searchForm!: NgForm;
searchValue = ''
suggestions$!: Observable<City[]>;
cities$ = getCities();
ngOnInit(): void {
this.suggestions$ = this.searchForm.form.valueChanges.pipe(findCities(this.cities$));
}
}
TypeAheadComponent
imports CommonModule
, FormsModule
and HighlightSuggestionPipe
in the imports array. CommonModule
is included to make ngIf and async pipe available in the inline template. After importing FormsModule
, I can build a template form to accept search value. Finally, HighlightSuggestionPipe
highlights the search value in the search results for aesthetic purpose.
cities$
is an observable that fetches USA cities from external JSON file. Angular 15 introduces inject that simplifies HTTP request logic in a function. Thus, I don’t need to inject HttpClient in the constructor and perform the same logic.
const getCities = () => {
const httpService = inject(HttpClient);
const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
return httpService.get<City[]>(endpoint).pipe(shareReplay(1));
}
cities$ = getCities();
suggestions$
is an observable that holds the matching cities and states after search value changes. It is subsequently resolved in inline template to render in a list.
Create RxJS custom operator
It is a matter of taste but I prefer to refactor RxJS operators into custom operators when observable has many lines of code. For suggestion$
, I refactor the chain of operators into findCities custom operator and reuse it in TypeAheadComponent
.
// find-cities.operator.ts
const findMatches = (formValue: { searchValue: string }, cities: City[]) => {
const wordToMatch = formValue.searchValue;
if (wordToMatch === '') {
return [];
}
const regex = new RegExp(wordToMatch, 'gi');
// here we need to figure out if the city or state matches what was searched
return cities.filter(place => place.city.match(regex) || place.state.match(regex));
}
export const findCities = (cities$: Observable<City[]>) => {
return (source: Observable<{ searchValue: string }>) =>
source.pipe(
skip(1),
debounceTime(300),
distinctUntilChanged(),
withLatestFrom(cities$),
map(([formValue, cities]) => findMatches(formValue, cities)),
startWith([]),
);
}
- skip(1) – The first valueChange emits undefined for unknown reason; therefore, skip is used to discard it
- debounceTime(300) – emit search value after user stops typing for 300 milliseconds
- distinctUntilChanged() – do nothing when search value is unchanged
- withLatestFrom(cities$) – get the cities returned from HTTP request
- map(([formValue, cities]) => findMatches(formValue, cities)) – call findMatches to filter cities and states by search value
- startWith([]) – initially, the search result is an empty array
Finally, I use findCities to compose suggestion$ observable.
Use RxJS and Angular to implement observable in type ahead component
// type-ahead.component.ts
this.suggestions$ = this.searchForm.form.valueChanges
.pipe(findCities(this.cities$));
- this.searchForm.form.valueChanges – emit changes in template form
- findCities(this.cities$) – apply custom operator to find matching cities and states
This is it, we have created a type ahead search that filters out USA cities and states by search value.
Final Thoughts
In this post, I show how to use RxJS and Angular standalone components to create type ahead search for filtering. The application has the following characteristics after using Angular 15's new features:
- The application does not have NgModules and constructor boilerplate codes.
- In main.ts, the providers array provides the HttpClient by invoking providerHttpClient function
- In TypeAheadComponent, I inject HttpClient in a function to make http request and obtain the results. In construction phase, I assign the function to cities$ observable
- Using
inject
to inject HttpClient offers flexibility in code organization. I can define getCities function in the component or move it to a utility file. Pre-Angular 15, HttpClient is usually injected in a service and the service has a method to make HTTP request and return the results
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Resources:
- Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day6-ng-type-ahead
- Live demo: https://railsstudent.github.io/ng-rxjs-30/day6-ng-type-ahead/
- Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
Posted on February 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.