I'm building a site that has a pretty standard RESTful web service to handle persistence and complex business logic. The UI I'm building to consume this service is using Angular 2 with components written in TypeScript.
Rather than build my own authentication system, I'm hoping to rely on Google Sign-In for Websites. The idea being that users will come to the site, sign in via the framework provided there and then send along the resulting ID tokens, which the server hosting the RESTful service can then verify.
In the Google Sign-In documentation there are instructions for creating the login button via JavaScript which is what needs to happen since the login button is being rendered dynamically in an Angular template. The relevant portion of the template:
<div class="login-wrapper">
<p>You need to log in.</p>
<div id="{{googleLoginButtonId}}"></div>
</div>
<div class="main-application">
<p>Hello, {{userDisplayName}}!</p>
</div>
And the Angular 2 component definition in Typescript:
import {Component} from "angular2/core";
// Google's login API namespace
declare var gapi:any;
@Component({
selector: "sous-app",
templateUrl: "templates/sous-app-template.html"
})
export class SousAppComponent {
googleLoginButtonId = "google-login-button";
userAuthToken = null;
userDisplayName = "empty";
constructor() {
console.log(this);
}
// Angular hook that allows for interaction with elements inserted by the
// rendering of a view.
ngAfterViewInit() {
// Converts the Google login button stub to an actual button.
api.signin2.render(
this.googleLoginButtonId,
{
"onSuccess": this.onGoogleLoginSuccess,
"scope": "profile",
"theme": "dark"
});
}
// Triggered after a user successfully logs in using the Google external
// login provider.
onGoogleLoginSuccess(loggedInUser) {
this.userAuthToken = loggedInUser.getAuthResponse().id_token;
this.userDisplayName = loggedInUser.getBasicProfile().getName();
console.log(this);
}
}
The basic flow goes:
- Angular renders the template and the message "Hello, empty!" is shown.
- The
ngAfterViewInit
hook is fired and the gapi.signin2.render(...)
method is called which converts the empty div into a Google login button. This works correctly and clicking on that button will trigger the login process.
- This also attaches the component's
onGoogleLoginSuccess
method to actually process the returned token after a user logs in.
- Angular detects that the
userDisplayName
property has changed and updates the page to now display "Hello, Craig (or whatever your name is)!".
The first problem that occurs is in the onGoogleLoginSuccess
method. Notice the console.log(...)
calls in the constructor
and in that method. As expected, the one in the constructor
returns the Angular component. The one in the onGoogleLoginSuccess
method, however, returns the JavaScript window
object.
So it looks like the context is getting lost in the process of hopping out to Google's login logic so my next step was to try incorporating jQuery's $.proxy
call to hang on to the correct context. So I import the jQuery namespace by adding declare var $:any;
to the top of the component and then convert the contents of the ngAfterViewInit
method to be:
// Angular hook that allows for interaction with elements inserted by the
// rendering of a view.
ngAfterViewInit() {
var loginProxy = $.proxy(this.onGoogleLoginSuccess, this);
// Converts the Google login button stub to an actual button.
gapi.signin2.render(
this.googleLoginButtonId,
{
"onSuccess": loginProxy,
"scope": "profile",
"theme": "dark"
});
}
After adding that, the two console.log
calls return the same object so property values are now updating correctly. The second log message shows the object with the expected updated property values.
Unfortunately, the Angular template does not get updated when this happens. While debugging, I stumbled on something that I believe explains what is going on. I added the following line to the end of the ngAfterViewInit
hook:
setTimeout(function() {
this.googleLoginButtonId = this.googleLoginButtonId },
5000);
This shouldn't really do anything. It just waits five seconds after the hook ends and then sets a property value equal to itself. However, with the line in place the "Hello, empty!"
message turns into "Hello, Craig!"
about five seconds after the page has loaded. This suggest to me that Angular just isn't noticing that the property values are changing in the onGoogleLoginSuccess
method. So when something else happens to notify Angular that property values have changed (such as the otherwise useless self-assignment above), Angular wakes up and updates everything.
Obviously that's not a hack I want to leave in place so I'm wondering if any Angular experts out there can clue me in? Is there some call I should be making to force Angular to notice some properties have changed?
UPDATED 2016-02-21 to provided clarity on the specific answer that solved the problem
I ended up needing to use both pieces of the suggestion provided in the selected answer.
First, exactly as suggested, I needed to convert the onGoogleLoginSuccess
method to use an arrow function. Secondly, I needed to make use of an NgZone
object to make sure that the property updates occurred in a context of which Angular is aware. So the final method ended up looking like
onGoogleLoginSuccess = (loggedInUser) => {
this._zone.run(() => {
this.userAuthToken = loggedInUser.getAuthResponse().id_token;
this.userDisplayName = loggedInUser.getBasicProfile().getName();
});
}
I did need to import the _zone
object: import {Component, NgZone} from "angular2/core";
I also needed to inject it as suggested in the answer via the class's contructor: constructor(private _zone: NgZone) { }
See Question&Answers more detail:
os