Create an Interactive Location Selector With Vue.js and Leaflet
Interactive Location Selector comes in handy if we want an alternative to address text fields.
Eyuel Berga Woldemichael
Interactive maps are good for displaying geographic information. But we can also use them to get location data from users. This comes in handy if we are building location-based applications and we want an alternative to address text fields.
Here is a demo of what we will be building in this article:

Our Location selector will be able to do the following:
- Initially, it starts at the user's current location, if location tracking is allowed and supported by the browser. If not, it will be centered at a default location.
- Double tapping(clicking) will place a marker on that spot and that will be the selected location.
- We can update the selected location by dragging and placing the marker in a new location.
- We’ll also be able to search for a location using the Geosearch tool on the top left of the map.
Okay! Let's start by initializing a new Vue.js application using the Vue CLI.
vue create location-selector
We also need to add Leaflet to our project. We add the main Leaflet library along with vue2-leaflet, which will make it much easier to work with Leaflet on our Vue application.
yarn add leaflet vue2-leaflet
We’ll also add the vue2-leaflet-geosearch plugin, to enable search
yarn add vue2-leaflet-geosearch leaflet-geosearch
When that is done, start the application on development mode
yarn serve
Before we start working on the selector, we need to take care of something. vue2-leaflet and leaflet-geosearch require their own CSS styles to display properly on the browser, so we need to add them. Go to main.js and add the following lines of code:
import "leaflet/dist/leaflet.css";
import "leaflet-geosearch/dist/geosearch.css";
Now, create a new file named LocationSelectorMap.vue in the components folder. We need to import some components from vue2-leaflet and add them to our template.
<template>
<l-map ref="map">
<l-tile-layer
:url="tileProvider.url"
:attribution="tileProvider.attribution"
/>
</l-map>
</template>
<script>
import { LMap, LTileLayer} from "vue2-leaflet";
export default {
components: {
LMap,
LTileLayer
},
data() {
return {
tileProvider: {
attribution:
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
}
}
}
};
</script>
You should now see something like this

The map starts off at a distance, but we want it to zoom in on our current location. Let's fix that.
We need to give it an initial zoom value and locate the user’s current location. To get the user’s location, we’ll use the Geolocation API. Because the API requires user permission and might not be supported by the browser, we need a fallback location. If we fail to get the location, we will default to our fallback.
<template>
<l-map
ref="map"
:zoom="zoom"
:center="[
userLocation.lat || defaultLocation.lat,
userLocation.lng || defaultLocation.lng
]"
>
<l-tile-layer
:url="tileProvider.url"
:attribution="tileProvider.attribution"
/>
</l-map>
</template>
<script>
import { LMap, LTileLayer} from "vue2-leaflet";
import { icon } from "leaflet";
export default {
components: {LMap, LTileLayer},
props: {
defaultLocation: {
type: Object,
default: () => ({
lat: 8.9806,
lng: 38.7578
})
}
},
data() {
return {
userLocation: {},
tileProvider: {
attribution:
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
},
zoom: 18
};
},
mounted() {
this.getUserPosition();
}
methods: {
async getUserPosition() {
// check if API is supported
if (navigator.geolocation) {
// get geolocation
navigator.geolocation.getCurrentPosition(pos => {
// set user location
this.userLocation = {
lat: pos.coords.latitude,
lng: pos.coords.longitude
};
});
}
}
}
};
</script>
The getUserPosition method calls the Geolocation API and sets the user’s coordinates to the userLocation variable. We also have a defaultLocation prop which is our fallback. We use both to set the map’s center value.
Now our map looks much better.

Next, we need to be able to select a location on the map and place a marker on it. To do that we import the LMarker and LTooltip components from vue2-leaflet and add them to our map. We also need to handle dblclick events on our map, so we can place a marker there.
<template>
<l-map
ref="map"
@dblclick="onMapClick"
:zoom="zoom"
:center="[
position.lat || userLocation.lat || defaultLocation.lat,
position.lng || userLocation.lng || defaultLocation.lng
]"
>
<l-tile-layer
:url="tileProvider.url"
:attribution="tileProvider.attribution"
/>
<l-marker
v-if="position.lat && position.lng"
visible
draggable
:icon="icon"
:lat-lng.sync="position"
@dragstart="dragging = true"
@dragend="dragging = false"
>
<l-tooltip :content="tooltipContent" :options="{ permanent: true }" />
</l-marker>
</l-map>
</template>
<script>
import { LMap,
LMarker,
LTileLayer,
LTooltip
} from "vue2-leaflet";
import { icon } from "leaflet";
export default {
components: {
LMap,
LTileLayer,
LMarker,
LTooltip,
},
props: {
value: {
type: Object,
required: true
},
defaultLocation: {
type: Object,
default: () => ({
lat: 8.9806,
lng: 38.7578
})
}
},
data() {
return {
loading:false,
userLocation: {},
icon: icon({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
iconUrl: require("leaflet/dist/images/marker-icon.png"),
shadowUrl: require("leaflet/dist/images/marker-shadow.png")
}),
position: {},
address:"",
tileProvider: {
attribution:
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
},
zoom: 18,
dragging:false
};
},
mounted() {
this.getUserPosition();
},
watch: {
position: {
deep: true,
async handler(value) {
this.address = await this.getAddress();
this.$emit("input", { position: value, address: this.address });
}
}
},
computed: {
tooltipContent() {
if (this.dragging) return "...";
if (this.loading) return "Loading...";
return `<strong>${this.address.replace(
",",
"/\n/g"
)}</strong> <hr/><strong>lat:</strong> ${
this.position.lat
}/\n/g <strong>lng:</strong> ${this.position.lng}`;
}
},
methods: {
async getAddress() {
this.loading = true;
let address = "Unresolved address";
try {
const { lat, lng } = this.position;
const result = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`
);
if (result.status === 200) {
const body = await result.json();
address = body.display_name;
}
} catch (e) {
console.error("Reverse Geocode Error->", e);
}
this.loading = false;
return address;
},
onMapClick(value) {
// place the marker on the clicked spot
this.position = value.latlng;
},
getUserPosition() {
if (navigator.geolocation) {
// get GPS position
navigator.geolocation.getCurrentPosition(pos => {
// set the user location
this.userLocation = {
lat: pos.coords.latitude,
lng: pos.coords.longitude
};
});
}
}
}
};
</script>
We use the sync modifier on the lat-lng property of the marker, which syncs our position variable with the current position of the marker.
A tooltip will display information on our current location. It gets the content from the tooltipContentcomputed property.
onMapClick method handles dblclick events on the map to set the marker position to the clicked spot.
getAddress method is for reverse geocoding. It returns a human-readable address from the marker’s point location. We use fetch to call the Nominatim API and get the location’s display name. Because the process is asynchronous, it’s an async function.
We also watch changes to position. When the value changes, we call the getAddress method and then emit an object with the new position and address name.
We should now be able to place a marker on our map.

The only thing we are missing now is the Geosearch feature. Import the component from vue2-leaflet-geosearch and place it on the map with a search provider.
<template>
<l-map
ref="map"
@dblclick="onMapClick"
:zoom="zoom"
:center="[
position.lat || userLocation.lat || defaultLocation.lat,
position.lng || userLocation.lng || defaultLocation.lng
]"
>
<l-tile-layer
:url="tileProvider.url"
:attribution="tileProvider.attribution"
/>
<l-geosearch :options="geoSearchOptions"></l-geosearch>
<l-marker
v-if="position.lat && position.lng"
visible
draggable
:icon="icon"
:lat-lng.sync="position"
@dragstart="dragging = true"
@dragend="dragging = false"
>
<l-tooltip :content="tooltipContent" :options="{ permanent: true }" />
</l-marker>
</l-map>
</template>
<script>
import { LMap, LMarker, LTileLayer, LTooltip } from "vue2-leaflet";
import { OpenStreetMapProvider } from "leaflet-geosearch";
import LGeosearch from "vue2-leaflet-geosearch";
import { icon } from "leaflet";
export default {
components: {
LMap,
LTileLayer,
LMarker,
LTooltip,
LGeosearch
},
props: {
value: {
type: Object,
required: true
},
defaultLocation: {
type: Object,
default: () => ({
lat: 8.9806,
lng: 38.7578
})
}
},
data() {
return {
loading: false,
geoSearchOptions: {
provider: new OpenStreetMapProvider(),
showMarker: false,
autoClose: true
},
userLocation: {},
icon: icon({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
iconUrl: require("leaflet/dist/images/marker-icon.png"),
shadowUrl: require("leaflet/dist/images/marker-shadow.png")
}),
position: {},
address: "",
tileProvider: {
attribution:
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
},
zoom: 18,
dragging: false
};
},
mounted() {
this.getUserPosition();
this.$refs.map.mapObject.on("geosearch/showlocation", this.onSearch);
},
watch: {
position: {
deep: true,
async handler(value) {
this.address = await this.getAddress();
this.$emit("input", { position: value, address: this.address });
}
}
},
computed: {
tooltipContent() {
if (this.dragging) return "...";
if (this.loading) return "Loading...";
return `<strong>${this.address.replace(
",",
"/\n/g"
)}</strong> <hr/><strong>lat:</strong> ${
this.position.lat
}/\n/g <strong>lng:</strong> ${this.position.lng}`;
}
},
methods: {
async getAddress() {
this.loading = true;
let address = "Unresolved address";
try {
const { lat, lng } = this.position;
const result = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`
);
if (result.status === 200) {
const body = await result.json();
address = body.display_name;
}
} catch (e) {
console.error("Reverse Geocode Error->", e);
}
this.loading = false;
return address;
},
async onMapClick(value) {
// place the marker on the clicked spot
this.position = value.latlng;
},
onSearch(value) {
const loc = value.location;
this.position = { lat: loc.y, lng: loc.x };
},
async getUserPosition() {
if (navigator.geolocation) {
// get GPS position
navigator.geolocation.getCurrentPosition(pos => {
// set the user location
this.userLocation = {
lat: pos.coords.latitude,
lng: pos.coords.longitude
};
});
}
}
}
};
</script>
On the mounted hook, we add a handler method onSearch for the geosearch/showlocation event which is invoked when a search result is selected. The onSearch method sets the marker to the search location.
Now that we have finished implementing all the features of our Location Selector, we can test it out. To do that, just import it to some other component and handle the input event or use the v-model directive.
That's all for this article, hope you enjoyed reading it. Check out the source code and demo and thank you for reading
Upvote
Eyuel Berga Woldemichael
I am a Software Developer currently based in Addis Ababa, Ethiopia. I am passionate about Web and Mobile Development.

Related Articles