Introduction
This is day 6 of Wes Bos’s JavaScript 30 challenge where I create real-time search input box to filter out cities and states in the USA. This is a challenge for me because I had to rewrite the original real-time search using Angular and adopting the styles of the framework. In the tutorial, I created the components using RxJS, custom operators, Angular standalone components and removed the NgModules. Moreover, Angular HTTP Client is responsible for retrieving the JSON data from GitHub gist.
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. It is because main.ts
uses bootstrapApplication
to render AppComponent
as the root component of the application. When compiler sees <app-type-ahead> in the inline template and AppComponent
does not import TypeAheadComponent
, the application fails to compile.
// 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 real-time 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 real-time search of the USA cities and states. 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