Load Google Maps JS API in component [Angular] - javascript

How is it possible to load an external js file, from a url in an Angular component?
To be specific, I'm trying to load google-maps-api to my angular project. Currently, I'm doing it in my index.html like this:
<script async defer src="https://maps.googleapis.com/maps/api/js?key=API_KEY&callback=initMap">
</script>
Note: I'm aware of angular-maps. That's not an option.

You can use Promise to load google API asynchronously whenever you want.
// map-loader.service.ts
import ...
declare var window: any;
#Injectable()
export class MapLoader {
private static promise;
map: google.maps.Map;
public static load() {
if (!MapLoader.promise) { // load once
MapLoader.promise = new Promise((resolve) => {
window['__onGapiLoaded'] = (ev) => {
console.log('gapi loaded')
resolve(window.gapi);
}
console.log('loading..')
const node = document.createElement('script');
node.src = 'https://maps.googleapis.com/maps/api/js?key=<YOUR _API_KEY>&callback=__onGapiLoaded';
node.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(node);
});
}
return MapLoader.promise;
}
initMap(gmapElement, lat, lng) {
return MapLoader.load().then((gapi) => {
this.map = new google.maps.Map(gmapElement.nativeElement, {
center: new google.maps.LatLng(lat, lng),
zoom: 12
});
});
}
}
Component
//maps.component.ts
...
#Component({
selector: 'maps-page',
templateUrl: './maps.component.html'
})
export class MapsComponent implements OnInit {
#ViewChild('gmap') gmapElement: any;
constructor(public mapLoader: MapLoader) { }
ngOnInit() {
this.mapLoader.initMap(this.gmapElement, -37.81, 144.96) // await until gapi load
}
...
}
Component HTML
// maps.component.html
<div #gmap ></div>
Don't forget to add CSS to the element.

A solution is to create the script tag dynamically in ngAfterViewInit()
import { DOCUMENT } from '#angular/common';
...
constructor(private #Inject(DOCUMENT) document,
private elementRef:ElementRef) {};
ngAfterViewInit() {
var s = this.document.createElement("script");
s.type = "text/javascript";
s.src = "https://maps.googleapis.com/maps/api/js?key=API_KEY";
this.elementRef.nativeElement.appendChild(s);
}
See also https://github.com/angular/angular/issues/4903
Update
<div id="map" #mapRef></div>
export class GoogleComponent implements OnInit {
#ViewChild("mapRef") mapRef: ElementRef;
constructor() { }
ngOnInit() {
this.showMap();
}
showMap() {
console.log(this.mapRef.nativeElement);
const location = { lat: 28.5355, lng: 77.3910 };
var options = {
center: location,
zoom: 8
}
const map = new google.maps.Map(this.mapRef.nativeElement, options);
this.addMarket(map, location);
}
addMarket(pos, map) {
return new google.maps.Marker({
position: pos,
map: map,
});
}
}

Related

Why can't I load 3D models with Mapbox GL JS and Threebox with this Angular code

