Skip to content

Michael LaRoy - Home

Filtering Google Maps with Statamic and Alpine.js


After writing about Google Maps with Statamic and Alpine.js, I thought it might be nice to extend this piece by adding some basic filtering on the map, and provide a nice interaction that would look like this:

Animated screnshot of a Google Map with pinned locations, with a select for filtering

In order to achieve this, we’ll need to create a taxonomy in Statamic so that each location can reference a shared location_type taxonomy, which we can use to filter the list of locations displayed, as well as their corresponding pins. In addition, we’ll leverage the Alpine Components Listbox.

If you don’t want to use the (paid) Alpine components, a simple select element with an x-model should do the trick, though you won’t get the keyboard interactions or the nice styles.

Creating a taxonomy in Statamic

To begin, I created a taxonomy in Statamic called Location Type, and seeded a few terms like restaurants, schools, and industrial to start out with.

This is easy enough to accomplish in the Statamic dashboard - in the Taxonomy section of the dashboard, click the big blue button to create a new taxonomy, and fill in the requied fields. That’s it!

Statamic admin dashboard: taxonomy view

Once we create some terms for the taxonomy, we can add a field into our Locations fieldset, which will reference this new taxonomy’s entries.

Updating the the view templates

In the template that shows our buttons on the left side of the view, we can add a new section above it to show our taxonomy terms. For the listbox, we’ll add this container that handles the component logic; otherwise, we’d use a select element with the same x-model binding to control the active term. Most CSS classes are omitted for brevity:

<div
    x-listbox
    x-model="locationFilter"
    class="relative mb-4"
    x-effect="filterMarkers"
>
    <label x-listbox:label class="sr-only">Location Type</label>

    <button x-listbox:button>
        <span x-text="locationFilter ? locationFilter : 'Filter locations'" class="truncate"></span>
        <!-- Heroicons up/down can go here-->
    </button>

    <ul
        x-listbox:options
        x-transition.origin.top.left
        x-cloak
        class="absolute left-0"
    >

        <!-- list items go here -->

    </ul>
</div>

As for the list items, we will output them as buttons (as below), or as select options with the same value:

{{ taxonomy:location_types }}
    <li x-listbox:option value="{{ title }}">
        {{ title }}
        <span x-show="$listboxOption.isSelected">&check;</span>
    </li>
{{ /taxonomy:location_types }}

Finally, we will update the location list items to show only when each one’s respective term is active:

<ul class="space-y-4">
    {{ locations }}
        <li x-show="locationFilter == 'All' || locationFilter == '{{ location_type['title'] }}'">
            <button
                type="button"
                data-lat="{{ latitude }}"
                data-lng="{{ longitude }}"
                data-name="{{ name }}"
                @click="focusLocation"
                >
                {{ name }}
            </button>
            <p>{{ short_description }}</p>
        </li>
    {{ /locations }}
</ul>

Hooking up the Alpine component

Next, we need to leverage the Listbox behaviour to filter our markers and buttons. Notice the listbox has the x-effect="filterMarkers" attribute - this is so that the filterMarkers method will fire whenever the locationFilter model changes.

This will also fire when the component loads, so we can set up our initial marker display logic here in this method.

But first, we need to add a property to our markers so that they are aware of the new data (see the post linked at the top for more complete code):

// part of the async setMarkers method

const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");

listings.forEach( location => {
    const marker = new AdvancedMarkerElement({
        map: map,
        title: location.name,
        position: {
            lat: parseFloat(location.latitude),
            lng: parseFloat(location.longitude),
        },
    })

    // this is our location term from Statamic 👇🏼
    marker.locationType: location.location_type.title
});

// etc.

this.markers.push(marker)

Finally, we need to flesh out a few more methods so that that the filtering mechanism can a) set or remove the markers from the map, and b) resize the map bounds according to the new list of markers showing:


// Function to be called on `x-effect`
filterMarkers() {
    const map = Alpine.raw(this.map);
    const filteredListings = [];

    this.markers.forEach(item => {
        if(this.locationFilter == 'All' || item.locationType == this.locationFilter) {
            item.map = map;
            const listing = this.listings.find(listing => listing.name === item.title);
            filteredListings.push(listing);
        } else {
            item.map = null
        }
    })

    // errors will be thrown if there are no markers,
    // which will happen the first time this runs
    if(!this.markers.length) return;

    // set new map bounds after
    // filtering the markers
    const bounds = this.getBuildingBounds(filteredListings);
    this.setMapSize(bounds, map)

},

setMapSize(bounds, map){
    if(bounds) {
        // changes the map bounds based on the filtered data
        map.fitBounds( bounds );

        // handy for adjusting with the window resizes,
        // not detailed here
        this.handleBoundsOnResize({ bounds, map })
    }
},

getBuildingBounds(listings) {
    const lats = listings.map(listing => parseFloat(listing.latitude))
    const lngs = listings.map(listing => parseFloat(listing.longitude))

    const buildingBounds = {
        north: Math.max(...lats),
        south: Math.min(...lats),
        east: Math.max(...lngs),
        west: Math.min(...lngs)
    }

    return buildingBounds;
},

Wrapping it up

The magic of Alpine’s x-effect lets us tap into the changing value of the location filter term, and run our methods without the need to write any more code to run them explicitly. And without much more effort, we can resize the map to fit with the currently displayed markers. Even better, it runs straight away, without needing us to invoke the functions directly.

On top of that, the Alpine Listbox gives us a nice little accessible UI that we can style the way we want. Have you got your hands on Alpine Components yet?

I’d suggest you do.

Dwight from The Office: Do it now

5 Accessibility Fixes You Can Make Today

Learn about the most common reasons that websites fail accessibility standards, and what you can do about it.