Create type ahead search using RxJS and Angular standalone components
Tags: angular, rxjs, tutorial, typescript
Published: 2023-02-25 14:40:04

##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
```bash
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.

```typescript
// 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.

```typescript
// 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.

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

```typescript
// 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.

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

```typescript
// 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

```typescript
// 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