Can anyone help me understand how I need to structure my code for this mapbox w/ threebox project in Angular?
I can't figure out why this code:
this.tb.loadObj(options, function (model) {
var house = model.setCoords(origin);
this.tb.add(house);
});
is throwing the following error:
ERROR Error: Uncaught (in promise): ReferenceError: tb is not defined
ReferenceError: tb is not defined
Even after seeming to recognize tb as defined when I run console.log statements within this code block. But then this error shows up right afterwards and my 3D model never loads.
Here is the full code for the component, any advice on how to solve this issue would be appreciated:
import { Component, OnInit } from '#angular/core';
import { environment } from '../../../environments/environment';
import {Threebox} from 'threebox-plugin';
import * as M from 'mapbox-gl';
#Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit {
/// default settings
long = -122.4192;
lat = 37.7793;
map: M.Map;
style = 'mapbox://styles/mapbox/light-v10';
// data
source: any;
markers: any;
// render
tb: Threebox;
constructor() { }
ngOnInit(): void {
(M as any).accessToken = environment.mapbox.accessToken;
this.buildMap();
this.map.on('style.load', this.onLoad.bind(this));
this.map.on('load', (event) => {
/// register source
this.map.addSource('localdata', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [this.long, this.lat]
},
properties: {
title: 'Mapbox',
description: 'San Francisco, California'
}
}]
}
});
/// get source
this.source = this.map.getSource('localdata')
// markers
this.source._data.features.forEach((marker) => {
var lng = marker['geometry']['coordinates'][0]
var lat = marker['geometry']['coordinates'][1]
// create a HTML element for each feature
var el = document.createElement('div');
el.className = 'marker';
// make a marker for each feature and add to the map
new M.Marker({color: 'black'})
.setLngLat([lng, lat])
.addTo(this.map);
});
});
// Add map controls
this.map.addControl(new M.NavigationControl());
this.map.on('mousemove', function (e) {
document.getElementById('info').innerHTML =
// e.point is the x, y coordinates of the mousemove event relative
// to the top-left corner of the map
// e.lngLat is the longitude, latitude geographical position of the event
e.lngLat.lat.toFixed(6) + ', ' + e.lngLat.lng.toFixed(6) ;
});
};
// functions
buildMap() {
this.map = new M.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10',
zoom: 18,
center: [this.long, this.lat],
pitch: 60,
});
}
onLoad(){
this.map.addLayer({
id: 'house',
type: 'custom',
// renderingMode: '3d',
onAdd(map, mbxContext) {
this.tb = new Threebox(
map,
mbxContext,
{ defaultLights: true }
);
//starting location for both map and eventual sphere
var origin = [this.long, this.lat];
var options = {
// obj: 'src/assets/3d/house.gltf',
obj: 'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
type: 'gltf',
scale: 1,
units: 'meters',
rotation: { x: 90, y: 0, z: 0 } //default rotation
}
this.tb.loadObj(options, function (model) {
var house = model.setCoords(origin);
this.tb.add(house);
});
},
render: function (gl, matrix) {
this.tb.update();
}
});
};
}
tb needs to be global:
https://github.com/jscastro76/threebox/blob/master/docs/Threebox.md#threebox-instance
try:
(window as any).tb = new Threebox(map, mbxContext, {defaultLights: true});
and
window['tb'].loadObj(options, function (model) {
var house = model.setCoords(origin);
window['tb'].add(house);
});
It seems as window.tb solves that issue.
Initialise tb outside onAdd function.
this.tb = new Threebox(map, mbxContext, options);
window.tb = this.tb;
Now you can use either this.tb or window.tb because both are the same.

Incorporating javascript maps into Angular

I am trying to incorporate Javascript google maps into my Angular 8 project in order to add more functionality than AGM. I have created a typings.d.ts file and included my javascript in the angular.json but i am still getting the error: 'map' is declared but its value is never read. Here is my code...
component.html
<body>
<div id="map"></div>
<script src="https://maps.googleapis.com/maps/api/js?key={{apiKey}}&callback=initMap"
async defer>
</script>
<script >
</script>
</body>
</html>
component.ts
import * as map from 'map';
import { Component, OnInit } from '#angular/core';
import { environment } from '../../environments/environment';
#Component({
selector: 'app-google-maps',
templateUrl: './google-maps.component.html',
styleUrls: ['./google-maps.component.css']
})
export class GoogleMapsComponent implements OnInit {
private apiKey = environment.googleMapsAPIkey;
constructor() { }
ngOnInit() {
}
}
maps.js
var map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
typings.d.ts
declare module Map {
export interface Main {
map;
}
}
declare var Map:Map.Main;
Following code displated google maps in developer mode.
html
<div style="width: 1000px;height: 300px;" id="map"></div>
component.ts
constructor() {
var dynamicScripts = ["https://maps.googleapis.com/maps/api/js?key="apiKey"];
for (var i = 0; i < dynamicScripts.length; i++) {
let node = document.createElement('script');
node.src = dynamicScripts [i];
node.type = 'text/javascript';
node.async = true;
node.charset = 'utf-8';
document.getElementsByTagName('head')[0].appendChild(node);
}
}
initMap() {
var uluru = {lat: -25.363, lng: 131.044};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: uluru
});
}
ngOnInit() {
setTimeout(()=>{ //
this.initMap();
},2000);
}
Note Instead of setTimeout we can use promies.then().

OverlappingMarkerSpiderfier is not defined (Vue.js)

I'm trying to implement the OverlappingMarkerSpiderfier for my Google Maps, and it works because my markers are able to "spiderfy" when I click on a marker.
My issue is that in my dev console on VS Code, ESLint is still giving me the error 'OverlappingMarkerSpiderfier' is not defined. I don't really know what the issue is since my markers are working as intended when I click on them. Below is a picture showing that OverlappingMarkerWorkers even though there is an error from ESLint:
I want to get rid of the error in case a future error arises because of it. I've searched for solutions, and many people have commented that OverlappingMarkerSpiderfier should be loaded after Google Maps load. I've done that, but the error still persists.
I load my Google Maps asynchronously; below is my .js file that loads the Google Maps and OverlappingMarkerSpiderfier:
import api_keys from './api_keys'
const API_KEY = api_keys.google_maps_api_key;
const CALLBACK_NAME = 'gmapsCallback';
let initialized = !!window.google;
let resolveInitPromise;
let rejectInitPromise;
const initPromise = new Promise((resolve, reject) => {
resolveInitPromise = resolve;
rejectInitPromise = reject;
});
export default function init() {
if (initialized) return initPromise;
initialized = true;
window[CALLBACK_NAME] = () => resolveInitPromise(window.google);
const script = document.createElement('script');
script.async = true;
script.defer = true;
script.src = `https://maps.googleapis.com/maps/api/jskey=${API_KEY}&callback=${CALLBACK_NAME}`;
script.onerror = rejectInitPromise;
document.querySelector('head').appendChild(script);
const spiderfier = document.createElement('script');
spiderfier.defer = true;
spiderfier.src = "https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier/1.0.3/oms.min.js";
spiderfier.onerror = rejectInitPromise;
document.querySelector('head').appendChild(spiderfier);
return initPromise;
}
The following is my GoogleMaps component. The OverlappingMarkerSpiderfier implementation is located within "watch":
<template>
<div id="google-map">
</div>
</template>
<script>
import gMaps from '../lib/gMaps.js'
export default {
name: 'GoogleMaps',
props: {
events: Array
},
data() {
return {
map: null,
locations: []
}
},
async mounted() {
try {
const google = await gMaps();
const geocoder = new google.maps.Geocoder();
this.map = new google.maps.Map(this.$el);
geocoder.geocode({ address: 'USA'}, (results, status) => {
if (status !== 'OK' || !results[0]) {
throw new Error(status);
}
this.map.setCenter(results[0].geometry.location);
this.map.fitBounds(results[0].geometry.viewport);
});
} catch (error) {
console.error(error);
}
},
watch: {
async events() { //creates markers for the map; data is from a 3rd party API that is handled by a different component
try {
const google = await gMaps();
var oms = new OverlappingMarkerSpiderfier(this.map, {
markersWontMove: true,
markersWontHide: true,
basicFormatEvents: true
})
for(let i = 0; i < this.events.length; i++) {
let marker = new google.maps.Marker({
position: {
lat: parseInt(this.events[i].latitude, 10),
lng: parseInt(this.events[i].longitude, 10)
},
map: this.map,
title: this.events[i].title
})
let iw = new google.maps.InfoWindow({
content: this.events[i].description || 'No description available.'
});
google.maps.event.addListener(marker, 'spider_click', function() {
iw.open(this.map, marker);
});
oms.addMarker(marker);
}
}
catch(error) {
console.error(error)
}
}
}
}
</script>
<style lang="scss" scoped>
#google-map {
width: auto;
height: 100vh;
}
</style>
try either 1 of these
this.$nexttick(()=>{
code in mounted hook....
})
check if window.google object is loaded and your map reference is available before instantiating OverlappingMarkerSpiderfier.

VueJS2 Accessing HTML Elements within Child

