Skip to content

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 holds the app’s persistent navigation and an inline search form. When submitted, it navigates to /search?q=<query>:

nav-bar.ts
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-bar.html
<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.

search-results.ts
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;
});
}
}
search-results.html
<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>
  1. User types in NavBar and submits
  2. router.navigate(['/search'], { queryParams: { q: 'inception' } }) fires
  3. URL becomes /search?q=inception
  4. SearchResults is rendered (or stays if already active)
  5. route.queryParamMap emits the new params
  6. switchMap cancels any previous search and starts a new one
  7. Results appear in the grid

If the user searches again quickly, the old request is cancelled and only the latest results display.

  1. Implement NavBar with the search form and navigation links.
  2. Add NavBar to app.ts imports and <app-nav-bar /> to app.html.
  3. Implement SearchResults with the queryParamMap → switchMap pattern.
  4. Search for “Inception” and confirm results appear.
  5. Search again immediately while results are loading — confirm only the final search results appear.
  • The NavBar uses [(ngModel)] for the search input and router.navigate() to push ?q= to the URL.
  • SearchResults subscribes to route.queryParamMap — when the query changes, it re-searches.
  • switchMap ensures only the most recent search request is active.
  • EMPTY short-circuits the stream when the query is blank or when an error occurs.
  • @empty in the @for block shows a friendly “no results” message.