How to Add Geolocation to a Vue.js App
Tags: webdev, javascript, tutorial, vue
Published: 2020-01-12 22:55:33

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

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:

```javascript
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](https://www.npmjs.com/package/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](https://represent.opennorth.ca/).

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:

```javascript
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](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:

```javascript
<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:

```javascript
<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:

```javascript
<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:

```javascript
<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:

```javascript
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:

```javascript
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` .