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
155 views
in Technique[技术] by (71.8m points)

javascript - Google Sign-In for Websites and Angular 2 using Typescript

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:

  1. Angular renders the template and the message "Hello, empty!" is shown.
  2. 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.
  3. This also attaches the component's onGoogleLoginSuccess method to actually process the returned token after a user logs in.
  4. 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

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

1 Reply

0 votes
by (71.8m points)

For your first problem solution is to use arrow function which will preserve context of this :

  onGoogleLoginSuccess = (loggedInUser) => {
    this.userAuthToken = loggedInUser.getAuthResponse().id_token;
    this.userDisplayName = loggedInUser.getBasicProfile().getName();
    console.log(this);
  }

Second issue is happening because third-party scripts run outside the context of Angular. Angular uses zones so when you run something, for example setTimeout(), which is monkey-patched to run in the zone, Angular will get notified. You would run jQuery in zone like this:

  constructor(private zone: NgZone) {
    this.zone.run(() => {
      $.proxy(this.onGoogleLoginSuccess, this);
    });
  }

There are many questions/answers about the zone with much better explanations then mine, if you want to know more, but it shouldn't be an issue for your example if you use arrow function.


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

...