Re-using VueJS Mixins and Filtering Google Map Data

Part 30 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori December 11th, 2017
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

In our last tutorial, we built a whole bunch of mixing to help filter coffee shops https://serversideup.net/filtering-vuejs-mixins/. This filtering took place on the home page. To show the power and reusability of the mixins that we created, we are going to apply similar filters to the map of cafes. This filtering system will use the mixins that we already created.

To make this work we will need to add a searching component similar to the one we made for the home page. This searching box will appear hovered over the google map and allow users to select certain filters. We will then have to listen to the events on our CafeMap.vue component and filter the markers that are shown according to what the user has selected in the filter. Let’s get started!

Step 1: Create Google Maps Filter Component

Similar to the component we made for the home page, this component will handle all of our filtering UI/UX. We will place this little filter in the top right over the Google Map so when users select filters, we can show and hide markers. By adding filters to the map we add a whole new visualization aspect and we can find out coffee shops near a specific location that have certain features we are looking for.

First, add the following file: /resources/assets/js/components/cafes/CafeMapFilter.vue. This will be our visual filter container. Add the following component code:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';
</style>

<template>
  <div id="cafe-map-filter">

  </div>
</template>

<script>
  export default {

  }
</script>

Next, open up your /resources/assets/js/components/cafes/CafeMap.vue component. We will add a few small style tweaks to the component to make room for our filter. We will need to add a container around the cafe map in our template:

<div id="cafe-map-container">
    <div id="cafe-map">

    </div>
  </div>

We then need to adjust the styling so the map fills up the entire map container, and the map container fills up the page:

<style lang="scss">
  div#cafe-map-container{
    position: absolute;
    top: 50px;
    left: 0px;
    right: 0px;
    bottom: 50px;

    div#cafe-map{
      position: absolute;
      top: 0px;
      left: 0px;
      right: 0px;
      bottom: 0px;
    }
  }
</style>

Now we can include our CafeMapFilter.vue component into the map (even tho it’s empty right now). To import add:

import CafeMapFilter from './CafeMapFilter.vue';

then add the filter to the components array:

components: {
   CafeMapFilter
},

and finally underneath our map component:

<div id="cafe-map">

</div>
<cafe-map-filter></cafe-map-filter>

Since everything is absolutely positioned, we will be able to display it over the map. In our CafeMapFilter.vue component, add the following styles:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div#cafe-map-filter{
    background-color: white;
    border-radius: 10px;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
    padding: 5px;
    z-index: 999999;
    position: absolute;
    right: 45px;
    top: 50px;
    width: 25%;
  }
</style>

Our filter component will now appear nicely over the map! We will move pretty quick through adding the filter templates and component functionality since it’s very similar to: https://serversideup.net/filtering-vuejs-mixins/. The functionality of displaying filtered markers on the map we will explain in full detail.

Step 2: Add Text Filter to our Component

Now we can begin to add the filters to our CafeMapFilter.vue component.

First, let’s add our text filter. This will process a text search for cafes on the map. Add the following code to the template:

<div class="grid-container">
  <div class="grid-x grid-padding-x">
    <div class="large-12 medium-12 small-12 cell">
      <label>Search</label>
      <input type="text" v-model="textSearch" placeholder="Search"/>
    </div>
  </div>
</div>

We added a little bit of grid action to the component as well, but then threw in our input for the text search. Let’s build a data model in our component to house the text search data:

data(){
  return {
    textSearch: ''
  }
}

We now have our model built for our text search.

Step 3: Add Is Roaster Filter

Let’s quickly add our “is roaster” filter to our component to filter out cafes that have roasters. Add the following code to the template below our search box:

<div class="is-roaster-container">
  <input type="checkbox" v-model="isRoaster"/> <label>Is Roaster?</label>
</div>

Then add the isRoaster boolean to the data():

data(){
  return {
    textSearch: '',
    isRoaster: false,
  }
}

