onYouTubeIframeAPIReady not firing on angular2 web app
Asked Answered
H

1

5

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.

Hieronymus answered 17/3, 2017 at 22:45 Comment(5)
possible duplicate of #12256882Lodging
@AluanHaddad I've seen that question before and I tried to follow its logic, however I couldn't adapt it to typescript, that's why I asked here specifying these details.Hieronymus
I'm not sure how this is TypeScript related. If it works in JavaScript then it works the same way in TypeScript. This fully extends to scoping, which appears to be the issue here. It's very important not to think of TypeScript as an alternative to JavaScript as it's nothing of the sort.Lodging
What it comes down to is that the YouTube API calls a global function named onYouTubeIframeAPIReady. The method of the same name you've defined in your service is completely arbitrary. How would the YouTube API call that method?Lodging
@AluanHaddad Ok, I get it now, but how could I make it global with typescript?Hieronymus
L
8

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.

  1. Specify a type assertion on window that suppresses the error

    (window as any).onYouTubeIframeAPIReady = function () {}
    
  2. 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);
});
Lodging answered 18/3, 2017 at 22:0 Comment(2)
Thank you very much! You just clarified alot to me and this have helped me a lot! It works perfectly. I really appreciate it, thanks.Hieronymus
Glad I was able to helpLodging

© 2022 - 2024 — McMap. All rights reserved.