Skip to content

feat(youtube-player): add config option to automatically load the YouTube API #21401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/dev-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
<script src="zone.js/dist/zone.js"></script>
<script src="systemjs/dist/system.js"></script>
<script src="system-config.js"></script>
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js"></script>
<script>
(function loadGoogleMaps(key) {
Expand Down
12 changes: 11 additions & 1 deletion src/dev-app/youtube-player/youtube-player-demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {FormsModule} from '@angular/forms';
import {MatRadioModule} from '@angular/material/radio';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {YouTubePlayerModule} from '@angular/youtube-player';
import {
YouTubePlayerModule,
YOUTUBE_PLAYER_CONFIG,
YouTubePlayerConfig,
} from '@angular/youtube-player';
import {YouTubePlayerDemo} from './youtube-player-demo';

@NgModule({
Expand All @@ -23,6 +27,12 @@ import {YouTubePlayerDemo} from './youtube-player-demo';
RouterModule.forChild([{path: '', component: YouTubePlayerDemo}]),
],
declarations: [YouTubePlayerDemo],
providers: [{
provide: YOUTUBE_PLAYER_CONFIG,
useValue: {
loadApi: true
} as YouTubePlayerConfig
}]
})
export class YouTubePlayerDemoModule {
}
32 changes: 15 additions & 17 deletions src/youtube-player/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,34 @@ If your video is found at https://www.youtube.com/watch?v=PRQCAL_RMVo, then your

```typescript
// example-module.ts
import {NgModule, Component, OnInit} from '@angular/core';
import {YouTubePlayerModule} from '@angular/youtube-player';
import {NgModule, Component} from '@angular/core';
import {
YouTubePlayerModule,
YOUTUBE_PLAYER_CONFIG,
YouTubePlayerConfig,
} from '@angular/youtube-player';

@NgModule({
imports: [YouTubePlayerModule],
// Optionally tells the `youtube-player` component to automatically load
// the YouTube iframe API. Omit this if you plan to load the API yourself.
providers: [{
Copy link
Member Author

@crisbeto crisbeto Dec 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jelbourn I would appreciate your feedback on a couple of things that I was considering when implementing it:

  1. How the config should be provided: through an injection token like here or through something like YouTubePlayerModule.withConfig({ loadApi: true }). I went with the former, because it's clearer what is going on, but I can see an argument for the latter, because the API is a bit cleaner and I expect that the most common use case would be to let the component load the API automatically. I wouldn't mind changing it to use the second approach.
  2. Whether loadApi should be turned on by default. I decided against it, because it does add an external script to people's apps and I figured that's something we'd want them to opt into, but it does make it slightly harder to consume.

provide: YOUTUBE_PLAYER_CONFIG,
useValue: {
loadApi: true
} as YouTubePlayerConfig
}]
declarations: [YoutubePlayerExample],
})
export class YoutubePlayerExampleModule {
}

let apiLoaded = false;

// example-component.ts
@Component({
template: '<youtube-player videoId="PRQCAL_RMVo"></youtube-player>',
selector: 'youtube-player-example',
})
class YoutubePlayerExample implements OnInit {
ngOnInit() {
if (!apiLoaded) {
// This code loads the IFrame Player API code asynchronously, according to the instructions at
// https://developers.google.com/youtube/iframe_api_reference#Getting_Started
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(tag);
apiLoaded = true;
}
}
}

class YoutubePlayerExample {}
```

## API
Expand Down
2 changes: 1 addition & 1 deletion src/youtube-player/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
*/

export * from './youtube-module';
export {YouTubePlayer} from './youtube-player';
export {YouTubePlayer, YouTubePlayerConfig, YOUTUBE_PLAYER_CONFIG} from './youtube-player';
60 changes: 59 additions & 1 deletion src/youtube-player/youtube-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
ViewEncapsulation,
Inject,
PLATFORM_ID,
InjectionToken,
Optional,
} from '@angular/core';
import {isPlatformBrowser} from '@angular/common';

Expand Down Expand Up @@ -91,6 +93,25 @@ interface PendingPlayerState {
seek?: {seconds: number, allowSeekAhead: boolean};
}

/** Injection token used to configure the `YouTubePlayer`. */
export const YOUTUBE_PLAYER_CONFIG =
new InjectionToken<YouTubePlayerConfig>('YOUTUBE_PLAYER_CONFIG');

