Categories
JavaScript Vue

How to Add Geolocation to a Vue.js App

Spread the love

Many apps want provide an experience based on the user’s location. This is where the HTML Geolocation API comes in. You can use it easily to get the location of the current device.

To get the location of the device in the browser using plain JavaScript, we write:

if (navigator.geolocation) {  
  navigator.geolocation.getCurrentPosition(getPosition);  
}

function getPosition(position) {  
  console.log(position.coords.latitude, position.coords.longitude);  
}

As you can see, getting the latitude and longitude is very easy.

We can also easily add geolocation to any Vue.js app. The vue-browser-geolocation package is a great add-on for adding geolocation capabilities to your app. It provides a promise-based API for getting the location of the device, so we can easily use async and await to get the location with this package.

Getting Started

In this article, we will build an app to get the list of Canadian politicians in your local area if you are in Canada from the Represent Civic Information API.

We will use the vue-browser-geolocation package to get the location, and then use the API to get the list of items from the API. We will get the list of politicians from the local area, the constituent boundaries from the local area, and the list of representatives from the given legislative body selected from a list.

To start, we run the Vue CLI to create the project. Run npx @vue/cli create politicians-app to create the app. In the wizard, we choose ‘Manually select features’, then choose to include Babel, and Vue Router.

Next we install some packages we need to build the app. We will use Axios for making HTTP requests, BootstrapVue for styling, Vue Avatar for showing the avatar picture of the politician, and vue-browser-geolocation to get the location of the device. Run npm i axios bootstrap-vue vue-avatar vue-browser-geolocation to install all the packages.

Making API Requests

Now we can start writing the app. We start by creating a mixin for making HTTP requests that we use in our components. Create a mixins folder in the src folder and create requestsMixin.js in the mixins folder. Then we add the following to the file:

const axios = require("axios");  
const APIURL = "https://represent.opennorth.ca";

export const requestsMixin = {  
  methods: {  
    getRepresentatives(lat, lng) {  
      return axios.get(`${APIURL}/representatives/?point=${lat},${lng}`);  
    }, 

    getBoundaries(lat, lng) {  
      return axios.get(`${APIURL}/boundaries/?contains=${lat},${lng}`);  
    }, 

    getRepresentativeSetsRepresentatives(set, lat, lng) {  
      return axios.get(  
        `${APIURL}/representatives/${set}/?point=${lat},${lng}`  
      );  
    }, 

    getRepresentativeSets() {  
      return axios.get(`${APIURL}/representative-sets/?offset=0&limit=200`);  
    }  
  }  
};

We use the Represent Civic Information API to get the representatives and boundaries by location, and also get the list of legislative bodies in Canada. The full list of endpoints is at https://represent.opennorth.ca/api/#representativeset.

Getting Geolocation

Next create the pages for display the data. In the views folder, create Boundaries.vue and add:

<template>  
  <div class="page">  
    <h1 class="text-center">Your Constituent Boundary</h1>  
    <template v-if="noLocation">  
      <h2 class="text-center">Enable geolocation to see list of boundaries</h2>  
    </template>  
    <template v-if="!noLocation">  
      <b-card v-for="(b, i) in boundaries" :key="i" class="card">  
        <b-card-title>  
          <h2>{{b.name}}</h2>  
        </b-card-title> <b-card-text>  
          <p>  
            <b>Boundary Set Name:</b>  
            {{b.boundary_set_name}}  
          </p>  
        </b-card-text>  
      </b-card>  
    </template>  
  </div>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";

export default {  
  name: "boundaries",  
  mixins: [requestsMixin],  
  data() {  
    return {  
      boundaries: [],  
      noLocation: true  
    };  
  },  
  beforeMount() {  
    this.getBounds();  
  },  
  methods: {  
    async getBounds() {  
      try {  
        const coordinates = await this.$getLocation({  
          enableHighAccuracy: true  
        });  
        const { lat, lng } = coordinates;  
        const response = await this.getBoundaries(lat, lng);  
        this.boundaries = response.data.objects;  
        this.noLocation = false;  
      } catch (error) {  
        this.noLocation = true;  
      }  
    }  
  }  
};  
</script>

In this page, we get the constituency boundaries for the location that the user’s device is currently in with the vue-browser-geolocation package. this.$getLocation in the getBounds function is provided by the package. The promise resolves to an object with the latitude and longitude when geolocation is enabled by the user. We wrap the code with a try...catch in case that it is disabled. If geolocation is disabled, we show a message to let the user know that they have to enable geolocation to see data.

If geolocation is enabled, then this.getBoundaries will be called. The function is provided by the requestsMixin that we created earlier. We included it with the mixin property of this component. The items are displayed in BootstrapVue cards.

Next we replace the code in Home.vue with the following:

<template>  
  <div class="page">  
    <h1 class="text-center">Your Local Representatives</h1>  
    <template v-if="noLocation">  
      <h2 class="text-center">Enable geolocation to see list of representatives</h2>  
    </template>  
    <template v-if="!noLocation">  
      <b-card v-for="(r, i) in reps" :key="i" class="card">  
        <b-card-title>  
          <h2>  
            <avatar :src="r.photo_url" :inline="true"></avatar>  
            <span class="title">{{r.name}}</span>  
          </h2>  
        </b-card-title> <b-card-text>  
          <h5>Offices:</h5>  
          <div v-for="(o,i) in r.offices" :key="i">  
            <p>  
              <b>Address:</b>  
              {{o.postal}}  
            </p>  
            <p>  
              <b>Telephone:</b>  
              {{o.tel}}  
            </p>  
            <p>  
              <b>Type:</b>  
              {{o.type}}  
            </p>  
          </div>  
          <p>  
            <b>Party:</b>  
            {{r.party_name}}  
          </p>  
        </b-card-text> <b-button :href="r.url" variant="primary" target="_blank">Go to Source</b-button>  
      </b-card>  
    </template>  
  </div>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";  
import Avatar from "vue-avatar";

export default {  
  name: "home",  
  mixins: [requestsMixin],  
  data() {  
    return {  
      reps: [],  
      noLocation: true  
    };  
  },  
  components: {  
    Avatar  
  },

  beforeMount() {  
    this.getAllRepresentatives();  
  },  
  methods: {  
    async getAllRepresentatives() {  
      try {  
        const coordinates = await this.$getLocation({  
          enableHighAccuracy: true  
        });  
        const { lat, lng } = coordinates;  
        const response = await this.getRepresentatives(lat, lng);  
        this.reps = response.data.objects;  
        this.noLocation = false;  
      } catch (error) {  
        this.noLocation = true;  
      }  
    }  
  }  
};  
</script>

We use the same this.$geoLocation function as Boundaries.vue. In this page, we get the list of representatives for the location that the user’s device is currently in. The geolocation feature is the same as Boundaries.vue. We run the getAllRepresentatives function in beforeMount so that it loads when the page loads.

The items from the API are displayed in the BootstrapVue cards. We display the picture in an avatar with the vue-avatar package.

Next we create Representatives.vue in the views folder. We add the following code to the file:

<template>  
  <div class="page">  
    <h1 class="text-center">Representative Sets</h1>  
    <template v-if="noLocation">  
      <h2 class="text-center">Enable geolocation to see list of representatives</h2>  
    </template>  
    <template v-if="!noLocation">  
      <b-form>  
        <b-form-group label="Representative Set" label-for="repSet">  
          <b-form-select  
            name="repSet"  
            v-model="form.repSet"  
            :options="repSets"  
            required  
            @change="getRepSetReps()"  
          ></b-form-select>  
        </b-form-group>  
      </b-form>  
      <b-card v-for="(r, i) in reps" :key="i" class="card">  
        <b-card-title>  
          <h2>  
            <avatar :src="r.photo_url" :inline="true"></avatar>  
            <span class="title">{{r.name}}</span>  
          </h2>  
        </b-card-title> <b-card-text>  
          <h5>Info:</h5>  
          <p>  
            <b>Elected Office:</b>  
            {{r.office}}  
          </p>  
          <p>  
            <b>District Name:</b>  
            {{r.district_name}}  
          </p>  
          <p>  
            <b>Party:</b>  
            {{r.party_name || 'None'}}  
          </p>  
          <h5>Offices:</h5>  
          <div v-for="(o,i) in r.offices" :key="i">  
            <p>  
              <b>Address:</b>  
              {{o.postal}}  
            </p>  
            <p>  
              <b>Telephone:</b>  
              {{o.tel}}  
            </p>  
            <p>  
              <b>Type:</b>  
              {{o.type}}  
            </p>  
          </div>  
        </b-card-text>  
      </b-card>  
    </template>  
  </div>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";  
import Avatar from "vue-avatar";

