Thursday, 2 June, 2016 UTC


Summary

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.
Limitations Outside Angular (+2) World
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:
  1. Youtube Player sends events from youtube domain to echoes player domain.
  2. Google Sign-in sends events from google auth domain to echoes player domain.
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.
The Problems That Have Been Resolved By NgZone
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.

The Youtube Player Problem: No UI Update

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.

The Youtube Player Solution: NgZone

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

The Google Sign-in Problem: No User’s Playlists

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:
  1. the user navigates to the “my playlists” screen
  2. then clicks the google sign-in button
  3. a pop up window in google’s domain opens with details to sign in
  4. the user authorizes sign-in to echoes player
  5. the pop-up is closed and the user is back to echoes player page
  6. echoes player gets access the user’s playlists
  7. the playlists should be rendered and the sign-in button should be hidden
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.

The Google Sign-in Solution: NgZone

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.
What’s More With NgZone
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)