/** Object that can be used to configure the `YouTubePlayer`. */
export interface YouTubePlayerConfig {
/**
* Whether the component should automatically load YouTube Iframe API.
* Otherwise it assumes that the API will be loaded by the consumer.
*/
loadApi?: boolean;

/**
* URL from which to load the API, if `loadApi` is enabled.
* Otherwise falls back to `https://www.youtube.com/iframe_api`.
*/
apiUrl?: string;
}

/**
* Angular component that renders a YouTube player via the YouTube player
* iframe API.
Expand Down Expand Up @@ -200,7 +221,10 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
@ViewChild('youtubeContainer')
youtubeContainer: ElementRef<HTMLElement>;

constructor(private _ngZone: NgZone, @Inject(PLATFORM_ID) platformId: Object) {
constructor(
private _ngZone: NgZone,
@Inject(PLATFORM_ID) platformId: Object,
@Optional() @Inject(YOUTUBE_PLAYER_CONFIG) private _config?: YouTubePlayerConfig) {
this._isBrowser = isPlatformBrowser(platformId);
}

Expand All @@ -216,6 +240,8 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
throw new Error('Namespace YT not found, cannot construct embedded youtube player. ' +
'Please install the YouTube Player API Reference for iframe Embeds: ' +
'https://developers.google.com/youtube/iframe_api_reference');
} else if (this._config?.loadApi) {
loadApi(this._config?.apiUrl || 'https://www.youtube.com/iframe_api');
}

const iframeApiAvailableSubject = new Subject<boolean>();
Expand Down Expand Up @@ -756,3 +782,35 @@ function filterOnOther<R, T>(
map(([value]) => value),
);
}

let apiLoaded = false;

/** Loads the YouTube API from a specified URL only once. */
function loadApi(url: string): void {
if (apiLoaded) {
return;
}

// We can use `document` directly here, because this logic doesn't run outside the browser.
const script = document.createElement('script');
const callback = (event: Event) => {
script.removeEventListener('load', callback);
script.removeEventListener('error', callback);

if (event.type === 'error') {
apiLoaded = false;

if (typeof ngDevMode === 'undefined' || ngDevMode) {
console.error(`Failed to load YouTube API from ${url}`);
}
}
};
script.addEventListener('load', callback);
script.addEventListener('error', callback);
script.src = url;

// Set this immediately to true so we don't start loading another script
// while this one is pending. If loading fails, we'll flip it back to false.
apiLoaded = true;
document.body.appendChild(script);
}
14 changes: 12 additions & 2 deletions tools/public_api_guard/youtube-player/youtube-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { AfterViewInit } from '@angular/core';
import { ElementRef } from '@angular/core';
import * as i0 from '@angular/core';
import { InjectionToken } from '@angular/core';
import { NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { OnDestroy } from '@angular/core';
Expand All @@ -20,9 +21,12 @@ const DEFAULT_PLAYER_HEIGHT = 390;
// @public (undocumented)
const DEFAULT_PLAYER_WIDTH = 640;

// @public
export const YOUTUBE_PLAYER_CONFIG: InjectionToken<YouTubePlayerConfig>;

// @public
export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
constructor(_ngZone: NgZone, platformId: Object);
constructor(_ngZone: NgZone, platformId: Object, _config?: YouTubePlayerConfig | undefined);
// (undocumented)
readonly apiChange: Observable<YT.PlayerEvent>;
// @deprecated (undocumented)
Expand Down Expand Up @@ -78,7 +82,13 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<YouTubePlayer, "youtube-player", never, { "videoId": "videoId"; "height": "height"; "width": "width"; "startSeconds": "startSeconds"; "endSeconds": "endSeconds"; "suggestedQuality": "suggestedQuality"; "playerVars": "playerVars"; "showBeforeIframeApiLoads": "showBeforeIframeApiLoads"; }, { "ready": "ready"; "stateChange": "stateChange"; "error": "error"; "apiChange": "apiChange"; "playbackQualityChange": "playbackQualityChange"; "playbackRateChange": "playbackRateChange"; }, never, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<YouTubePlayer, never>;
static ɵfac: i0.ɵɵFactoryDeclaration<YouTubePlayer, [null, null, { optional: true; }]>;
}

// @public
export interface YouTubePlayerConfig {
apiUrl?: string;
loadApi?: boolean;
}

// @public (undocumented)
Expand Down