I tried to import some classes or function from Google gapi.auth2 in typescript. But below code never works even I correctly added the gapi.auth2 types in typings directory.
import { GoogleAuth } from 'gapi.auth2';
I always got error:
Error TS2307: Cannot find module 'gapi.auth2'
Shall I use some relative directory searching, such as '../../typings/gapi.auth2'?
Or maybe the way I am using the gapi is totally wrong?
Thanks!
To use gapi
and gapi.auth
with Angular2, install the type script definitions using NPM.
npm install --save @types/gapi
npm install --save @types/gapi.auth2
This will install two packages, @types/gapi and @types/gapi.auth2 to the node_modules
folder and save the configuration in package.json
.
Inspect your node_modules
folder to check they install correctly. If your Angular app is called "main-app", you should see:
main-app/
node_modules/
@types/
gapi/
gapi.auth2/
The red pill and blue pill scenario:
gapi
or gapi.auth2
to the "types": []
compiler option in tsconfig.json
because@types
packages are included during compilation.node_modules/@types
(of any enclosing folder) are considered visible.types
is already specified, the TSConfig Reference explains that you MUST add gapi
or gapi.auth2
otherwise they will NOT be included by default. In this secnario, edit tsconfig.json
to include new gapi
and gapi.auth2
types:{
"compilerOptions": {
"types": ["jest", "lodash", "gapi", "gapi.auth2"]
}
}
At this point, if you feel motivated enough, you could read Typescript Module Resolution, you can skip straight to How Node.js resolves modules:
Node will look for your modules in special folders named
node_modules
. Anode_modules
folder can be on the same level as the current file, or higher up in the directory chain. Node will walk up the directory chain, looking through eachnode_modules
until it finds the module you tried to load.
For this reason, you shouldn't need to add a reference to the type definitions in your Angular2 Service or Component (or wherever you're using gapi
or gapi.auth2
).
However, if you do add a reference to the gapi
or gapi.auth2
TypeScript definitions, it must reference the .ts
file installed using npm install
(note, you must keep the ///
oherwise you'll get an error):
/// <reference path="../../node_modules/@types/gapi/index.d.ts" />
The path is relative, so yours may differ depending on where your .ts
file is relative to where you installed the TypeScript definitions.
Whether you added an explicit reference or used TypeScript's Node module resolution mechanism, you still need to declare your variables in your .ts
file so Angular knows about the window gapi
variable at compile time. Add declare var gapi: any;
to your .ts
file but do not place it within a class definition. I put mine just below any imports:
// You may not have this explicit reference.
/// <reference path="../../node_modules/@types/gapi/index.d.ts" />
import { NgZone, Injectable, Optional } from '@angular/core';
declare var gapi: any;
Working with Other JavaScript Libraries in TypeScript documentation is worth reading to understand what we're getting with all this work.
Next, load the gapi
client from your own function (possibly in an Angular Service):
loadClient(): Promise<any> {
return new Promise((resolve, reject) => {
this.zone.run(() => {
gapi.load('client', {
callback: resolve,
onerror: reject,
timeout: 1000, // 5 seconds.
ontimeout: reject
});
});
});
}
This function is non-trivial, here's why...
Firstly, note we're calling gapi.load with a configuration object and not a callback. The GAPI reference states either can be used:
Using a configuration option allows us to reject the Promise when loading the library times-out, or just errors. In my experience, loading the library fails more often than initializing it - which is why configuration object is better than just a callback.
Secondly, we're wrapping gapi.load
in
this.zone.run(() => {
// gapi.load
});
NgZone.run is documented and states
Running functions via
zone.run
allows you to reenter Angular zone from a task that was executed outside of the Angular zone [...]
This is exactly what we want since the call to gapi.load
leaves the Angular zone. Omitting this can leave to very funky results that can be hard to debug.
Thirdly, loadClient()
returns a promise that is resolved - allowing the caller to choose how they handle gapi.load
. For example if our loadClient
method belonged to an Angular service, apiLoaderServce
, a component may use ngOnInit
to load gapi
:
ngOnInit(): void {
this.apiLoaderService.loadClient().then(
result => this.apiLoaded = true,
err => this.apiLoaded = false
);
}
Once gapi.load
has been called, gapi.client
will be ready and you should use it to initializes the JavaScript client with you API key, OAuth client ID, scope, and API discovery document(s):
initClient(): Promise<any> {
var API_KEY = // Your API key.
var DISCOVERY_DOC = // Your discovery doc URL.
var initObj = {
'apiKey': API_KEY,
'discoveryDocs': [DISCOVERY_DOC],
};
return new Promise((resolve, reject) => {
this.zone.run(() => {
gapi.client.init(initObj).then(resolve, reject);
});
});
}
Notice our friend NgZone.run is used once again to ensure the Angular Zone is re-entered.
In practice, I add loadClient()
and initClient()
to an Angular Service. In a high-level Angular component (usually just below the app-component) I load and initialize in ngOnInit
:
ngOnInit(): void {
this.apiLoaderService.loadClient().then(
result => {
this.apiLoaded = true;
return this.apiLoaderService.initClient()
},
err => {
this.apiFailed = true;
}
).then(result => {
this.apiReady = true;
}, err => {
this.apiFailed = true;
});
}
Lastly, you need to add the gapi script file to your file.
<html>
<head>
<script src="https://apis.google.com/js/api.js"></script>
You must not use the async
or defer
attributes since they will cause the Angular world to enter before the gapi library has loaded.
<!-- This will not work. -->
<html>
<head>
<script async defer src="https://apis.google.com/js/api.js"></script>
I previously suggested keeping page-load speeds fast by loading a local, minified copy of the gapi library in the /main-app/src/assests
folder and importing:
<html>
<head>
<script src="assets/api.js"></script>
However, I strongly recommend not doing this. Google may update https://apis.google.com/js/api.js and your client will break. I have been caught-out by this twice. In the end it was better just to import from //apis.google.com/js/
and keep it as a blocking call.