Lazy load Google Maps library in React with TypeScript - google-maps

I'm following this tutorial to write a Google Maps React Component that lazy loads the library. I have implemented ScriptCache from this gist. The problem I have is that the code only shows Loading map... and the map is never rendered. Any obvious thing that I have missed?
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
<title>App</title>
</head>
<body>
<div id="app"></div>
<script src="/Scripts/dist/bundle.js"></script>
</body>
</html>
index.tsx:
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactRouter from "react-router";
import * as ReactBootstrap from "react-bootstrap";
import Container from "./Google/GoogleMapComponent";
ReactDOM.render(
<div>
<Container google={(window as any).google} />
</div>,
document.getElementById("app")
);
GoogleApi.tsx:
export const GoogleApi = function (opts: any) {
opts = opts || {}
const apiKey: any = opts.apiKey;
const libraries: any = opts.libraries || [];
const client: any = opts.client;
const URL: string = 'https://maps.googleapis.com/maps/api/js';
const googleVersion: string = '3.22';
let script: any = null;
let google: any = (window as any).google = null;
let loading = false;
let channel: any = null;
let language: any = null;
let region: any = null;
let onLoadEvents: any[] = [];
const url = () => {
let url = URL;
let params = {
key: apiKey,
callback: 'CALLBACK_NAME',
libraries: libraries.join(','),
client: client,
v: googleVersion,
channel: channel,
language: language,
region: region
}
let paramStr = Object.keys(params)
.filter(k => !!(params as any)[k])
.map(k => `${k}=${(params as any)[k]}`).join('&');
return `${url}?${paramStr}`;
}
return url();
}
export default GoogleApi
GoogleApiComponent.tsx:
import * as React from "react";
import * as ReactDOM from 'react-dom'
import cache from './ScriptCache'
import GoogleApi from './GoogleApi'
const defaultMapConfig = {}
export const wrapper = (options: any) => (WrappedComponent: any) => {
const apiKey = options.apiKey;
const libraries = options.libraries || ['places'];
class Wrapper extends React.Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
this.state = {
loaded: false,
map: null,
google: null
}
}
scriptCache: any;
map: any;
mapComponent: any
refs: {
[string: string]: any;
map: any;
}
componentDidMount() {
const refs: any = this.refs;
this.scriptCache.google.onLoad((err: any, tag: any) => {
const maps = (window as any).google.maps;
const props = Object.assign({}, this.props, {
loaded: this.state.loaded
});
const mapRef: any = refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let center = new maps.LatLng(this.props.lat, this.props.lng)
let mapConfig = Object.assign({}, defaultMapConfig, {
center, zoom: this.props.zoom
})
this.map = new maps.Map(node, mapConfig);
this.setState({
loaded: true,
map: this.map,
google: (window as any).google
})
});
}
componentWillMount() {
this.scriptCache = cache({
google: GoogleApi({
apiKey: apiKey,
libraries: libraries
})
});
}
render() {
const props = Object.assign({}, this.props, {
loaded: this.state.loaded,
map: this.state.map,
google: this.state.google,
mapComponent: this.refs.map
})
return (
<div>
<WrappedComponent {...props} />
<div ref='map' />
</div>
)
}
}
return Wrapper;
}
export default wrapper;
GoogleMapComponent.tsx:
import * as React from "react";
import * as ReactDOM from 'react-dom'
import GoogleApiComponent from "./GoogleApiComponent";
export class Container extends React.Component<any, any> {
render() {
const style = {
width: '100px',
height: '100px'
}
return (
<div style={style}>
<Map google={this.props.google} />
</div>
)
}
}
export default GoogleApiComponent({
apiKey: 'AIzaSyAyesbQMyKVVbBgKVi2g6VX7mop2z96jBo ' //From Fullstackreact.com
})(Container)
export class Map extends React.Component<any, any> {
refs: {
[string: string]: any;
map: any;
}
map: any;
componentDidMount() {
this.loadMap();
}
componentDidUpdate(prevProps: any, prevState: any) {
if (prevProps.google !== this.props.google) {
this.loadMap();
}
}
loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;
const mapRef = this.refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let zoom = 14;
let lat = 37.774929;
let lng = -122.419416;
const center = new maps.LatLng(lat, lng);
const mapConfig = Object.assign({}, {
center: center,
zoom: zoom
})
this.map = new maps.Map(node, mapConfig);
}
// ...
}
render() {
return (
<div ref='map'>
Loading map...
</div>
)
}
}
ScriptCache.tsx:
let counter = 0;
let scriptMap = new Map();
export const ScriptCache = (function (global: any) {
return function ScriptCache(scripts: any) {
const Cache: any = {}
Cache._onLoad = function (key: any) {
return (cb: any) => {
let stored = scriptMap.get(key);
if (stored) {
stored.promise.then(() => {
stored.error ? cb(stored.error) : cb(null, stored)
})
} else {
// TODO:
}
}
}
Cache._scriptTag = (key: any, src: any) => {
if (!scriptMap.has(key)) {
let tag : any = document.createElement('script');
let promise = new Promise((resolve: any, reject: any) => {
let resolved = false,
errored = false,
body = document.getElementsByTagName('body')[0];
tag.type = 'text/javascript';
tag.async = false; // Load in order
const cbName = `loaderCB${counter++}${Date.now()}`;
let cb: any;
let handleResult = (state: any) => {
return (evt: any) => {
let stored = scriptMap.get(key);
if (state === 'loaded') {
stored.resolved = true;
resolve(src);
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = []
} else if (state === 'error') {
stored.errored = true;
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = [];
reject(evt)
}
cleanup();
}
}
const cleanup = () => {
if (global[cbName] && typeof global[cbName] === 'function') {
global[cbName] = null;
}
}
tag.onload = handleResult('loaded');
tag.onerror = handleResult('error')
tag.onreadystatechange = () => {
handleResult(tag.readyState)
}
// Pick off callback, if there is one
if (src.match(/callback=CALLBACK_NAME/)) {
src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`)
cb = (window as any)[cbName] = tag.onload;
} else {
tag.addEventListener('load', tag.onload)
}
tag.addEventListener('error', tag.onerror);
tag.src = src;
body.appendChild(tag);
return tag;
});
let initialState = {
loaded: false,
error: false,
promise: promise,
tag
}
scriptMap.set(key, initialState);
}
return scriptMap.get(key);
}
Object.keys(scripts).forEach(function (key) {
const script = scripts[key];
Cache[key] = {
tag: Cache._scriptTag(key, script),
onLoad: Cache._onLoad(key)
}
})
return Cache;
}
})(window)
export default ScriptCache;

Even though Container looks like this nothing will get printed:
export class Container extends React.Component<any, any> {
render()
{
const style = {
width: '100px',
height: '100px'
}
return (
<div style={style}>
<Map google={this.props.google} />
</div>
)
}
}
If I do however set width and height on map ref directly it will print correctly. You can not add width and height in percent.
export class Map extends React.Component<any, any> {
...
render() {
const style = {
width: '100vw',
height: '100vh'
}
return (
<div ref='map' style={style}>
Loading map...
</div>
)
}
}

Related

Date picker in Html is not working as expected

I'm working on a requirement where date picker is displayed based on min and max dates when minimum and maximum are provided. But the format shows as attached enter image description here
Requirement is enter image description here
Below is the code snippet i have added HTML and TS.
<mat-form-field>
<input matInput type="date" [min]="todayDate [max]="maxReleaseDate" name='spoDate'
#spoDate="ngModel" [(ngModel)]="newSpoDate" autocomplete="off" required
[disabled]="spoPoCreateDate">
<mat-error *ngIf="spoDate && spoDate.untouched && spoDate.invalid">This is required field
</mat-error>
</mat-form-field>
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { DatePipe } from '#angular/common';
import { Order } from 'app/general-component/models/order';
import { GetService } from 'app/general-component/services/get.service';
import { PostService } from 'app/general-component/services/post.service';
import { environment } from 'environments/environment.prod';
#Component({
selector: 'app-spo-date-popup',
templateUrl: './spo-date-popup.component.html',
styleUrls: ['./spo-date-popup.component.css']
})
export class SpoDatePopupComponent implements OnInit {
currentSpoDate: any;
newSpoDate: any
orderId: any;
selectedOrganization: any;
itemId: any;
orderData: Order;
todayDate: any;
spoCreatePoDateNote: any;
maxReleaseDate: any
orderLineId: any;
currentOrderId: any;
loggedInUser: string;
userGrantList: any;
spoPoCreateDate: boolean;
constructor(private route: ActivatedRoute, private getService: GetService, private postService: PostService, private datePipe: DatePipe) { }
ngOnInit(): void {
let date = this.datePipe.transform(new Date(), 'dd/MM/yyyy');
this.todayDate = (date as string).split('/').reverse().join('-');
this.route.queryParams.subscribe(
(params) => {
this.orderId = params["orderId"];
this.selectedOrganization = params["selectedOrganization"];
this.itemId = params["ItemId"];
this.orderLineId = params["OrderLineId"];
this.currentOrderId = params["OrderId"];
this.loggedInUser = params["loggedInUser"]
},
(error) => console.log("error" + error)
)
this.grantCheck();
this.getService.getData(environment.getOrderByIdURL + this.currentOrderId, this.selectedOrganization).subscribe(res => {
this.orderData = res as Order;
if (this.orderData) {
this.orderData.OrderLine.forEach((e) => {
if (this.orderLineId === e.OrderLineId) {
this.currentSpoDate = e.Extended.PODropDate ? e.Extended.PODropDate : "";
this.maxReleaseDate = e.Extended.ReleaseDate ? this.datePipe.transform(new Date(e.Extended.ReleaseDate), 'yyy-MM-dd') : "";
console.log("SPOPODATEPOPUP :: ", this.maxReleaseDate);
}
})
}
});
let configData = {};
this.getService.getData(`${environment.configStoreURL}/REST_EX01`, this.selectedOrganization).subscribe((res) => {
let REST_EX01_CONFIG = res.ConfigStoreData;
if (res.ConfigStoreData) {
let list = REST_EX01_CONFIG.split(';');
list.forEach(e => {
let ex01Configs = e.split('=');
configData = {
...configData,
[ex01Configs[0]]: ex01Configs[1]
}
})
// #ts-ignore
this.spoCreatePoDateNote = configData.SPOPOCreateDateUpdateNote;
}
})
}
updateDate(res: any) {
console.log(res);
if (res && res.spoDate) {
if (this.orderData && this.orderData.OrderLine) {
let orderLineData = this.orderData.OrderLine.find((i) => i.OrderLineId === this.orderLineId);
let isAllOLSame = this.orderData.OrderLine.every((e) =>
e.OrderLinePromisingInfo && e.FulfillmentGroupId &&
e.FulfillmentGroupId === orderLineData.FulfillmentGroupId &&
e.OrderLinePromisingInfo.ShipFromLocationId === orderLineData.OrderLinePromisingInfo.ShipFromLocationId);
this.orderData.OrderLine.forEach((e) => {
if (this.orderLineId === e.OrderLineId) {
e.Extended.PODropDate = this.datePipe.transform(res.spoDate, 'MM/dd/yyy');
e.Extended.SPOPOCreateDateUpdated = true;
} else {
e.Extended.PODropDate = e.Extended.PODropDate;
e.Extended.SPOPOCreateDateUpdated = e.Extended.SPOPOCreateDateUpdated;
}
if (isAllOLSame) {
e.Extended.PODropDate = this.datePipe.transform(res.spoDate, 'MM/dd/yyy');
e.Extended.SPOPOCreateDateUpdated = true;
}
})
this.currentSpoDate = this.datePipe.transform(res.spoDate, 'MM/dd/yyy');
this.saveOrder(this.orderData)
}
}
// Extended.SPOPOCreateDateUpdated is flipped to true on updating
}
saveOrder(order: any) {
// let saveOrder: any;
// let postURL = environment.post_order_save;
// saveOrder = this.postService.postData(postURL, this.selectedOrganization, order);
this.postService.postData(environment.post_order_save, this.selectedOrganization, order)
.switchMap((res) => {
let data = {}
return this.postService.postData(environment.rhFetaFocURL, this.selectedOrganization, data)
})
.subscribe(response => {
},
error => { throw error; }
);
}
grantCheck() {
this.getService.getData(environment.getUserGrant + this.loggedInUser, this.selectedOrganization).subscribe(response => {
this.userGrantList = response;
let spoPoCreateDateGrant = this.userGrantList.some((res) => {
return res.ResourceId === "SPOPOCreateDateUpdate";
})
if (!spoPoCreateDateGrant) {
this.spoPoCreateDate = true;
} else {
this.spoPoCreateDate = false
}
console.log('SPOPOCREATEDATE', this.spoPoCreateDate);
});
}
}
can anyone suggest how to fix?
This is might solve your problem
<input type="date" value="2022-02-03" min="2022-02-03" max="2022-03-03">

React : i have following code, i am using setInterval to change text value time to time ,when i switch pages and come back text is changing reallyfast

I assume when component is rendered multiple times something happens to setinterval,but how can i fix this.
bottom code is for Store that i am using and i don't understand.someone said that i must have useffect outside component but then it gives me error.
Anyways im new to react so i need help ,everyones appriciated.Thanks.
import SmallLogo from '../img/logo.svg';
import StarskyText from '../img/starskyproject.svg';
import './Statement.css'
import { BrowserRouter as Router,Routes,Route,Link } from "react-router-dom";
import { getElementError } from '#testing-library/react';
import react, { useRef , useState, useEffect } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { store } from "./appReducer";
function TempText(props) {
return <span className="yellow changetext"> {props.body} </span>;
}
function doUpdate(callback) {
setInterval(callback, 1300);
}
export default function Statement(){
const dispatch = useDispatch();
const textOptions = ["NFT", "CRYPTO", "METAVERSE", "WEB3"];
const tempText = useSelector((state) => state.tempText);
function change() {
let state = store.getState();
const index = state.index;
console.log(index);
console.log(textOptions[index]);
dispatch({
type: "updatetext",
payload: textOptions[index]
});
let newIndex = index + 1 >= textOptions.length ? 0 : index + 1;
dispatch({
type: "updateindex",
payload: newIndex
});
}
useEffect(() => {
doUpdate(change);
}, []);
var [dropdownOpen , Setdrop] = useState(false);
return(
<div>
<Link to="/">
<img className='star-fixed' alt='starlogo' src={SmallLogo}></img>
</Link>
<img className='starsky-fixed' alt='starsky-project' src={StarskyText}></img>
<div className='text-content'>
<span className='statement-text'>WEB3 IS NOT ONLY THE FUTURE.
IT’S THE ONLY FUTURE!</span>
<span className='starsk-link'>starsk.pro</span>
</div>
<div className='text-content-bottom'>
<span className='statement-text-bottom'>CREATE YOUR NEXT
<TempText body={tempText} />
<span className='flex'> PROJECT WITH
<Dropdown className="hover-drop-out" onMouseOver={() => Setdrop(dropdownOpen=true) } onMouseLeave={() => Setdrop(dropdownOpen=false)} isOpen={dropdownOpen} toggle={() => Setdrop(dropdownOpen = !dropdownOpen) }>
<DropdownToggle className='hover-drop'> STRSK.PRO </DropdownToggle>
<DropdownMenu> </DropdownMenu>
</Dropdown> </span>
</span>
</div>
</div>
)
}
import { createStore } from "redux";
const initialState = {
tempText: "NFT",
index: 1
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "updatetext":
return {
...state,
tempText: action.payload
};
case "updateindex":
return {
...state,
index: action.payload
};
default:
return state;
}
};
export const store = createStore(reducer);
You can clear your timer by calling clearTimeout function with a reference to your timer when your component unmounting.
useEffect(() => {
const timer = setInterval(change, 1300);
// in order to clear your timeout
return () => clearTimeout(timer);
}, [])

eosjs's httpEndPoint is not changed

I test some eosjs apis.
But, I am stucked because of some problems.
This is my code.
/////////////////////////////////
import * as Eos from 'eosjs'
class EOSService {
constructor() {
this.eos = Eos({
httpEndPoint : 'https://eu1.eosdac.io:443',
chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
})
}
fetchTransactionData(txid, callback) {
this.eos.getTransaction(txid)
.then( (result) => {
callback(result)
})
}
fetchAccountData(accountName, callback) {
this.eos.getAccount(accountName)
.then( (result) => {
callback(result)
})
}
}
export default EOSService;
I call this methods from react component like below.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import EOSService from 'services/EOSService'
class AccountPage extends Component {
componentDidMount() {
let accountName = this.props.match.params.accountName;
(new EOSService()).fetchAccountData(accountName, (result) => {
console.log(result)
})
}
render() {
return(
<div>AccountPage</div>
);
}
}
AccountPage.propTypes = propTypes;
AccountPage.defaultProps = defaultProps;
export default AccountPage;
But, I encounter error like this.
=> POST http://127.0.0.1:8888/v1/chain/get_account 0 ()
In other words, I want to ask data from BP's httpEndPoint, but eosjs called 127.0.0.1:8888.
How to solve this?
Summary
You have to use httpEndpoint, not httpEndPoint.
Check below:
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/eosjs#16.0.0/lib/eos.min.js"
integrity="sha512-vNyLnOEb7uFmEtVbLnyZQ9/k4zckM2Vu3jJOKq6XfWEVZG0yKcjDExVN4EQ7e3F+rePWRncMolI2xFi/3qo62A=="
crossorigin="anonymous"></script>
<script>
eos = Eos({
httpEndpoint: 'https://eu1.eosdac.io:443',
chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
verbose: true
})
</script>
</head>
<body>
See console object: Eos
</body>
</html>
If you use httpEndPoint, config will be set by default. Check this eos.js code:
var Eos = function Eos() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
config = Object.assign({}, {
httpEndpoint: 'http://127.0.0.1:8888',
debug: false,
verbose: false,
broadcast: true,
sign: true
}, config);
var defaultLogger = {
log: config.verbose ? console.log : null,
error: console.error
};
config.logger = Object.assign({}, defaultLogger, config.logger);
return createEos(config);
};

React+Redux - show InfoWindow on Marker click

I would like to display InfoWindow on Marker click. I followed some tutorials and I used react-google-maps for my project. I would like my app to work like this: "https://tomchentw.github.io/react-google-maps/basics/pop-up-window" but my code is a little bit different.
class Map extends React.Component {
handleMarkerClick(){
console.log("Clicked");
}
handleMarkerClose(){
console.log("CLOSE");
}
render(){
const mapContainer= <div style={{height:'100%',width:'100%'}}></div>
//fetch markers
const markers = this.props.markers.map((marker,i) => {
return (
<Marker key={i} position={marker.location} showTime={false} time={marker.time} onClick={this.handleMarkerClick} >
{
<InfoWindow onCloseClick={this.handleMarkerClose}>
<div>{marker.time}</div>
</InfoWindow>
}
</Marker>
)
})
/* set center equals to last marker's position */
var centerPos;
if(markers[markers.length-1]!== undefined)
{
centerPos=markers[markers.length-1].props.position;
}
else {
centerPos={};
}
return (
<GoogleMapLoader
containerElement={mapContainer}
googleMapElement={
<GoogleMap
defaultZoom={17}
center={centerPos}
>
{markers}
</GoogleMap>
}/>
);
}
}
export default Map;
I got "this.props.markers" from another class component, which fetching data from URL. I am almost sure, that it is easy problem to solve. Currently on marker click in console I got "Clicked" and on Marker close "CLOSE" as you can guess from above code it is because of handleMarkerClick() and handleMarkerClose(). I want to have pop-window with InfoWindow.
What should I do to make it work?
Here is heroku link : App on heroku
Hi I came across the same requirement. I did this (I am using redux and redux-thunk) :
GoogleMap.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
withGoogleMap,
GoogleMap,
Marker,
InfoWindow
} from 'react-google-maps';
import { onMarkerClose } from '../actions/Cabs';
const GettingStartedGoogleMap = withGoogleMap(props => (
<GoogleMap
defaultZoom={12}
defaultCenter={{ lat: 12.9716, lng: 77.5946 }}
>
{props.markers.map( (marker, index) => (
<Marker {...marker} onClick={() => props.onMarkerClose(marker.key)}>
{marker.showInfo &&(
<InfoWindow onCloseClick={() => props.onMarkerClose(marker.key)}>
<div>
<h1>Popover Window</h1>
</div>
</InfoWindow>
)}
</Marker>
))}
</GoogleMap>
));
class CabType extends Component{
constructor(props){
super(props);
}
render(){
if(this.props.cabs.length === 0){
return <div>loading...</div>
}
return(
<div className="map-wrapper">
<GettingStartedGoogleMap
containerElement={
<div style={{ height: '100%' }} />
}
mapElement={
<div style={{ height: '100%' }} />
}
onMarkerClose = {this.props.onMarkerClose}
markers={this.props.showMap ? this.props.markers : []}
/>
</div>
)
}
}
export default connect(store => {return {
cabs : store.cabs,
markers: store.markers
}}, {
onMarkerClose
})(CabType);
Action.js
const getMarkers = (cabs , name) => dispatch => {
let markers = [];
let data = {};
cabs.map(cab => {
if(cab.showMap){
data = {
position: {
lat : cab.currentPosition.latitude,
lng : cab.currentPosition.longitude
},
showInfo: false,
key: cab.cabName,
icon: "/images/car-top.png",
driver: cab.driver,
contact: cab.driverContact,
};
markers.push(data);
}
});
dispatch(emitMarker(markers));
};
function emitSetMarker(payload){
return{
type: SET_MARKER,
payload
}
}
export const onMarkerClose = (key) => dispatch => {
dispatch(emitSetMarker(key))
};
RootReducer.js
import { combineReducers } from 'redux';
import { cabs } from "./Cabs";
import { markers } from "./Markers";
const rootReducer = combineReducers({
cabs,
markers,
});
export default rootReducer;
MarkerReducer.js
import { GET_MARKERS, SET_MARKER } from "../types"
export const markers = (state = [], action) => {
switch (action.type){
case GET_MARKERS:
return action.payload;
case SET_MARKER:
let newMarker = state.map(m => {
if(m.key === action.payload){
m.showInfo = !m.showInfo;
}
return m;
});
return newMarker;
default: return state;
}
};
Sorry for a long post but this is code which is tested and running. Cheers!

setting google map API key from angular 2 service

Im using sebastine google map in angular 2 application. I know AgmCoreModule.forRoot({ apiKey: "xxxxxxxx" }) can be used to set API key but I need to set API key from a angular service in my #component is it possible....help needed.
You may need to update provide a custom provider like this { provide: MapsAPILoader, useClass: CustomLazyAPIKeyLoader } where you have imported AgmCoreModule.
And in CustomLazyAPIKeyLoader class override the load method.
import { Injectable, Inject } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { MapsAPILoader, LAZY_MAPS_API_CONFIG, LazyMapsAPILoaderConfigLiteral, GoogleMapsScriptProtocol } from 'angular2-google-maps/core';
import { DocumentRef, WindowRef } from 'angular2-google-maps/core/utils/browser-globals';
#Injectable()
export class CustomLazyAPIKeyLoader extends MapsAPILoader {
private _scriptLoadingPromise: Promise<void>;
private _config: LazyMapsAPILoaderConfigLiteral;
private _windowRef: WindowRef;
private _documentRef: DocumentRef;
constructor( #Inject(LAZY_MAPS_API_CONFIG) config: any, w: WindowRef, d: DocumentRef, private http: Http) {
super();
this._config = config || {};
this._windowRef = w;
this._documentRef = d;
}
load(): Promise<void> {
if (this._scriptLoadingPromise) {
return this._scriptLoadingPromise;
}
const script = this._documentRef.getNativeDocument().createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
const callbackName: string = `angular2GoogleMapsLazyMapsAPILoader`;
this.http.get("getKey")
.subscribe((res: any) => {
this._config.apiKey = res._body;
script.src = this._getScriptSrc(callbackName);
this._documentRef.getNativeDocument().body.appendChild(script);
});
this._scriptLoadingPromise = new Promise<void>((resolve: Function, reject: Function) => {
(<any>this._windowRef.getNativeWindow())[callbackName] = () => { console.log("loaded"); resolve(); };
script.onerror = (error: Event) => { reject(error); };
});
return this._scriptLoadingPromise;
}
private _getScriptSrc(callbackName: string): string {
let protocolType: GoogleMapsScriptProtocol =
(this._config && this._config.protocol) || GoogleMapsScriptProtocol.HTTPS;
let protocol: string;
switch (protocolType) {
case GoogleMapsScriptProtocol.AUTO:
protocol = '';
break;
case GoogleMapsScriptProtocol.HTTP:
protocol = 'http:';
break;
case GoogleMapsScriptProtocol.HTTPS:
protocol = 'https:';
break;
}
const hostAndPath: string = this._config.hostAndPath || 'maps.googleapis.com/maps/api/js';
const queryParams: { [key: string]: string | Array<string> } = {
v: this._config.apiVersion || '3',
callback: callbackName,
key: this._config.apiKey,
client: this._config.clientId,
channel: this._config.channel,
libraries: this._config.libraries,
region: this._config.region,
language: this._config.language
};
const params: string =
Object.keys(queryParams)
.filter((k: string) => queryParams[k] != null)
.filter((k: string) => {
// remove empty arrays
return !Array.isArray(queryParams[k]) ||
(Array.isArray(queryParams[k]) && queryParams[k].length > 0);
})
.map((k: string) => {
// join arrays as comma seperated strings
let i = queryParams[k];
if (Array.isArray(i)) {
return { key: k, value: i.join(',') };
}
return { key: k, value: queryParams[k] };
})
.map((entry: { key: string, value: string }) => { return `${entry.key}=${entry.value}`; })
.join('&');
return `${protocol}//${hostAndPath}?${params}`;
}
}
this.http.get("getKey")
.subscribe((res: any) => {
this._config.apiKey = res._body;
script.src = this._getScriptSrc(callbackName);
this._documentRef.getNativeDocument().body.appendChild(script);
});
Above code will make it async.
I added an resolver to get the API key
import { Resolve, ActivatedRouteSnapshot } from '#angular/router'
import { Injectable, Inject } from '#angular/core'
import { SomeService } from '../services/some.service'
import { LazyMapsAPILoaderConfigLiteral, LAZY_MAPS_API_CONFIG } from '#agm/core'
import { Observable, of } from 'rxjs'
import { map, catchError } from 'rxjs/operators'
#Injectable()
export class GoogleMapAPiResolver implements Resolve<boolean> {
constructor(
private someService: SomeService,
#Inject(LAZY_MAPS_API_CONFIG)
private config: LazyMapsAPILoaderConfigLiteral
) {}
resolve(router: ActivatedRouteSnapshot): Observable<boolean> {
return this.someService.getGoogleMapApiKey().pipe(
catchError(error => {
return of(false)
}),
map(response => {
this.config.apiKey = response
return true
})
)
}
}
The SomeService consume an endpoint that return the Key
you have put the API key in the app.module.ts under the #NgModule
and make sure to enable Maps JavaScript API in the google cloud console
https://console.cloud.google.com/apis/library/maps-backend.googleapis.com
Thanks!
#NgModule({
imports: [
BrowserModule,
FormsModule,
AgmCoreModule.forRoot({
// please get your own API key here:
// https://developers.google.com/maps/documentation/javascript/get-api-key?hl=en
apiKey: 'API_KEY'
})
],
declarations: [ AppComponent, HelloComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }