I'm working my first Svelte app which includes an embedded Google Map.
Setup Google Maps With Svelte 3 provided a good starting point for creating a component which asynchronously loads a Google Map. However, I can't figure out how to access the map object outside the initial onMount call, and without access to that object I can't add in functionality I want (for example, re-centering the map based on a button the user clicks).
main.js
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
ready: false,
}
});
window.initMap = function ready() {
app.$set({ ready: true });
}
export default app;
App.svelte
<script>
import Map from './Map.svelte';
export let ready;
</script>
<svelte:head>
<script defer async src="https://maps.googleapis.com/maps/api/js?key={}&callback=initMap"></script>
</svelte:head>
{ #if ready }
<Map></Map>
{ /if }
Map.svelte
<script>
// Imports
import { onMount } from 'svelte';
// Globals
let map;
let container;
// Load the map async
onMount(async () => {
map = new google.maps.Map(container, {
zoom: 6,
center: { lat: 0, lng: 0 },
});
});
// This function has no access to the `map` variable created above!!
function recenterMap() {
map.setCenter({lat: 1, lng: 1});
}
</script>
<div class="full-screen" bind:this={container}></div>
<button on:click="{recenterMap}"></button>
Running the above gives an error for trying to invoke the setCenter call on an object that does not exist. I've searched for how other frameworks (React, Vue) might handle this, but the solutions are specific to the frameworks and are not applicable here.
Any help would be greatly appreciated!
Related
I am trying to create a custom button for arrows in the drawing tool of leaflet-geoman.
The idea was to work with the copyDrawControl function, and to use Line as a model to make Polylines with arrow tips.
I wrote a code mostly inspired from this demonstration https://codesandbox.io/s/394eq?file=/src/index.js and modified it for my goals. Here is the code :
import { useEffect } from "react";
import { useLeafletContext } from "#react-leaflet/core";
import "#geoman-io/leaflet-geoman-free";
import "#geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
const Geoman = () => {
const context = useLeafletContext();
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
positions: {
draw: 'topleft',
edit: 'topright',
},
drawMarker: false,
rotateMode: false,
cutPolygon: false,
position: "bottomright"
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
name: 'SoonToBeArrow',
block: 'draw',
title: 'Display text on hover button',
actions: [
// uses the default 'cancel' action
'cancel',
],
});
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
export default Geoman;
When trying to add the copyDrawControl, I faced a bug that would announce that "Button with this name already exists"
I suspect its because I add the button inside a useEffect that gets called several times, but it's also the only way to access leafletContainer, since it must be updated everytime the context changes.
I tried creating another useEffect that contains the same context and my new button, but it did not work.
Does anyone have any suggestion on how to solve this ?
Thnak you in advance
You only want to run this effect once, just after context becomes available. In order to do this, we can make a state variable to track whether or not you've already added the control:
const Geoman = () => {
const context = useLeafletContext();
const [added, setAdded] = useState(false);
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
// if the context is ready, and we've not yet added the control
if (leafletContainer && !added){
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
// ...
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
// ...
});
// register that we've already added the control
setAdded(true);
}
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
In this way, you effect will run whenever context changes - once context is ready, you add the control. You register that you've added the control, but then your if statement will make sure that further changes in context will not try to keep adding controls again and again.
BTW, a second option to using leaflet geoman with react leaflet is to use the official createControlComponent hook to create custom controls. This is not at all straightforward with leaflet-geoman, as createControlComponent requires you to feed it an instance of an L.Control that has all the required hooks and initializer methods. geoman does not have these - it is quite different in the way it initializes and adds to a map. However, you can create an L.Control from geoman methods, and then feed it to createControlComponent.
Create the L.Control:
/**
* Class abstraction wrapper around geoman, so that we can create an instance
* that is an extension of L.Control, so that react-leaflet can call all
* L.PM methods using the expected L.Control lifecycle event handlers
*/
const GeomanControl = L.Control.extend({
initialize(options: Props) {
L.PM.setOptIn(options.optIn ?? false);
L.setOptions(this, options);
},
addTo(map: L.Map) {
const { globalOptions, events } = this.options;
// This should never happen, but its better than crashing the page
if (!map.pm) return;
map.pm.addControls(toolbarOptions);
map.pm.setGlobalOptions({
pmIgnore: false,
...globalOptions,
});
// draw control options
map.pm.addControls({
// ...
});
// new button
map.pm.Toolbar.copyDrawControl('Line', {
// ...
});
Object.entries(events ?? {}).forEach(([eventName, handler]) => {
map.on(eventName, handler);
});
},
});
Then simply use createControlComponent
const createControl = (props) => {
return new GeomanControl(props);
};
export const Geoman = createControlComponent(createControl);
You can add quite a lot of logic into the addTo method, and base a lot of its behaviors off the props you feed to <Geoman />. This is another flexible way of adapting geoman for react-leaflet v4.
I am tring to use the Google Map Javascript API into my VueJs project WITHOUT using this package https://www.npmjs.com/package/vue2-google-maps (because it's too limited as I see it).
So here what I have done, after having register my Vue component in app.js:
require('./bootstrap');
window.Vue = require('vue');
const app = new Vue({
el: '#app',
});
Vue.component(
'map-component',
require('./components/MapComponent.vue').default
);
In MapComponent.vue
<template>
<div id="map"></div>
</template>
<script>
export default {
data: function() {
return {
}
},
created() {
let googleApi = document.createElement('script');
googleApi.setAttribute('src', 'https://maps.googleapis.com/maps/api/js?key=AIzaSyA.......S-3SvLw_-twW72Zg&callback='+this.initMap+'');
document.head.appendChild(googleApi);
},
mounted() {
initMap: function() {
var map;
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
},
}
</script>
I also tried to switch created() with mounted(), but it shows the same error
The result without error should show a Google map on the page.
Thanks for your help
Aymeric
Try this in Created this.initMap() = function() { ... }
Your code will not work because you are trying to access a variable that is not initialized yet. In Vue First created method is executed then mounted method.
More info here: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks
I am trying to use marker-animate-unobtrusive but I keep getting this error:
I found another post on SO that talked about requiring the file after google has loaded but I do not know how to do that. in my component I have this:
import SlidingMarker from 'marker-animate-unobtrusive'
In my mounted method I have this:
SlidingMarker.initializeGlobally()
Any help is greatly appreciated
This is expected error since SlidingMarker extends google.maps.Marker class, GoogleMaps JavaScript API library needs to be loaded first, one option would to add a reference via index.html file:
<script src="https://maps.googleapis.com/maps/api/js?key=--KEY-GOES-HERE--"></script>
Another option would to utilize async JavaScript loader, e.g. scriptjs. The example for loading GoogleMaps JavaScript API and marker-animate-unobtrusive module could look like this:
created: function(){
get("https://maps.googleapis.com/maps/api/js?key=", () => {
const SlidingMarker = require('marker-animate-unobtrusive')
SlidingMarker.initializeGlobally()
const map = new google.maps.Map(document.getElementById('map'), this.mapOptions);
const marker = new SlidingMarker({
position: this.mapOptions.center,
map: map,
title: 'Im sliding marker'
});
});
}
Here is a demo for your reference
Update
With vue-google-maps library marker-animate-unobtrusive plugin could be integrated like this:
<template>
<div>
<GmapMap :center="center" :zoom="zoom" ref="mapRef"></GmapMap>
</div>
</template>
<script>
/* global google */
export default {
data() {
return {
zoom: 12,
center: { lat: 51.5287718, lng: -0.2416804 },
};
},
mounted: function() {
this.$refs.mapRef.$mapPromise.then(() => {
this.initSlidingMarker(this.$refs.mapRef.$mapObject)
})
},
methods: {
initSlidingMarker(map){
const SlidingMarker = require('marker-animate-unobtrusive')
SlidingMarker.initializeGlobally()
const marker = new SlidingMarker({
position: map.getCenter(),
map: map,
title: 'Im sliding marker'
});
google.maps.event.addListener(map, 'click', (event) => {
marker.setDuration(1000);
marker.setEasing('linear');
marker.setPosition(event.latLng);
});
}
}
}
</script>
<style>
.vue-map-container {
height: 640px;
}
</style>
Install the package google from npm this should fix it.
I'm trying to use the Google Maps API with Meteor 1.3 and with the dburles:google-maps package.
I tried various way to load it but the thing is that I can't use it because it takes too long to load I think and my page is rendered before.
I load it this way in my main.js to be sure that is loaded first.
import { GoogleMaps } from 'meteor/dburles:google-maps';
import { Meteor } from 'meteor/meteor';
Meteor.startup(function () {
GoogleMaps.load({ key: 'myKey' });
});
Then I include the helper in my template to display the map.
<template name="home">
<h1>Home</h1>
<div class="map-container">
{{> googleMap name="exampleMap" options=exampleMapOptions}}
</div>
</template>
Finally there is my helper to set the options for the template.
import { Template } from 'meteor/templating';
import { GoogleMaps } from 'meteor/dburles:google-maps';
import './home_page.html';
Template.home.helpers({
exampleMapOptions() {
// Make sure the maps API has loaded
if (GoogleMaps.loaded()) {
// Map initialization options
return {
center: new google.maps.LatLng(-37.8136, 144.9631),
zoom: 8,
};
}
},
});
Template.home.onCreated(function() {
GoogleMaps.ready('exampleMap', function(map) {
console.log("I'm ready!");
});
});
I think that the condition if (GoogleMaps.loaded()) is the reason why nothing is displayed but if I dont put it I got an error because the google object doesn't exist.
If you don't have an error on your JS console, the map could be loaded but not shown for missing css.
If it's so, Add the line below to your main.css
body, html {
height: 100%;
width: 100%;
}
.map-container {
width: 100%;
height: 100%;
}
I want to use a mixin within my VUEJS module:
Module
<script>
var GoogleMaps = require('../mixins/GoogleMaps');
export default {
mixins: [GoogleMaps],
events: {
MapsApiLoaded: function(data) {
GoogleMaps.initGISMap(data);
}
},
}
</script>
Mixin
export default {
methods: {
initGISMap(selector) {
map = new google.maps.Map(selector, {
zoom: 10,
mapTypeId: google.maps.MapTypeId.ROADMAP,
});
// Set initial Location and center map to this location
initialLocation = new google.maps.LatLng(48.184845, 11.252553);
map.setCenter(initialLocation);
// Create a searchmarker
searchMarker = createMarker();
// Init Autocomplete for GIS
initAutoComplete();
}
}
}
But I get an error, that GoogleMaps.initGISMap is not a function. How do I use a method of a mixin within a component?
-- edit to correct mistake I made in interpreting your needs
When using mixins, you don't reference the methods MixinName.method() - it's just 'this' - those methods and properties returned by your mixin and are first order, so to speak, so they are bound to 'this'.
<script>
var GoogleMaps = require('../mixins/GoogleMaps');
export default {
mixins: [GoogleMaps],
events: {
MapsApiLoaded: function(data) {
this.initGISMap(data);
}
},
}
</script>