Step 4: Add Brew Methods Filters

Now we need to add our brew methods to the filter box.

First, we should include our brew methods as a computed variable from the Vuex store:

/*
  Loads the Vuex data we need such as brew methods
*/
computed: {
  cafeBrewMethods(){
    return this.$store.getters.getBrewMethods;
  },
},

Now we need to add the data to house the selected brew methods:

data(){
  return {
    textSearch: '',
    isRoaster: false,
    brewMethods: []
  }
},

Next, we need to implement the selectors in the template:

<div class="brew-methods-container">
  <div class="filter-brew-method" v-on:click="toggleBrewMethodFilter( method.method )" v-bind:class="{'active' : brewMethods.indexOf( method.method ) > -1 }" v-for="method in cafeBrewMethods">
    {{ method.method }}
  </div>
</div>

The functionality on this is the exact same as it is in the previous article so for a more in depth explanation check out: https://serversideup.net/filtering-vuejs-mixins/.

Let’s add the method that does the toggling of the brew method filter:

methods: {
  toggleBrewMethodFilter( method ){
    if( this.brewMethods.indexOf( method ) > -1 ){
      this.brewMethods.splice( this.brewMethods.indexOf( method ), 1 );
    }else{
      this.brewMethods.push( method );
    }
  },
}

I also tweaked the styles so the buttons are a little bit smaller so they don’t take up as much space in the filter container:

div.filter-brew-method{
  display: inline-block;
  height: 30px;
  text-align: center;
  border: 1px solid #ededed;
  border-radius: 5px;
  padding-left: 10px;
  padding-right: 10px;
  padding-top: 5px;
  padding-bottom: 5px;
  margin-right: 10px;
  margin-top: 10px;
  cursor: pointer;
  color: #7F5F2A;
  font-family: 'Josefin Sans', sans-serif;
  font-size: 12px;

  &.active{
    border-bottom: 4px solid $primary-color;
  }
}

Since we are focusing more on the location based data and a few filters I’m going to leave out the tags filter. You can add it similarly to what we did in the previous article: https://serversideup.net/filtering-vuejs-mixins/. I’m just leaving it out due to space for the little filter component.

Let’s implement some of the functionality!

Step 5: Finish Cafe Map Filter Functionality

We have our visual display set up for the filtering, so let’s finish our filtering functionality and passing of events so we can listen in our CafeMap.vue component.

First, let’s include our Event Bus on top of our filter component like so:

/*
  Imports the Event Bus to pass updates.
*/
import { EventBus } from '../../event-bus.js';

This will allow us to pass events containing filter changes.

Next, let’s add our watchers. These will watch our component data and send out an event bus events when the user changes the filter:

watch: {
  textSearch(){
    this.updateFilterDisplay();
  },

  isRoaster(){
    this.updateFilterDisplay();
  },

  brewMethods(){
    this.updateFilterDisplay();
  }
},

Now, let’s implement our updateFilterDisplay() method.

The method should look like:

updateFilterDisplay(){
  EventBus.$emit('filters-updated', {
    text: this.textSearch,
    roaster: this.isRoaster,
    brew_methods: this.brewMethods
  });
}

Whenever one of our filters has been updated, the Event Bus will emit a filter-updated event that can be handled by another component to display the data to the user. We will listen for this event on the CafeMap.vue component and use our mixins to determine if we should show the cafe or not on the map.

Our CafeMapFilter.vue method should look like:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div#cafe-map-filter{
    background-color: white;
    border-radius: 10px;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
    padding: 5px;
    z-index: 999999;
    position: absolute;
    right: 45px;
    top: 50px;
    width: 25%;

    div.filter-brew-method{
      display: inline-block;
      height: 30px;
      text-align: center;
      border: 1px solid #ededed;
      border-radius: 5px;
      padding-left: 10px;
      padding-right: 10px;
      padding-top: 5px;
      padding-bottom: 5px;
      margin-right: 10px;
      margin-top: 10px;
      cursor: pointer;
      color: #7F5F2A;
      font-family: 'Josefin Sans', sans-serif;
      font-size: 12px;

      &.active{
        border-bottom: 4px solid $primary-color;
      }
    }
  }
