Как отобразить карусель видео YouTube в этом приложении фильмов Angular 16?

Я работал над SPA с Angular 16, TypeScript и The Movie Database (TMDB).

Я столкнулся с проблемой при отображении трейлеров фильмов в карусели Bootstrap 5.

В app\services\movie-service.service.ts у меня есть:

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}`);
  }
}

В app\models\Trailer.ts у меня есть:

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

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

Я создал компонент TrailerCarouselComponent со следующим файлом ts:

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[];
}

И этот шаблон:

<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>

Я использую указанный выше компонент в компоненте MovieDetailsComponent:

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();
  }
}

Шаблон:

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

Ошибок компиляции нет, но есть неожиданная (для меня) проблема.

Проблема

Вместо желаемого результата я получаю эту ошибку в браузере:

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

Строка, о которой идет речь, src = "https://www.youtube.com/embed/{{ video.key }} из:

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

Для меня это тем более удивительно, что я уже создал это приложение с помощью Vue3 и Typescript и не столкнулся с проблемой.

ОБНОВЛЯТЬ

Использование трубы, как предложил @Kosta, мне не помогло. Я добавил Stackblitz.

Вопросы

  1. Что я делаю не так?
  2. Какой самый надежный способ решить эту проблему?
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
0
106
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Могут быть небезопасные URL-адреса, если они генерируются динамически, поэтому нам необходимо убедиться, что очищенный доверенный URL-адрес попадает в src, мы можем использовать bypassSecurityTrustResourceUrl из DomSanitizer angular, чтобы гарантировать, что URL-адрес очищен!

HTML

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

тс

constructor(private sanitizer: DomSanitizer) {}
...

...
trustUrl(key: string): string {
    return this.sanitizer.bypassSecurityTrustResourceUrl(`https://www.youtube.com/embed/${key}`) as string;
}
...

Я получаю ошибку Cannot find name 'SecurityContext'. Did you mean 'isSecureContext'? в app\components\trailer-carousel\trailer-carousel.component.t‌​s

Razvan Zamfir 21.04.2024 14:50

@RazvanZamfir нам нужно импортировать SecurityContext, можешь попробовать это? он находится в @angular/core

Naren Murali 21.04.2024 15:07

Я сделал это, и iframe содержит всю страницу. Источник <iframe _ngcontent-ng-c2152715611 = "" src = "trustUrl(video.key)" class = "embed-responsive-item"></iframe>. Может быть, <iframe class = "embed-responsive-item" src = "trustUrl(video.key)"></iframe> неправильный?

Razvan Zamfir 21.04.2024 15:21

@RazvanZamfir извините, нам нужно добавить привязку свойства [src] обновил мой ответ!

Naren Murali 21.04.2024 15:23

Я тоже так сделал и получаю ошибку TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.

Razvan Zamfir 21.04.2024 15:26

@RazvanZamfir обновлено! добавленный тип

Naren Murali 21.04.2024 15:28

Что исправляет ошибку компиляции, так это <iframe *ngIf = "video.key" class = "embed-responsive-item" [src] = "trustUrl(video.key)"></iframe>, но... видео не отображается.

Razvan Zamfir 21.04.2024 15:34

@RazvanZamfir попробуйте bypassSecurityTrustResourceUrl вместо этого обновите ответ демо stackblitz

Naren Murali 21.04.2024 15:39

Я обновил вопрос, указав подробную информацию о родительском компоненте. Пожалуйста, взгляните

Razvan Zamfir 21.04.2024 15:56
Ответ принят как подходящий

Первая проблема заключается в том, что вы передаете не очищенный URL-адрес в iframe, что по умолчанию не разрешено в angular. Создайте канал, который будет обходить безопасность URL-адреса iframe.

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);
  }
}

Поскольку у вашего компонента нет свойства Standalone, я предполагаю, что ваше приложение основано на модулях. Вы можете импортировать эту трубу внутрь NgModule.

После того, как вы добавите его в NgModules, давайте воспользуемся этим каналом:

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

Предупреждение: вызов этого метода с ненадежными пользовательскими данными подвергает ваше приложение угрозе безопасности XSS!

Ответ на второй вопрос — использовать каналы вместо функций, которые будут напрямую использоваться внутри html, это вызовет многократное обнаружение изменений, лучший подход — каналы или обработка источника через наблюдаемые (поток).

Например:

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

Имейте в виду, что это более лучший подход async — это канал для прослушивания потоков, поэтому вам не нужно использовать подписку на поток.

Старайтесь чаще использовать сигналы или наблюдаемые (rxjs)

Я понимаю ERROR NullInjectorError: R3InjectorError(AppModule)[BypassSanitizePipe -> BypassSanitizePipe]. Я добавил stackblitz.

Razvan Zamfir 22.04.2024 12:40

Обновление: я добавил BypassSanitizePipe в массив поставщиков в app.module.ts. И у меня все еще есть ошибка: Error: NG0904: unsafe value used in a resource URL context. Взгляните на stackblitz.

Razvan Zamfir 22.04.2024 12:46

Проблема заключалась в том, что я предложил неправильный способ передачи односторонней привязки, это было больше похоже на «взлом», но он отлично подходит для использования rxjs. просто обновите строку передачи src: [src] = "'https://www.youtube.com/embed/' + video.key | bypassSanitize" и все должно работать. Вот stackblitz, имейте в виду, что stackblitz не отображает iframe по соображениям безопасности. Кстати, когда вы импортируете канал, он должен идти в массив объявлений, а не в поставщики, и нет необходимости вводить его в конструктор.

Kosta 22.04.2024 16:31

Пожалуйста, попробуйте ответить и на этот вопрос. Спасибо!

Razvan Zamfir 27.04.2024 22:28

Другие вопросы по теме