I'm building a web application using angular 2, typescript and YouTube API to add a player to the page after the user login.
So once logged in, the app loads the following component:
export class MyComponent implements OnInit {
myService: MyService;
constructor( private _myService: MyService ) {
this.myService = _myService;
}
ngOnInit() {
this._myService.loadAPI();
}
}
The component html contains the following tag:
<iframe id="player" type="text/html" width="640" height="360"
src="http://www.youtube.com/embed/M7lc1UVf-VE?enablejsapi=1"
frameborder="0" allowfullscreen></iframe>
And finally, the service has the following:
player: YT.Player;
loadAPI(){
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
console.log('API loaded'); // this is shown on the console.
}
onYouTubeIframeAPIReady(){
this.player = new YT.Player('player', {
events: {
'onReady': this.onPlayerReady,
'onStateChange': this.onPlayerStateChange
}
});
console.log('youtube iframe api ready!'); // this is never triggered.
}
onPlayerReady(event){
event.target.playVideo();
}
onPlayerStateChange(status){
console.log(status.data);
}
I've read that the function "onYouTubeIframeAPIReady" is automatically called by the API, so I wonder what should I do differently to have it working properly.
You need to define your function, onYouTubeIframeAPIReady
, on the global object. This works exactly the same way as in the linked answer for JavaScript.
What follows is all 100% JavaScript stuff here, applicable to TypeScript by way of its superset of JavaScript nature.
If you are using modules, as is generally the case with an Angular 2 application, then your code is isolated and does not execute in the global scope by default. This means that in order to define a global, we need to obtain a reference to the Global Object. In a browser this is very simple as window
refers to the global (unless it is shadowed).
What you need to write is quite straightforward. It is essentially
window.onYouTubeIframeAPIReady = function () { ... };
That means taking your current code, which looks like this
export default class YouTubeService {
...
loadAPI() {
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
console.log('API loaded'); // this is shown on the console.
}
onYouTubeIframeAPIReady() { }
}
And changing it to this
export default class YouTubeService {
...
loadAPI() {
window.onYouTubeIframeAPIReady = function () { };
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
console.log('API loaded'); // this is shown on the console.
}
}
You will get a TypeScript error telling you that window
has no definition for onYouTubeIframeAPIReady
. This is easily resolved in numerous ways, but I just will illustrate two possibilities, either will do the job, and technically neither is necessary since TypeScript will still emit code in spite of the error.
Specify a type assertion on window that suppresses the error
(window as any).onYouTubeIframeAPIReady = function () {}
Declare the member on window so that you can assign to it without an error. Inside a module (recall we are not in the global scope) we can use the following form
declare global {
interface Window {
onYouTubeIframeAPIReady?: () => void;
}
}
Remember all JavaScript is valid TypeScript and that TypeScript does not add behavior or functionality to JavaScript. It is a typed view, an interpretation if you will, of JavaScript that allows it to be statically verified and have excellent tooling, to catch errors, provide a productive editing experience, and allow expectations to be documented at code level.
This is just JavaScript. It is the very same solution as used in Youtube iframe api not triggering onYouTubeIframeAPIReady, I only posted it because there seemed to be a disconnect.
Addendum: It is worth noting that if you using a module loader such as SystemJS or RequireJS, you can abstract the manual script tag injection process via loader configuration. The benefit is cleaner, more declarative code and also increased testability as you can then stub the YouTube dependency, isolating your tests from the network.
For SystemJS, you would use the following configuration
SystemJS.config({
map: {
"youtube": "https://www.youtube.com/iframe_api"
},
meta: {
"https://www.youtube.com/iframe_api": {
"format": "global",
"scriptLoad": true,
"build": false
}
}
});
You can write
export default class YouTubeService {
async loadAPI() {
window.onYouTubeIframeAPIReady = function () {
console.log('API loaded'); // this is shown on the console.
};
try {
await import('youtube'); // automatically injects a script tag
}
catch (e) {
console.error('The YouTube API failed to load');
}
}
}
declare global {
interface Window {
onYouTubeIframeAPIReady?: () => void;
}
}
Now if you wanted to test this code, mocking the YouTube API, you could write
test/test-stubs/stub-youtube-api.ts
(function () {
window.onYouTubeIframeAPIReady();
}());
test/services/youtube-service.spec.ts
import test from 'blue-tape';
import YouTubeService from 'src/services/youtube.service'
SystemJS.config({
map: {
"youtube": "test/test-stubs/stub-youtube-api.ts"
}
});
if(typeof window === 'undefined') {
global.window = {};
}
test('Service must define a callback for onYouTubeIframeAPIReady', async ({isNot}) => {
const service = new YouTubeService();
await service.loadAPI();
t.isNot(undefined, window.onYouTubeIframeAPIReady);
});