</style>

<template>
  <div id="cafe-map-filter">
    <div class="grid-container">
      <div class="grid-x grid-padding-x">
        <div class="large-12 medium-12 small-12 cell">
          <label>Search</label>
          <input type="text" v-model="textSearch" placeholder="Search"/>

          <div class="is-roaster-container">
            <input type="checkbox" v-model="isRoaster"/> <label>Is Roaster?</label>
          </div>

          <div class="brew-methods-container">
            <div class="filter-brew-method" v-on:click="toggleBrewMethodFilter( method.method )" v-bind:class="{'active' : brewMethods.indexOf( method.method ) > -1 }" v-for="method in cafeBrewMethods">
              {{ method.method }}
            </div>
          </div>

        </div>
      </div>
    </div>
  </div>
</template>

<script>
  /*
    Imports the Event Bus to pass updates.
  */
  import { EventBus } from '../../event-bus.js';

  export default {
    data(){
      return {
        textSearch: '',
        isRoaster: false,
        brewMethods: []
      }
    },

    /*
      Loads the Vuex data we need such as brew methods
    */
    computed: {
      cafeBrewMethods(){
        return this.$store.getters.getBrewMethods;
      },
    },

    watch: {
      textSearch(){
        this.updateFilterDisplay();
      },

      isRoaster(){
        this.updateFilterDisplay();
      },

      brewMethods(){
        this.updateFilterDisplay();
      }
    },

    methods: {
      toggleBrewMethodFilter( method ){
        if( this.brewMethods.indexOf( method ) > -1 ){
          this.brewMethods.splice( this.brewMethods.indexOf( method ), 1 );
        }else{
          this.brewMethods.push( method );
        }
      },

      updateFilterDisplay(){
        EventBus.$emit('filters-updated', {
          text: this.textSearch,
          roaster: this.isRoaster,
          brew_methods: this.brewMethods
        });
      }
    }
  }
</script>

Step 6: Listen for Filter Updates in CafeMap.vue

Let’s first open up our /resources/assets/js/components/cafes/CafeMap.vue component.

We will need to import our Event Bus so we can listen to the filtering events. Add the following code to the top of the script for the component:

/*
  Imports the Event Bus to pass updates.
*/
import { EventBus } from '../../event-bus.js';

This will allow us to hook into the filter-updated event and show/hide cafes on the map accordingly. To do this, add the following code to your component in the mounted lifecycle hook:

mounted(){
  /*
    We don't want the map to be reactive, so we initialize it locally,
    but don't store it in our data array.
  */
  this.map = new google.maps.Map(document.getElementById('cafe-map'), {
    center: {lat: this.latitude, lng: this.longitude},
    zoom: this.zoom
  });

  /*
    Clear and re-build the markers
  */
  this.clearMarkers();
  this.buildMarkers();

  /*
    Listen to the filters-updated event to filter the map markers
  */
  EventBus.$on('filters-updated', function( filters ){
    this.processFilters( filters );
  }.bind(this));
},

We already have a little code to set up the map in the mounted hook, so we just need to add it to the end. Now when the filters-updated event is passed, we run the processFilters() method and pass the filters we got from the event.

This method will be a little different than the last implementation since the cafes are on the google map.

Step 7: Implement processFilters() Method

This method will show and hide markers on the map depending on the filter selection by the user.

First, let’s include our handy mixins at the top of the component. These will handle the processing if the map matches filters.

import { CafeIsRoasterFilter } from '../../mixins/filters/CafeIsRoasterFilter.js';
import { CafeBrewMethodsFilter } from '../../mixins/filters/CafeBrewMethodsFilter.js';
import { CafeTagsFilter } from '../../mixins/filters/CafeTagsFilter.js';
import { CafeTextFilter } from '../../mixins/filters/CafeTextFilter.js';

