skip to Main Content

I have been working on an SPA with Angular 16, TypeScript and The Movie Database (TMDB).

I run into a strange problem while working on a movies search feature.

In appservicesmovie-service.service.ts I have:

import { environment } from '../../environments/environment';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { MovieResponse, Movie } from '../models/Movie';

@Injectable({
  providedIn: 'root'
})

export class MovieService {
  constructor(private http: HttpClient) {}

  public searchMovies(searchTerm: string): Observable<MovieResponse> {
    return this.http.get<MovieResponse>(`${environment.apiUrl}/search/movie?api_key=${environment.apiKey}&query=${searchTerm}`);
  }
}

I use the method above inside the TopBarComponent like this:

import { Component } from '@angular/core';
import { MovieResponse, Movie } from '../../models/Movie';
import { MovieService } from '../../services/movie-service.service';

@Component({
  selector: 'app-top-bar',
  templateUrl: './top-bar.component.html',
  styleUrls: ['./top-bar.component.scss']
})
export class TopBarComponent {
  constructor(private movieService: MovieService) { }

  public searchTerm: string = '';
  public isSearch: boolean = false;
  public timeOutInterval: number = 500;
  public searchResultsResponse!: MovieResponse;
  public searchResults: Movie[] | undefined = [];

  public hideSearchResults(): void {
    this.isSearch = false;
  }

  public debounceMovieSearch(): void {
    setTimeout(() => this.doMovieSearch(), this.timeOutInterval);
  }
  
  public doMovieSearch() {
    if (this.searchTerm && this.searchTerm.length > 2) {
      this.isSearch = true;
      this.movieService.searchMovies(this.searchTerm).subscribe((response) => {
        this.searchResultsResponse = response;
        this.searchResults = this.searchResultsResponse.results;
      })
    } else {
      this.isSearch = false;
    }
  }
}

Search form:

<form class="search_form w-100 mx-auto mt-2 mt-md-0">
    <div class="input-group">
      <input type="search" name="search" [(ngModel)]="searchTerm" (input)="debounceMovieSearch()" placeholder="Search" autocomplete="off" class="form-control search-box">
      <button class="btn btn-search" type="button">Search</button>
    </div>

    <div *ngIf="isSearch" (clickOutside)="hideSearchResults()" class="search-results shadow-sm">
      <div *ngIf="searchResults && searchResults.length">
        <a routerLink="/movie/{{ movie.id }}" *ngFor="let movie of searchResults">
          <app-search-item [movie]="movie"></app-search-item>
        </a>
      </div>

      <div *ngIf="!(searchResults && searchResults.length)">
        <p class="m-0 p-2 text-center">No movies found for this search</p>
      </div>
    </div>
  </form>
  
  

The routes in the appapp-routing.module.ts:

const routes: Routes = [
  { path: '', component: HomePageComponent, data: { title: 'Now playing' } },
  { path: 'top-rated', component: TopMoviesComponent, data: { title: 'Top Rated' } },
  { path: 'movie/:id', component: MovieDetailsComponent, data: { title: '' } },
  { path: 'actor/:id', component: ActorDetailsComponent, data: { title: '' } },
  { path: '**', component: NotFoundComponent, data: { title: '' } },
];
  

The result looks like this:

enter image description here

The problem

Clicking a movie item in the searcg results list will navigate us to the movie details route (MovieDetailsComponent), except when we already are on a movie details page.

Stackblitz

There is a Stackblitz with all the code I have so far.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to fix this issue?

2

Answers


  1. You are almost correct, what you are missing is using paramMap rather than snapshot in the movie-details.component, so you can react to the movie id changes.

    You need to address this change on the function getMovieDetails like so:

    getMovieDetails(): void {
      this.activatedRoute.paramMap.pipe(
        map(params => params.get('id')), // retrieve id from route
        switchMap((id: string | undefined) => {
          if (id) return this.movieService.getMovieDetails(Number(id));
          return of(undefined); // if there is no id, then return undefined
       })
      ).subscribe((response: Movie | undefined) => {
         if (response) { // if movie exists then do something...
            this.movie = response;
            // ...
         }
      })
    }
    

    Additionally, since paramMap returns an observable, keep in mind that you should only subscribe in one place, that is why I am using pipe and returning the final observable result, which is either a Movie or undefined

    Login or Signup to reply.
  2. When you navigate from movie/1 to movie/2, MovieDetailsComponent isn’t recreated. Because you don’t listen for id changes, the component doesn’t know that movie id has changed and it needs to update the view. So listen to route parameter changes:

    ngOnInit() {
      this.activatedRoute.paramMap.subscribe(paramMap => {
        const movie_id = Number(paramMap.get('id'));
        this.getMovieDetails(movie_id);
      });
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search