Skip to content

MovieCard, MovieList, and the Browse Route

With the data layer in place, build the shared MovieCard component and the first two pages: Home and Browse.

First, wire the root component. Update app.ts, app.html, and app.routes.ts:

app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet />`
})
export class App {}
app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', loadComponent: () => import('./pages/home/home').then(m => m.Home) },
{ path: 'browse', loadComponent: () => import('./pages/browse/browse').then(m => m.Browse) },
{ path: 'search', loadComponent: () => import('./pages/search-results/search-results').then(m => m.SearchResults) },
{ path: 'movies/:id', loadComponent: () => import('./pages/movie-detail/movie-detail').then(m => m.MovieDetail) },
{ path: 'watchlist', loadComponent: () => import('./pages/watchlist/watchlist').then(m => m.Watchlist) },
{ path: '**', redirectTo: '' }
];

Using loadComponent lazy-loads each page — they are only downloaded when the user navigates to them.

src/app/components/movie-card/movie-card.ts:

import { Component, Input, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Movie } from '../../models/movie.model';
import { WatchlistService } from '../../services/watchlist';
import { posterUrl } from '../../core/tmdb.config';
@Component({
selector: 'app-movie-card',
standalone: true,
imports: [RouterLink],
templateUrl: './movie-card.html',
styleUrl: './movie-card.css'
})
export class MovieCard {
@Input({ required: true }) movie!: Movie;
private watchlist = inject(WatchlistService);
get isInWatchlist(): boolean {
return this.watchlist.has(this.movie.id);
}
get poster(): string {
return posterUrl(this.movie.poster_path);
}
toggle(event: Event): void {
event.preventDefault();
this.watchlist.toggle(this.movie);
}
}

movie-card.html:

<a [routerLink]="['/movies', movie.id]" class="card">
<div class="poster-wrapper">
<img [src]="poster" [alt]="movie.title" class="poster" loading="lazy" />
<button
class="watchlist-btn"
[class.saved]="isInWatchlist"
(click)="toggle($event)"
[attr.aria-label]="isInWatchlist ? 'Remove from watchlist' : 'Add to watchlist'"
>
{{ isInWatchlist ? '✓' : '+' }}
</button>
</div>
<div class="card-info">
<h3 class="title">{{ movie.title }}</h3>
<p class="year">{{ movie.release_date | slice:0:4 }}</p>
<p class="rating">★ {{ movie.vote_average | number:'1.1-1' }}</p>
</div>
</a>

src/app/pages/home/home.ts:

import { Component, OnInit, inject } from '@angular/core';
import { MovieService } from '../../services/movie';
import { MovieCard } from '../../components/movie-card/movie-card';
import { Movie } from '../../models/movie.model';
@Component({
selector: 'app-home',
standalone: true,
imports: [MovieCard],
templateUrl: './home.html',
styleUrl: './home.css'
})
export class Home implements OnInit {
private movieService = inject(MovieService);
trending: Movie[] = [];
popular: Movie[] = [];
loading = true;
ngOnInit(): void {
this.movieService.getTrending().subscribe(movies => {
this.trending = movies;
});
this.movieService.getPopular().subscribe(movies => {
this.popular = movies;
this.loading = false;
});
}
}

home.html:

<div class="home">
@if (loading) {
<p class="loading">Loading movies…</p>
} @else {
<section>
<h2>Trending This Week</h2>
<div class="grid">
@for (movie of trending; track movie.id) {
<app-movie-card [movie]="movie" />
}
</div>
</section>
<section>
<h2>Popular Now</h2>
<div class="grid">
@for (movie of popular; track movie.id) {
<app-movie-card [movie]="movie" />
}
</div>
</section>
}
</div>

The Browse page adds filter controls above the movie grid. See the complete implementation in the CinemaVault source — it uses FormBuilder, debounceTime, distinctUntilChanged, and switchMap exactly as you learned in Module 06.

  1. Create WatchlistService in src/app/services/watchlist.ts with signal<Movie[]>, has(), toggle(), and localStorage persistence. (The full implementation is in the CinemaVault source.)
  2. Implement MovieCard as shown above.
  3. Implement the Home page.
  4. Navigate to localhost:4200 and confirm you see two movie grids.
  5. Click the + button on any card and confirm it toggles to and back.
  • Use loadComponent in routes for automatic lazy loading — each page is only bundled when visited.
  • MovieCard accepts a required movie input, reads watchlist state from WatchlistService, and toggles via a button.
  • Home calls getTrending() and getPopular() in ngOnInit and shows a loading state while requests are pending.
  • Browse uses FormBuilder + valueChanges + debounceTime + switchMap to filter movies reactively.