We just need to add this to our mixins array in our component:

mixins: [
  CafeIsRoasterFilter,
  CafeBrewMethodsFilter,
  CafeTagsFilter,
  CafeTextFilter
],

Now we have access to these mixins methods in our component.

Next, let’s add the method to our methods object:

/*
  Process filters on the map selected by the user.
*/
processFilters( filters ){

},

Before we do any more to our processFilters() method, let’s make a single change to our buildMarkers() method. When we create a marker, let’s add the cafe as an object to the marker. This way we can access the cafe when we need it and we can set the map to the google map or to null to hide the marker if we filter the cafe. This will make for a smoother UX since we don’t have to clear all of the markers every time there’s a change. Our buildMarkers() method should look like:

/*
  Builds all of the markers for the cafes
*/
buildMarkers(){
  /*
    Initialize the markers to an empty array.
  */
  this.markers = [];

  /*
    Iterate over all of the cafes
  */
  for( var i = 0; i < this.cafes.length; i++ ){

    /*
      Create the marker for each of the cafes and set the
      latitude and longitude to the latitude and longitude
      of the cafe. Also set the map to be the local map.
    */
    var image = '/img/coffee-marker.png';

    var marker = new google.maps.Marker({
      position: { lat: parseFloat( this.cafes[i].latitude ), lng: parseFloat( this.cafes[i].longitude ) },
      map: this.map,
      icon: image,
      cafe: this.cafes[i]
    });

    /*
      Create the info window and add it to the local
      array.
    */
    let infoWindow = new google.maps.InfoWindow({
      content: this.cafes[i].name
    });

    this.infoWindows.push( infoWindow );

    /*
      Add the event listener to open the info window for the marker.
    */
    marker.addListener('click', function() {
      infoWindow.open(this.map, this);
    });

    /*
      Push the new marker on to the array.
    */
    this.markers.push( marker );
  }
}

Now we have access to the cafe to pass to our mixing in our processFilters() method. That’s a lot to think about, so let’s begin to break it down. Back to the processFilters() method.

The goal of the process filters method on this map is to either show a marker or hide a marker. First, let’s start iterating over the array of markers:

/*
  Process filters on the map selected by the user.
*/
processFilters( filters ){
  for( var i = 0; i < this.markers.length; i++ ){

  }
},

Then we check to see if there are any filters selected by the user. If there aren’t we disregard the filter data.

/*
  Process filters on the map selected by the user.
*/
processFilters( filters ){
  for( var i = 0; i < this.markers.length; i++ ){
    if( filters.text == '' 
        && filters.roaster == false 
        && filters.brew_methods.length == 0 ){
          this.markers[i].setMap( this.map );
        }else{

        }
  }
},

What this does, is set the marker’s map to the Google Map if no filters are selected.

