Adding youtube player and google sign-in features to the echoes player version that I started developed with Angular (+2) was almost a no brainer. Since I took few steps ahead in preparing the AngularJS version code to Angular (+2), all left to do is copy and paste. However, issues started to rise once I started to check its functionality. Then I discovered NgZone.
In Angular (+2), the change detection mechanism has been redesigned well enough to boost optimization and performance for several cases. This is all true when the app is running inside Angular (+2) world.
In Echoes Player, I had to use 2 features that required interacting with external services. By external I mean that, the interactions goes outside of the Angular (+2) scope:
Upon each event that is received asynchronously, in Echoes player scope, a chain of reactions happen and updates internal state which is managed currently with ngrx/store.
However, since these events originates in an external source - another domain - Angular (+2) change detection doesn’t recognize that the data has changed. So, with Angular (+2), we’re still left with connecting these to angular’s change detection.
I quickly noticed the problems with integrating the above external services to Echoes.
Whenever a change has been made following an external event, it wasn’t rendered unless I did a simple behavior (click) which invoked the change detection mechanism. Lets go through each problem and its solution.
Echoes is integrated with youtube player iframe api. In order to use it, a player has to be created with the api:
// youtube-player.service.ts
createPlayer (callback) {
const store = this.store;
const service = this;
const defaultSizes = this.defaultSizes;
return new window.YT.Player('player', {
height: defaultSizes.height,
width: defaultSizes.width,
videoId: '',
events: {
onReady: () => {},
onStateChange: onPlayerStateChange
}
});
}
In this process, the api creates a player object which interacts with youtube’s domain. The actual video playing is played on youtube’s domain, so, the actual player (in youtube’s domain) notifies a change thru the ”onPlayerStateChange” callback that is passed in the constructor of this player.
Echoes uses this events in order to update the app’s player state (using ngrx/store) of the current state of the player, so it can, i.e, show/hide the pause/play buttons. This is the point where i started to see the problem. The buttons wouldn’t reflect the actual state of the player.
In order to notify angular change detection that there are changes which originated outside of the app, I has to use NgZone.
NgZone is a service that can be used in order to execute work (functions/code) outside or inside the angular world. Thus, in the end of these operations, Angular’s change detection is triggered, and updates whatever is necessary. I see it very similar to AngularJS ”scope.apply”, however, more mature, optimized and performant.
In order to re-enter angular’s world, NgZone’s ”run” function should take a function as the operation that is needed to be run, and then, the relevant changes will be rendered.
First I imported NgZone in ”youtube-player.service.ts“:
import { Injectable, NgZone } from '@angular/core';
Then, I injected it to its constructor:
constructor (public store: Store<any>, private zone: NgZone) {
// now zone is available via:
// this.zone
}
Then, I updated the ”createPlayer” function and wrapped the ”onPlayerStateChange” callback with NgZone’s run function. Notice that I also used the es6/es2015 fat arrow in order to keep the “this” context so I can access the service’s zone:
createPlayer (callback) {
const store = this.store;
const service = this;
const defaultSizes = this.defaultSizes;
return new window.YT.Player('player', {
height: defaultSizes.height,
width: defaultSizes.width,
videoId: '',
events: {
onReady: () => {},
onStateChange: (ev) => this.zone.run(() => onPlayerStateChange(ev))
}
});
}
With this version of Echoes which is implemented with Angular (+2), I chose to experiment with google’s web sign-in strategy. I chose assigning a handler to a button with google’s api “gapi.auth2”.
The expected use case is:
The code which is responsible for steps 2-5 starts with assigning a click handler and listeners the the sign-in button:
// user-manager.service.ts
attachSignIn() {
if (this.auth2 && !this.isSignedIn && !this.isAuthInitiated) {
this.isAuthInitiated = true;
// Attach the click handler to the sign-in button
this.auth2.attachClickHandler('signin-button', {}, this.onLoginSuccess.bind(this), this.onLoginFailed.bind(this));
}
}
Notice that although the ”success” and ”fail” functions are bind with ”this” (service) context, still, these will be invoked asynchronously outside of angular’s world - so even, the ”bind” function isn’t the answer to this one.
After the ”success” callback is invoked, steps 6-7 should be invoked - however - with this implementation it won’t.
Similar to the youtube player problem, the solution here is using NgZone’s ”run” function. It is setup and injected in a similar manner to the ”user-manager.service.ts” and defined as a private member. In this case, I created an expression using es6/es2015 fat arrow to reuse and simplify wrapping the relevant callbacks:
attachSignIn() {
// an experssion for reuse with "this.zone"
const run = (fn) => (r) => this.zone.run(() => fn(r));
if (this.auth2 && !this.isSignedIn && !this.isAuthInitiated) {
this.isAuthInitiated = true;
// Attach the click handler to the sign-in button with "run"
this.auth2.attachClickHandler('signin-button', {}, run(this.onLoginSuccess.bind(this)), run(this.onLoginFailed.bind(this)));
}
}
That’s it - problem is solved for this scenario.
NgZone has a lot more to offer. If there is a code that should be run, however shouldn’t affect the change detection, then the ”runOutsideAngular” method should be used.
Moreover, NgZone emits some useful events like: onUnstable, onError and more.
Feel free to explore the code of Echoes Player with Angular (+2)