I am usually using the ref keyword to access components within vue. But I seem to not have understood how I access HTML tags from within a component.
I tried:
<input type="text" ref="searchAddress" id="searchAddress" name="searchAddress" class="form-control" v-model="incidentForm.searchAddress">
and within the vue component:
var input = this.$refs.searchAddress;
but this does not work, so I assume it only works when referencing components? How do I access the input tag from within vue?
I created a class for managing all the GoogleMaps Api calls that will be included into my Vue files. Therefore I do not see a way, how I could handle accessing the data of a specific input field. What would be the right approach to avoid a direct access like this?
The full code example and being more specific:
The Autocomplete Function of GoogleMaps does not return anything as I would expect. Uncaught TypeError: Cannot set property 'autocompletePlace' of undefined. this.autocompletePlace = place seems to be not working.
methods: {
initMap: function initMap() {
this.initialLocation = this.createInitialLocation(48.184845, 11.252553);
this.mapSetup(this.$refs.map, this.initialLocation, function () {
this.initAutoCompleteListener(function (place) {
this.autocompletePlace = place;
});
}.bind(this));
}
}
GoogleMapsApi.js
export default {
data() {
return {
map: '',
currentIncidentLocation: '',
autocomplete: '',
searchMarker: ''
}
},
events: {
currentIncidentLocation: function(location) {
this.currentIncidentLocation = location;
}
},
methods: {
createInitialLocation: function(latitude, longitude) {
return new google.maps.LatLng(latitude, longitude);
},
mapSetup: function(selector, initialLocation, callback) {
this.map = new google.maps.Map(selector, {
zoom: 10,
mapTypeId: google.maps.MapTypeId.ROADMAP,
});
this.map.setCenter(initialLocation);
this.searchMarker = this.createMarker();
var input = document.getElementById('searchAddress');
this.autocomplete = new google.maps.places.Autocomplete(input);
callback();
},
initAutoCompleteListener: function(callback) {
this.autocomplete.addListener('place_changed', function() {
var place = this.autocomplete.getPlace();
if (!place.geometry) {
window.alert("Der Ort konnte nicht gefunden werden");
return;
}
callback(place);
}.bind(this));
},
createMarker: function() {
var marker = new google.maps.Marker({
map: this.map
})
return marker;
}
}
}
GISView.vue
<template>
<div ref="map" id="map" class="google-map" style="height: 800px; position: relative; overflow: hidden;">
</div>
</template>
<script>
import GoogleMaps from '../mixins/GoogleMaps.js';
export default {
mixins: [GoogleMaps],
data() {
return {
initialLocation: '',
autocompletePlace: ''
}
},
mounted() {
this.$events.$on("MapsAPILoaded", eventData => this.initMap());
},
methods: {
initMap: function() {
this.initialLocation = this.createInitialLocation(48.184845, 11.252553);
this.mapSetup(this.$refs.map, this.initialLocation, function() {
this.initAutoCompleteListener(function(place) {
this.autocompletePlace = place;
})
}.bind(this));
}
}
}
</script>
You bound the outer function, but not the inner one. Try
this.initAutoCompleteListener(function(place) {
this.autocompletePlace = place;
}.bind(this))
You can access HTML tags the same way using $refs, find below example:
<template>
<div class="example">
<input
type="text"
id="searchAddress"
name="searchAddress"
class="form-control"
ref="searchAddress"
placeholder="Input placeholder"
v-model="inputModel">
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
name: 'app',
data() {
return {
inputModel: ''
}
},
mounted() {
const el = this.$refs.searchAddress
console.log('Ref to input element: ', el)
console.log('Placeholder attr from the ref element: ', el.placeholder) // logs "Input placeholder"
}
}

Aurelia Google SignIn Button