Now we will implement our actual filtering. In the }else{ section
of the processFilters() method, we need to begin by first initializing the flags:

/*
  Initialize flags for the filtering
*/
var textPassed = false;
var brewMethodsPassed = false;
var roasterPassed = false;

These will keep track of what has passed filters for the sake of displaying on the map.

Next, we will determine if the cafe represented by the marker passes the isRoaster filter:

/*
  Check if the roaster passes
*/
if( filters.roaster && this.processCafeIsRoasterFilter( this.markers[i].cafe ) ){
  roasterPassed = true;
}else if( !filters.roaster ){
  roasterPassed = true;
}

One thing to point out is we are referencing the cafe attached to the marker we are at when we are iterating. This is very similar on how we did it before, but this time we are looking at the marker and the attached cafe instead of the cafe on the CafeCard.vue. Notice how the mixins provide useful re-usable functionality? This is the power of a mixin. We can use it in multiple places and don’t have to duplicate functionality.

Let’s now implement our text filter:

/*
  Check if text passes
*/
if( filters.text != '' && this.processCafeTextFilter( this.markers[i].cafe, filters.text ) ){
  textPassed = true;
}else if( filters.text == '' ){
  textPassed = true;
}

Same as the roaster filter, we are using the cafe attached to the marker. This is why we added the cafe in our buildMarkers() method.

Finally, let’s add the brewMethodsFilter:

/*
  Check if brew methods passes
*/
if( filters.brew_methods.length != 0 && this.processCafeBrewMethodsFilter( this.markers[i].cafe, filters.brew_methods ) ){
  brewMethodsPassed = true;
}else if( filters.brew_methods.length == 0 ){
  brewMethodsPassed = true;
}

Now it’s time to determine if the marker should display or not. Like in the previous tutorial (https://serversideup.net/filtering-vuejs-mixins/) we have an if statement that validates if the filter passes. This time, we set the map of the marker to the Google Map we are using or setting the marker’s map to null which will hide the marker. It’s convenient to have this array of markers since we can iterate over all of them and transform what we need to match the display. Our filter determination statement should look like:

/*
  If everything passes, then we show the Cafe Marker
*/
if( roasterPassed && textPassed && brewMethodsPassed){
  this.markers[i].setMap( this.map );
}else{
  this.markers[i].setMap( null );
}

If everything passes, we show the marker on the map otherwise we hide the map.

Our final processFilter() method should look like:

/*
  Process filters on the map selected by the user.
*/
processFilters( filters ){
  for( var i = 0; i < this.markers.length; i++ ){
    if( filters.text == ''
        && filters.roaster == false
        && filters.brew_methods.length == 0 ){
          this.markers[i].setMap( this.map );
        }else{
          /*
            Initialize flags for the filtering
          */
          var textPassed = false;
          var brewMethodsPassed = false;
          var roasterPassed = false;


          /*
            Check if the roaster passes
          */
          if( filters.roaster && this.processCafeIsRoasterFilter( this.markers[i].cafe ) ){
            roasterPassed = true;
          }else if( !filters.roaster ){
            roasterPassed = true;
          }

          /*
            Check if text passes
          */
          if( filters.text != '' && this.processCafeTextFilter( this.markers[i].cafe, filters.text ) ){
            textPassed = true;
          }else if( filters.text == '' ){
            textPassed = true;
          }

          /*
            Check if brew methods passes
          */
          if( filters.brew_methods.length != 0 && this.processCafeBrewMethodsFilter( this.markers[i].cafe, filters.brew_methods ) ){
            brewMethodsPassed = true;
          }else if( filters.brew_methods.length == 0 ){
            brewMethodsPassed = true;
          }

          /*
            If everything passes, then we show the Cafe Marker
          */
          if( roasterPassed && textPassed && brewMethodsPassed){
            this.markers[i].setMap( this.map );
          }else{
            this.markers[i].setMap( null );
          }
        }
  }

We now can show and hide markers on our map based on the filters we select!

Conclusion

This tutorial re-inforces the power of the VueJS mixin. We utilize the same function in this tutorial as we did the last tutorial to provide a proper filter functionality. While using a Google map, it’s interesting to see where the cafes are located and provides a powerful visualization to the users. Especially when the user can filter what they want and find cafes in their area. There’s a lot more filters we can add such as search radius and other API based filters which we will touch on later.

The other cool thing is the simplicity of showing and hiding markers on Google Maps when you have an array to work with. It’s a really slick processes of just setting the map.

Like always, leave questions, comments, etc. below and be sure to check out the source code at:
GitHub – serversideup/roastandbrew: Helping the coffee enthusiast find their next cup of coffee. Also, helping aspiring web and mobile app developers build a single page app and converting it to a mobile hybrid. All tutorials can be found at https://serversideup.net

We are also writing a book about API Driven Development. Make sure to sign up here: Server Side Up General List.

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Customize Google Map Info Windows

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.