Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

angular - onYouTubeIframeAPIReady not firing on angular2 web app

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...