Search with Reactive Forms
CinemaVault’s search spans two components: the NavBar contains the search input, and the SearchResults page displays results. Navigation between them happens via query parameters.
The NavBar search form
Section titled “The NavBar search form”The NavBar holds the app’s persistent navigation and an inline search form. When submitted, it navigates to /search?q=<query>:
import { Component, inject } from '@angular/core';import { Router, RouterLink, RouterLinkActive } from '@angular/router';import { FormsModule } from '@angular/forms';import { WatchlistService } from '../../services/watchlist';
@Component({ selector: 'app-nav-bar', standalone: true, imports: [RouterLink, RouterLinkActive, FormsModule], templateUrl: './nav-bar.html', styleUrl: './nav-bar.css'})export class NavBar { private router = inject(Router); watchlist = inject(WatchlistService); searchQuery = '';
onSearch(event: Event): void { event.preventDefault(); const q = this.searchQuery.trim(); if (q) { this.router.navigate(['/search'], { queryParams: { q } }); this.searchQuery = ''; } }}<nav class="navbar"> <a class="brand" routerLink="/">CinemaVault</a> <div class="nav-links"> <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a> <a routerLink="/browse" routerLinkActive="active">Browse</a> <a routerLink="/watchlist" routerLinkActive="active" class="watchlist-link"> Watchlist @if (watchlist.count() > 0) { <span class="count">{{ watchlist.count() }}</span> } </a> </div> <form class="search-form" (submit)="onSearch($event)"> <input [(ngModel)]="searchQuery" name="q" placeholder="Search movies…" /> <button type="submit" aria-label="Search">🔍</button> </form></nav>Add NavBar to app.ts’s imports and add <app-nav-bar /> to app.html.
The SearchResults page
Section titled “The SearchResults page”import { Component, OnInit, inject } from '@angular/core';import { ActivatedRoute } from '@angular/router';import { switchMap, map, catchError, EMPTY } from 'rxjs';import { MovieService } from '../../services/movie';import { MovieCard } from '../../components/movie-card/movie-card';import { Movie } from '../../models/movie.model';
@Component({ selector: 'app-search-results', standalone: true, imports: [MovieCard], templateUrl: './search-results.html', styleUrl: './search-results.css'})export class SearchResults implements OnInit { private route = inject(ActivatedRoute); private movieService = inject(MovieService);
results: Movie[] = []; totalResults = 0; query = ''; loading = false;
ngOnInit(): void { this.route.queryParamMap.pipe( map(params => params.get('q') ?? ''), switchMap(q => { this.query = q; if (!q) return EMPTY; this.loading = true; this.results = []; return this.movieService.search(q).pipe( catchError(() => EMPTY) ); }) ).subscribe(response => { this.results = response.results; this.totalResults = response.total_results; this.loading = false; }); }}<div class="search-results"> <h1> @if (query) { Results for "{{ query }}" <span class="count">({{ totalResults | number }} movies)</span> } @else { Search for a movie } </h1>
@if (loading) { <p class="loading">Searching…</p> } @else { <div class="grid"> @for (movie of results; track movie.id) { <app-movie-card [movie]="movie" /> } @empty { @if (query) { <p class="empty">No results found for "{{ query }}".</p> } } </div> }</div>How the flow works
Section titled “How the flow works”- User types in NavBar and submits
router.navigate(['/search'], { queryParams: { q: 'inception' } })fires- URL becomes
/search?q=inception SearchResultsis rendered (or stays if already active)route.queryParamMapemits the new paramsswitchMapcancels any previous search and starts a new one- Results appear in the grid
If the user searches again quickly, the old request is cancelled and only the latest results display.
Exercise
Section titled “Exercise”- Implement
NavBarwith the search form and navigation links. - Add
NavBartoapp.tsimports and<app-nav-bar />toapp.html. - Implement
SearchResultswith thequeryParamMap → switchMappattern. - Search for “Inception” and confirm results appear.
- Search again immediately while results are loading — confirm only the final search results appear.
- The NavBar uses
[(ngModel)]for the search input androuter.navigate()to push?q=to the URL. SearchResultssubscribes toroute.queryParamMap— when the query changes, it re-searches.switchMapensures only the most recent search request is active.EMPTYshort-circuits the stream when the query is blank or when an error occurs.@emptyin the@forblock shows a friendly “no results” message.