export default {  
  name: "boundaries",  
  mixins: [requestsMixin],  
  data() {  
    return {  
      repSets: [],  
      reps: [],  
      form: {  
        repSet: "strathcona-county-council"  
      },  
      noLocation: true,  
      coordinates: {}  
    };  
  },  
  components: {  
    Avatar  
  },  
  beforeMount() {  
    this.getRepSets();  
    this.getLocation();  
  },  
  methods: {  
    async getRepSets() {  
      const response = await this.getRepresentativeSets();  
      this.repSets = response.data.objects.map(s => {  
        const [part1, part2, value] = s.url.split("/");  
        return {  
          text: s.name,  
          value  
        };  
      });  
    }, 

    async getLocation() {  
      try {  
        const coordinates = await this.$getLocation({  
          enableHighAccuracy: true  
        });  
        this.coordinates = coordinates;  
        this.noLocation = false;  
      } catch (error) {  
        this.noLocation = true;  
      }  
    }, 

    async getRepSetReps() {  
      const { lat, lng } = this.coordinates;  
      const response = await this.getRepresentativeSetsRepresentatives(  
        this.form.repSet,  
        lat,  
        lng  
      );  
      this.reps = response.data.objects;  
    }  
  }  
};  
</script>

When the page loads, we call this.getRepSets and this.getLocation to populate the dropdown with the legislative bodies in Canada and get the location respectively. We get the items in the this.getRepSetReps function with the coordinates provided when the dropdown selection is changed and when geolocation API enabled. We only display the dropdown when geolocation is enabled so that we won’t let the user select anything without it being enabled.

The items are also displayed in BootstrapVue cards in this page.

Then we change the existing code in App.vue to:

<template>  
  <div>  
    <b-navbar toggleable="lg" type="dark" variant="info">  
      <b-navbar-brand href="#">Canadian Politicians App</b-navbar-brand><b-navbar-toggle target="nav-collapse"></b-navbar-toggle><b-collapse id="nav-collapse" is-nav>  
        <b-navbar-nav>  
          <b-nav-item to="/" :active="path == '/'">Home</b-nav-item>  
          <b-nav-item to="/boundaries" :active="path == '/boundaries'">Boundaries</b-nav-item>  
          <b-nav-item to="/representatives" :active="path == '/representatives'">Representatives</b-nav-item>  
        </b-navbar-nav>  
      </b-collapse>  
    </b-navbar>  
    <router-view />  
  </div>  
</template><script>  
export default {  
  data() {  
    return {  
      path: this.$route && this.$route.path  
    };  
  },  
  watch: {  
    $route(route) {  
      this.path = route.path;  
    }  
  }  
};  
</script>

<style>  
.page {  
  padding: 20px;  
  margin: 0 auto;  
}

.card,  
form {  
  max-width: 800px;  
  margin: 0 auto;  
}

.title {  
  position: relative;  
  top: -13px;  
  left: 10px;  
}  
</style>

We add the BootstrapVue toolbar and the links to each page. In the top bar, we set the active prop for the links so that we highlight the link of the current page that is displayed. In the scripts section, we watch the $route object provided by Vue Router for the current path of the app and assign it to this.path so that we can use it to set the active prop.

In the style block, we add padding and margin to the pages with the page class. We set the form and card widths so that they won’t be too wide, and we add the title class so the titles in the cards align with the avatars.

In main.js, we replace the existing code with:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import VueGeolocation from 'vue-browser-geolocation';  
import BootstrapVue from 'bootstrap-vue'  
import 'bootstrap/dist/css/bootstrap.css'  
import 'bootstrap-vue/dist/bootstrap-vue.css'Vue.use(BootstrapVue)  
Vue.use(VueGeolocation);Vue.config.productionTip = falsenew Vue({  
  router,  
  store,  
  render: h => h(App)  
}).$mount('#app')

This adds BootstrapVue library and styles to our app, along with the vue-browser-geolocation package.

In router.js we replace the existing code with:

import Vue from "vue";  
import Router from "vue-router";  
import Home from "./views/Home.vue";  
import Boundaries from "./views/Boundaries.vue";  
import Representatives from "./views/Representatives.vue";

Vue.use(Router);export default new Router({  
  mode: "history",  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: "/",  
      name: "home",  
      component: Home  
    },  
    {  
      path: "/boundaries",  
      name: "boundaries",  
      component: Boundaries  
    },  
    {  
      path: "/representatives",  
      name: "representatives",  
      component: Representatives  
    }  
  ]  
});

Now users can go the packages we linked to in the top bar, and also by entering the URLs directly.

Now we run the app by running npm run serve .

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

One reply on “How to Add Geolocation to a Vue.js App”

Great article. I think you should use v-if to render the vue-avatar component only if r.photo_url is not null to prevent the console errors

Leave a Reply

Your email address will not be published. Required fields are marked *