I just started using Aurelia and I am having a problem with Google Sign in. It looks like I might be able to create my own Google button but I'd rather get it to work this way if it is possible. Here is my code:
<script src="https://apis.google.com/js/platform.js" async defer></script>
...
<body aurelia-app="src/main">
...
<span id="googleButtonPlaceholder" class="g-signin2" data-onsuccess="onSignIn"></span>
I have the function setup in my Aurelia class but I do not know if/how I can call it. I have tried ${onSignIn()} which just calls the function when it loads, ${onSignIn}, onSignIn(), onSignIn, data-onsuccess.bind="onSignin()" but nothing seems to work. Is there a way to pass the Aurelia function to the Google data-onsuccess attribute?
As a note, I am switching from Angular 1.5.8 where this previously worked.
Here's an example: https://gist.run?id=5da90f48b43b9c5867c8d2ace0f6371f
app.html
<template>
<require from="google-signin-button"></require>
<google-signin-button success.call="signinSuccess(googleUser)"
error.call="signinError(error)">
</google-signin-button>
<h1>${message}</h1>
</template>
app.js
export class App {
message = 'Not signed in.';
signinSuccess(googleUser) {
const name = googleUser.getBasicProfile().getName();
this.message = `Signed in: ${name}`;
}
signinError(error) {
this.message = `Error: ${error}`;
}
}
google-signin-button.js
import {inject, noView, bindable} from 'aurelia-framework';
const googleSigninClientID = '927519533400-mfupo3lq9cjd67fmmvtth7lg7d8l50q9.apps.googleusercontent.com';
function preparePlatform() {
// https://developers.google.com/identity/sign-in/web/build-button
// The name of the global function the platform API will call when
// it's ready.
const platformCallbackName = 'setGooglePlatformReady';
// An "API ready" promise that will be resolved when the platform API
// is ready.
const ready = new Promise(
resolve => window[platformCallbackName] = resolve);
// Inject the client id meta tag
const meta = document.createElement('meta');
meta.name = 'google-signin-client_id';
meta.content = googleSigninClientID;
document.head.appendChild(meta);
// Inject an async script element to load the google platform API.
// Notice the callback name is passed as an argument on the query string.
const script = document.createElement('script');
script.src = `https://apis.google.com/js/platform.js?onload=${platformCallbackName}`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
return ready;
}
const platformReady = preparePlatform();
#noView()
#inject(Element)
export class GoogleSigninButton {
#bindable success = googleUser => { };
#bindable error = error => { };
#bindable scope = 'profile email';
#bindable theme = 'dark';
#bindable width = 240;
#bindable height = 50;
constructor(element) {
this.element = element;
}
attached() {
platformReady.then(this.renderButton);
}
renderButton = () => {
gapi.signin2.render(this.element, {
scope: this.scope,
width: this.width,
height: this.height,
longtitle: true,
theme: this.theme,
onsuccess: googleUser => {
console.info(googleUser);
this.success({ googleUser });
},
onfailure: error => {
console.error(error);
this.failure({ error });
}
});
}
}
#JeremyDanyow had a great answer but after I went to bed and read a little more about Aurelia, I thought of a solution to try before seeing his answer so I thought I'd share an alternate approach for those interested.
index.html
<main aurelia-app="src/main">
</main>
<script src="https://apis.google.com/js/platform.js" async defer></script>
app.html
<template>
<span id="my-signin2"></span>
<!-- other stuff -->
</template>
app.js
attached() {
this.render();
}
render() {
gapi.signin2.render('my-signin2', {
'scope': 'profile email',
'theme': 'dark',
'onsuccess': this.onSuccess,
'onfailure': this.onFailure
});
}
onSuccess(googleuser) {
let gUser = googleuser.getBasicProfile(),
id_token = googleuser.getAuthResponse().id_token;
}
onFailure(error) {
console.log(error);
}
This approach differs slightly from what Google shows on their website where they have you give platform.js an onload function to render the button. Instead, I create the button in the template and then once the template is done being loaded, attached() is called, which in turn, calls the function I would have had platform.js call onload.
Try data-onsuccess.call="onSignIn()".
After following #JeremyDanyow's example around a few corners I came up with this
It works ok for simple usage, but needs help...
when there is another window open using a google login there is an error loading something in the iframe google adds (this doesn't seem to break it)
the listeners don't work for more than a couple of login/logouts at most
Here's hoping that someone else can improve upon this.
google-signin-button.js
import { inject, noView, bindable } from 'aurelia-framework';
import { LogManager } from 'aurelia-framework';
const Console = LogManager.getLogger('google-signin-button');
// Integrating Google Sign-In into your web app
// https://developers.google.com/identity/sign-in/web/reference
// https://console.developers.google.com/apis/credentials
// inspiration: https://developers.google.com/identity/sign-in/web/build-button
function preparePlatform(): Promise<Function> {
// Inject an async script element to load the google platform API.
const script = document.createElement('script');
script.src = `https://apis.google.com/js/platform.js?onload=gapi_ready`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
// return a promise that will resolve with the onload callback
return new Promise(resolve => window['gapi_ready'] = resolve);
}
#noView
#inject(Element)
export class GoogleSigninButton {
#bindable authenticated = (signedIn: Boolean) => { };
#bindable authorized = (GoogleUser: any) => { };
#bindable scope = 'profile email';
#bindable clientId = 'none';
#bindable theme = 'dark';
#bindable width = 240;
#bindable height = 50;
public element: Element;
constructor(element) {
this.element = element;
}
public wasAuthenticated: Boolean;
sendAuthenticated(signedIn: Boolean) {
if (signedIn !== this.wasAuthenticated) {
this.authenticated(signedIn);
this.wasAuthenticated = signedIn;
}
}
public wasAuthorized: any;
sendAuthorized(googleUser: any) {
if (googleUser !== this.wasAuthorized) {
this.authorized(googleUser);
this.wasAuthorized = googleUser;
this.sendAuthenticated(true);
}
}
attached() {
// inject the script tag
preparePlatform()
.then(() => {
// load the auth lib
// Console.debug('gapi created, loading auth2');
window['gapi'].load('auth2', () => {
// init the auth lib
// Console.debug('gapi.auth2 loaded, intializing with clientId:', this.clientId);
window['gapi'].auth2.init({
client_id: this.clientId
})
.then(
(googleAuth: any) => {
// Console.debug('gapi.auth2 intialized');
// listen for user signed in/out
googleAuth.isSignedIn.listen((signedIn: Boolean) => {
// Console.debug('googleAuth.isSignedIn.listener', signedIn);
this.sendAuthenticated(signedIn);
});
// listen for who signed in
googleAuth.currentUser.listen((googleUser: any) => {
// Console.debug('googleAuth.currentUser.listener', googleUser);
this.sendAuthorized(googleUser);
});
// draw the button
window['gapi'].signin2.render(this.element, {
scope: this.scope,
width: this.width,
height: this.height,
longtitle: true,
theme: this.theme,
onsuccess: (googleUser: any) => {
// Console.debug('gapi.signin2.render success', googleUser);
this.sendAuthorized(googleUser);
},
// drawing button failure
onfailure: (error: any) => {
Console.error('gapi.signin2.render failure', error);
}
});
},
// intialization error
(errObj: any) => {
Console.error('gapi.auth2.init -> errObj', errObj);
}
);
});
});
}
}
some-usage.js
import environment from '../environment';
import { LogManager } from 'aurelia-framework';
const Console = LogManager.getLogger('Login');
import { inject } from 'aurelia-framework';
import { AuthService } from 'aurelia-authentication';
import { EventAggregator } from 'aurelia-event-aggregator';
import './login.scss';
#inject(AuthService, EventAggregator)
export class Login {
public authService: AuthService;
public eventAggregator: EventAggregator;
public googleSigninClientID: string = 'none';
constructor(authService: AuthService, eventAggregator: EventAggregator) {
this.eventAggregator = eventAggregator;
this.authService = authService;
this.googleSigninClientID = environment.googleSigninClientID;
};
isAuthenticated(signedIn: Boolean) {
Console.warn('isAuthenticated', signedIn);
}
isAuthorized(googleUser: any) {
Console.warn('isAuthorized', googleUser);
}
}
some-usage.html
<template>
<require from="../resources/elements/google-signin-button"></require>
<section>
<div class="container-fluid">
<center>
<google-signin-button client-id.bind="googleSigninClientID" authenticated.bind="isAuthenticated" authorized.bind="isAuthorized"> </google-signin-button>
</center>
</div>
</section>
</template>

Categories

Resources