Currently, I am in the process of creating a website that utilizes a typical RESTful web service to manage persistence and intricate business logic. To consume this service, I am using Angular 2 with components coded in TypeScript.
Instead of developing my own authentication system, I am planning to use Google Sign-In for Websites. The plan is for users to visit the site, sign in through the provided framework, and then send the resulting ID tokens to the server hosting the RESTful service for verification.
The Google Sign-In documentation contains detailed instructions on creating the login button using JavaScript, as the login button will be dynamically rendered in an Angular template. Here is the relevant part 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>
Furthermore, the Angular 2 component definition in Typescript is as follows:
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 allowing 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 unfolds as follows:
- Angular renders the template, displaying the message "Hello, empty!"
- The
ngAfterViewInit
hook is executed, calling thegapi.signin2.render(...)
method, which transforms the empty div into a Google login button. This works correctly, and clicking the button initiates the login process. - The component's
onGoogleLoginSuccess
method is also linked to process the returned token post user login. - Angular detects the change in the
userDisplayName
property and updates the page to show "Hello, Craig (or any other name)!"
The first issue arises in the onGoogleLoginSuccess
method, where the context gets lost, indicating the console.log(...)
in the constructor
returns the Angular component, while the one in the onGoogleLoginSuccess
method returns the JavaScript window
object.
It appears that the context is lost during the transition to Google's login logic. To address this, I attempted to integrate jQuery's $.proxy
call to maintain the correct context. This involved adding declare var $:any;
at the top of the component and modifying the contents of the ngAfterViewInit
method as follows:
// Angular hook enabling interaction with elements inserted by the view rendering.
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"
});
}
Following this adjustment, both console.log
calls return the same object, ensuring the property values update correctly. The second log displays the object with the expected updated property values.
However, the Angular template does not update accordingly. Through debugging, I discovered adding the following line at the end of the ngAfterViewInit
hook:
setTimeout(function() {
this.googleLoginButtonId = this.googleLoginButtonId },
5000);
Although seemingly redundant, this line causes the "Hello, empty!" message to transition to "Hello, Craig!" approximately five seconds after the page loads. This implies that Angular fails to notice property value changes in the onGoogleLoginSuccess
method, and only reacts when prompted by changes detected through other means.
This makeshift approach is not sustainable, prompting me to seek advice from Angular experts on how to address this. Is there a specific call I should be making to ensure Angular recognizes property changes?
UPDATED 2016-02-21 to provide clarity on the specific solution that resolved the issue
In the end, I found it necessary to implement both aspects of the suggested solution.
Firstly, as recommended, I converted the onGoogleLoginSuccess
method to utilize an arrow function. Additionally, I utilized an NgZone
object to ensure property updates occur within a context recognized by Angular. The final method now appears as follows:
onGoogleLoginSuccess = (loggedInUser) => {
this._zone.run(() => {
this.userAuthToken = loggedInUser.getAuthResponse().id_token;
this.userDisplayName = loggedInUser.getBasicProfile().getName();
});
}
It was necessary to import the _zone
object:
import {Component, NgZone} from "angular2/core";
Additionally, I had to inject it as suggested in the response through the class's constructor:
constructor(private _zone: NgZone) { }