Using Google Maps with Statamic and Alpine.js
I’ve recently begun work building a Statamic starter for myself, to help speed up development on new upcoming projects. As a part of this initiative, I wanted to include some common components and patterns that are often requested as part of a typical marketing-style website, yet keep the CSS to a minimum for easy styling once a given project gets going.
I decided to include a component featuring Google Maps, with a list of locations to the side which could focus a pin on the map, and possibly open an info window, when that list item gets clicked. It would look something like this:
Creating content in Statamic
First of all, I needed to create some fields in Statamic to handle the necessary info. In Globals, I have an “API Keys” blueprint, in which I have a field for my Google Maps API key, which will be needed later on as we get to the actual map.
Next, I created a Fieldset for the Map component itself, which mostly consists of a Replicator for locations. This in turn consists of some text fields, including fields for Latitude and Longitude for placing the locations on the map.
My Antlers template contains the new Google Maps API script (more on that later), as well as a JSON object containing the location information:
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "{{ api_keys:google_maps_api_key }}",
v: "weekly",
});
</script>
<script>
window.locationsOfInterest = {{ locations | to_json }};
</script>
Additionally, the template loops through the locations creating buttons for each location. These buttons will trigger methods in an Alpine component, which will handle the Google Maps initialization, as well as any logic needed to manipulate the map itself.
We also need a container for the map itself:
<ul class="space-y-4">
{{ locations }}
<li>
<h4>
<button
type="button"
data-lat="{{ latitude }}"
data-lng="{{ longitude }}"
data-name="{{ name }}"
@click="focusLocation"
class="text-blue-600"
>
{{ name }}
</button>
</h4>
<p class="text-lg">{{ short_description }}</p>
</li>
{{ /locations }}
</ul>
...
<div x-ref="map" class="aspect-video"></div>
Setting up the JavaScript with Alpine.js
Next, we will initialize Google Maps in the component’s init
method. With the new Google Maps script mentioned above, there are some changes that I should point out which could affect how to initialize certain things in and around your Google Map.
For instance, we no longer reference google
on the window
object when invoking the Map constructor. Rather, we import the Map class asynchronously from the “maps” library:
// Alpine component
export default () => ({
init() {
this.initMap()
},
map,
markers: [],
async initMap() {
let map;
let bounds;
let mapEl = this.$refs.map;
if( mapEl ) {
// see https://developers.google.com/maps/documentation/javascript/overview#javascript
const { Map } = await google.maps.importLibrary("maps");
map = new Map( mapEl, {
center: { lat: 49.24, lng: -123.1256 },
zoom: 8,
mapId: 'DEMO_MAP_ID',
scaleControl: true,
});
this.map = map;
// etc.
Likewise, to create our markers, we need to import some classes from some more libraries. The old “Marker” constructor is deprecated in this new version:
async setMarkers({ listings, map }) {
const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker");
const { InfoWindow } = await google.maps.importLibrary("maps");
// Create an info window to share between markers.
const infoWindow = new InfoWindow();
listings.forEach( location => {
const marker = new AdvancedMarkerElement({
map: map,
title: location.name,
position: {
lat: parseFloat(location.latitude),
lng: parseFloat(location.longitude),
},
})
/**
* This method gets invoked when a
* marker click is triggered
*/
marker.addListener("click", () => {
infoWindow.close();
infoWindow.setContent(marker.title);
infoWindow.open(marker.map, marker);
map.panTo(marker.position)
});
/**
* Push the marker to an array on the
* component for reference later
*/
this.markers.push(marker)
});
}
Finally, the focusLocation
method called on the button @click
taps into the Google Maps functions:
focusLocation(e) {
const { dataset } = e.target;
const marker = this.markers.find(m => m.title === dataset.name);
if (marker) {
google.maps.event.trigger(marker, 'click');
}
}
And just like that, we are working with the latest Google Maps JavaScript API, combined with an Alpine component and getting data from our Statamic CMS.
Naturally, there is more code to consider for setting the map bounds, how to set the map size based on the markers, and such. See it in action:
The use of Statamic, Alpine.js, and Google Maps is a powerful combo. Getting our structured data from the Statamic Fieldset, outputting in Antlers, and combined with light touch of Alpine to help us build out the interactions, feels like a superpower, especially for something as dynamic and complex as maps.
Now we are off to the races, to build our apps quicker than ever.
Part 2: Filtering Google Maps with Statamic and Alpine.js
For additional information on some of the new APIs for interacting with the Advanced Markers and Infowindows, see these links: