skip to Main Content

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

I run into a problem while working on displaying a movie trailers in a Bootstrap 5 carousel.

In appservicesmovie-service.service.ts I have:

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

@Injectable({
  providedIn: 'root'
})

export class MovieService {

  constructor(private http: HttpClient) { }

  public getMovieTrailers(id: Number): Observable<TrailerResponse>{
    return this.http.get<TrailerResponse>(`${environment.apiUrl}/movie/${id}/videos?api_key=${environment.apiKey}`);
  }

  public getMovieDetails(id: Number): Observable<Movie>{
    return this.http.get<Movie>(`${environment.apiUrl}/movie/${id}?api_key=${environment.apiKey}`);
  }
}

In appmodelsTrailer.ts I have:

export interface Trailer {
    id?: string;
    key?: string;
    name?: string;
    site?: string;
}

export interface TrailerResponse {
    results?: Trailer[];
}   

I have created a TrailerCarouselComponent component with the folowing ts file:

import { Component, Input } from '@angular/core';
import { Trailer } from '../../models/Trailer';

@Component({
  selector: 'app-trailer-carousel',
  templateUrl: './trailer-carousel.component.html',
  styleUrls: ['./trailer-carousel.component.scss']
})
export class TrailerCarouselComponent {
  @Input() trailers!: Trailer[];
}

And this template:

<div id="trailersCarousel" class="carousel slide" data-bs-interval="false">
  <ol *ngIf="trailers.length > 1" class="carousel-indicators">
    <li
      *ngFor="let video of trailers; let i = index"
      data-bs-target="#trailersCarousel"
      attr.data-slide-to="{{ i }}"
      class="{{ i === 0 ? 'active' : '' }}"
    >
      {{ i + 1 }}
    </li>
  </ol>

  <div class="carousel-inner">
    <div
      *ngFor="let video of trailers; let i = index"
      
      class="carousel-item"
      [ngClass]="{ 'active': i === 0 }"
    >
      <iframe
        class="embed-responsive-item"
        src="https://www.youtube.com/embed/{{ video.key }}"
      ></iframe>
    </div>
  </div>
</div>

I use the above component in the MovieDetailsComponent component:

import { Component, Input } from '@angular/core';
import { Movie } from '../../models/Movie';
import { Trailer, TrailerResponse } from '../../models/Trailer';
import { MovieService } from '../../services/movie-service.service';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-movie-details',
  templateUrl: './movie-details.component.html',
  styleUrls: ['./movie-details.component.scss']
})

export class MovieDetailsComponent {

  constructor(private movieService: MovieService, private activatedRoute: ActivatedRoute) {}

  public movie!: Movie;
  public trailerResponse!: TrailerResponse;
  public trailers: Trailer[] | undefined = [];


  public getMovieTrailers() {
    const movie_id = Number(this.activatedRoute.snapshot.paramMap.get('id'));

    this.movieService.getMovieTrailers(movie_id).subscribe((response) => {
      this.trailerResponse = response;
      this.trailers = this.trailerResponse.results;
      
      if (this.trailers && this.trailers?.length) {
        this.trailers = this.trailers.slice(0, 5);
      } 
    });
  }

  public getMovieDetails() {
    const movie_id = Number(this.activatedRoute.snapshot.paramMap.get('id'));

    this.movieService.getMovieDetails(movie_id).subscribe((response) => {
      this.movie = response;

      // get movie trailers
      this.getMovieTrailers();
    });
  }

  ngOnInit() {
    this.getMovieDetails();
  }
}

Template:

<div *ngIf="trailers && trailers.length" class="mb-3">
  <h2 class="section-title">Trailers</h2>
  <app-trailer-carousel [trailers]="trailers"></app-trailer-carousel>
</div>

There are no compilation errors and yet, there is a surprising (for me) problem.

The problem

Insted of the desired result, I get this error in the browser:

NG0904: unsafe value used in a resource URL context
...
 at TrailerCarouselComponent_div_3_Template (trailer-carousel.component.html:22:9)
 

The line in question is src="https://www.youtube.com/embed/{{ video.key }} from:

<iframe
    class="embed-responsive-item"
    src="https://www.youtube.com/embed/{{ video.key }}"
  ></iframe>

This is all the more surprising for me since I have already made this app with Vue3 and Typescript and did not encounter the problem.

Questions

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

2

Answers


  1. There can be unsafe URLs, when its dynamically generated, so we need to ensure a sanitized trusted URL goes into src, we can use bypassSecurityTrustResourceUrl from DomSanitizer of angular to ensure the URL is santized!

    html

    <iframe
        class="embed-responsive-item"
        [src]="trustUrl(video.key)"
      ></iframe>
    

    ts

    constructor(private sanitizer: DomSanitizer) {}
    ...
    
    ...
    trustUrl(key: string): string {
        return this.sanitizer.bypassSecurityTrustResourceUrl(`https://www.youtube.com/embed/${key}`) as string;
    }
    ...
    
    Login or Signup to reply.
  2. First issue is you are passing not sanitizied url to iframe, which by default isn’t allowed in angular. Create a pipe that will bypass the security of the iframe URL.

    ng g p bypass-sanitize
    
    import { Pipe, PipeTransform, inject } from '@angular/core';
    import { DomSanitizer } from '@angular/platform-browser';
    
    @Pipe({
      name: 'bypassSanitize',
      standalone: true,
    })
    export class BypassSanitizePipe implements PipeTransform {
      private readonly domSanitizer = inject(DomSanitizer);
    
      //  constructor(private domSanitizier: DomSanitizer) {} old way but still working
    
      transform(value: string) {
        return this.domSanitizer.bypassSecurityTrustResourceUrl(value);
      }
    }
    
    

    Since your component doesn’t have the standalone property, I assume your app is based on modules. You can import this pipe inside NgModule.

    After you add it to NgModules, let’s use this pipe:

    <iframe
        class="embed-responsive-item"
        src="{{ 'https://www.youtube.com/embed/' + video.key | bypassSanitize }}"
      ></iframe>
    

    Warning: calling this method with untrusted user data exposes your application to XSS security risks!

    Answer for second question is to use pipes instead of functions which will be directly used inside of html, this will trigger many time change detection, better approach is pipes or handling source via observables(stream).

    For example:

      readonly frameUrl$ = this.SomeStreamForYourFrameUrl$.pipe(
        map((url) => this.domSanitizer.bypassSecurityTrustResourceUrl(url)),
      );
    
    <iframe
        class="embed-responsive-item"
        src="{{frameUrl$ | async }}"
      ></iframe>
    

    This is more better approach keep in mind async is pipe for listening streams, by this way you don’t have to use subscribe on stream.

    Try to use more often signals or observables (rxjs)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search