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
.
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 ifr.photo_url
is not null to prevent the console errors