Map
Overview
The GoogleMapComponent
is a Vue.js component designed for rendering a Google Map using the vue3-google-map
library. It supports features like customizable markers, zoom controls, auto-centering, and geo-location. The component is highly flexible, allowing developers to set map options dynamically, update markers, and listen to user interactions on the map, such as clicks and zoom changes.
Key Features:
- Displays a Google Map with customizable center and zoom.
- Supports auto-centering based on user location.
- Allows adding and removing markers, each with custom colors and info windows.
- Emits events for interactions like zoom changes, map clicks, and marker updates.
- Provides utility methods for marker management.
Usage Examples
Basic Usage Example
vue
<template>
<GoogleMapComponent
:center="{ lat: 37.7749, lng: -122.4194 }"
:zoom="12"
:markers="markers"
@zoomed="handleZoomChange"
@clicked="handleMapClick"
@located="handleLocation"
@marked="handleMarkerChange"
/>
</template>
<script setup>
import { ref } from 'vue'
const markers = ref([
{ lat: 37.7749, lng: -122.4194, title: 'San Francisco', color: 'red', info: 'Welcome to SF' },
])
function handleZoomChange(zoom) {
console.log('Zoom level changed to:', zoom)
}
function handleMapClick(location) {
console.log('Map clicked at:', location)
}
function handleLocation(location) {
console.log('User located at:', location)
}
function handleMarkerChange(updatedMarkers) {
console.log('Markers updated:', updatedMarkers)
}
</script>
Adding and Removing Markers
vue
<template>
<GoogleMapComponent
:center="{ lat: 40.7128, lng: -74.006 }"
:zoom="10"
:markers="markerList"
@marked="handleMarkerUpdate"
/>
<button @click="addMarker">Add Marker</button>
<button @click="removeMarker">Remove Marker</button>
</template>
<script setup>
import { ref } from 'vue'
const markerList = ref([{ lat: 40.7128, lng: -74.006, title: 'New York', color: 'blue' }])
function addMarker() {
markerList.value.push({ lat: 40.73061, lng: -73.935242, title: 'New Marker', color: 'green' })
}
function removeMarker() {
markerList.value.pop()
}
function handleMarkerUpdate(updatedMarkers) {
console.log('Markers updated:', updatedMarkers)
}
</script>
API Reference
Props
Prop | Type | Default | Description |
---|---|---|---|
center | { lat: number, lng: number } | { lat: 0, lng: 0 } | Coordinates of the map center. Required. |
zoom | number | 10 | Initial zoom level for the map. Required. |
markers | Array<TBsbMapMarker> | [] | Array of markers with latitude, longitude, title, color, and info window. |
autoCenter | boolean | true | Automatically centers the map based on user's geolocation. |
autoLocation | boolean | true | Attempts to get the user’s location upon mounting the component. |
Emits
Event Name | Payload | Description |
---|---|---|
centered | { lat: number, lng: number } | Emitted when the map center is changed. |
zoomed | number | Emitted when the zoom level changes. |
located | { lat: number, lng: number } | Emitted when the user's location is determined. |
marked | Array<TBsbMapMarker> | Emitted when the markers are updated. |
clicked | { lat: number, lng: number } | Emitted when the map is clicked, with the location of the click. |
loading | boolean | Emitted when the component is in a loading state (e.g., fetching geolocation). |
Methods (Exposed via defineExpose
)
Method Name | Description |
---|---|
getMarkers | Returns the current array of markers. |
setMarkers | Updates the marker list by adding new markers. |
delMarkers | Removes specific markers from the marker list based on their coordinates. |
Slots
None.
Types
ts
export type TBsbMapMarker = {
lat: number
lng: number
title?: string
color?: string
info?: string
}
Source
- Install Dependencies
ps
npm install @types/google.maps
npm install vue3-google-map
- Add Google API Key to
/.env.production
and/.env.development.local
ini
VITE_GOOGLE_MAP_API_KEY = 'YOUR_GOOGLE_MAP_API_KEY'
- Create component and unit test.
Component @/components/VBsbMap.vue
vue
<template>
<GoogleMap
id="google-map"
ref="googleMap"
map-id="google-map"
:api-key
:center="localCenter"
:zoom="localZoom"
@zoom_changed="zoomChanged"
@click="clickChanged"
:auto-center
:auto-location
>
<AdvancedMarker
v-for="(marker, index) in localMarkers"
:key="marker.title || index"
:options="{ position: { lat: marker.lat, lng: marker.lng }, title: marker.title }"
:pin-options="{ background: marker.color }"
>
<InfoWindow v-if="marker.info"><div v-html="marker.info"></div></InfoWindow>
</AdvancedMarker>
<v-overlay contained persistent v-model="loading" />
</GoogleMap>
</template>
<script setup lang="ts">
import { GoogleMap, AdvancedMarker, InfoWindow } from 'vue3-google-map'
const apiKey = import.meta.env.VITE_GOOGLE_MAP_API_KEY
const googleMap: Ref<typeof GoogleMap | null> = ref(null)
const props = defineProps({
center: {
type: Object as PropType<{ lat: number; lng: number }>,
default: () => ({ lat: 0, lng: 0 }),
required: true,
},
zoom: {
type: Number,
default: 10,
required: true,
},
markers: {
type: Array as PropType<TBsbMapMarker[]>,
default: () => [],
},
autoCenter: {
type: Boolean,
default: true,
},
autoLocation: {
type: Boolean,
default: true,
},
})
const emits = defineEmits(['centered', 'zoomed', 'located', 'marked', 'clicked', 'loading'])
const localCenter = ref(props.center)
const setCenter = (newCenter: { lat: number; lng: number }) => {
localCenter.value = newCenter
emits('centered', localCenter.value)
}
watch(
() => props.center,
(newCenter: { lat: number; lng: number }) => {
setCenter(newCenter)
},
)
const localZoom = ref(props.zoom)
function zoomChanged() {
const newZoom = googleMap.value?.map.getZoom() || 0
const emit = localZoom.value !== newZoom
localZoom.value = newZoom
if (emit) emits('zoomed', localZoom.value)
}
watch(
() => props.zoom,
(newZoom: number) => {
const emit = localZoom.value !== newZoom
localZoom.value = newZoom
if (emit) emits('zoomed', localZoom.value)
},
)
const location = ref({ lat: 0, lng: 0 })
export type TBsbMapMarker = {
lat: number
lng: number
title?: string
color?: string
info?: string
}
const localMarkers = ref<TBsbMapMarker[]>(props.markers)
const getMarkers = () => localMarkers.value
const setMarkers = (newMarkers: TBsbMapMarker[]) => {
localMarkers.value = [
...new Map(
[...localMarkers.value, ...newMarkers].map((marker) => [JSON.stringify(marker), marker]),
).values(),
]
emits('marked', localMarkers.value)
}
const delMarkers = (newMarkers: TBsbMapMarker[]) => {
localMarkers.value = localMarkers.value.filter(
(marker) =>
!newMarkers.some((newMarker) => marker.lat === newMarker.lat && marker.lng === newMarker.lng),
)
emits('marked', localMarkers.value)
}
watch(
() => props.markers,
(newMarkers: TBsbMapMarker[]) => {
setMarkers(newMarkers)
},
)
const clickChanged = (event: google.maps.MapMouseEvent) => {
if (event.latLng) emits('clicked', { lat: event.latLng.lat(), lng: event.latLng.lng() })
}
const loading = ref(false)
defineExpose({
loading,
location,
getMarkers,
setMarkers,
delMarkers,
})
onMounted(() => {
if (!props.autoLocation) return
loading.value = true
emits('loading', loading.value)
navigator.geolocation.getCurrentPosition(
(position) => {
location.value = {
lat: position.coords.latitude,
lng: position.coords.longitude,
}
emits('located', location.value)
if (props.autoCenter) setCenter(location.value)
loading.value = false
emits('loading', loading.value)
},
(error) => {
console.error(error)
loading.value = false
emits('loading', loading.value)
},
{ timeout: 10000 },
)
})
</script>
<style scoped>
#google-map {
position: relative;
width: 100%;
height: 100%;
}
</style>
Test @/components/__tests__/VBsbMap.test.ts
ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import VBsbMap from '../../components/VBsbMap.vue'
import { GoogleMap, AdvancedMarker, InfoWindow } from 'vue3-google-map'
import vuetify from '../../plugins/vuetify'
import type { TBsbMapMarker } from '../../components/VBsbMap.vue'
type VBsbMapComponent = InstanceType<typeof VBsbMap>
describe('VBsbMap.vue', () => {
let wrapper: VueWrapper<VBsbMapComponent>
beforeEach(() => {
wrapper = mount(VBsbMap, {
global: {
components: {
GoogleMap,
AdvancedMarker,
InfoWindow,
},
plugins: [vuetify],
},
props: {
center: { lat: 37.7749, lng: -122.4194 },
zoom: 12,
markers: [
{ lat: 37.7749, lng: -122.4194, title: 'Marker 1', color: 'red', info: 'Marker 1 info' },
{ lat: 37.7849, lng: -122.4294, title: 'Marker 2', color: 'blue', info: 'Marker 2 info' },
],
autoCenter: true,
autoLocation: false,
},
})
})
it('renders GoogleMap with correct center and zoom', () => {
const googleMap = wrapper.findComponent(GoogleMap)
expect(googleMap.exists()).toBe(true)
expect(googleMap.props('center')).toEqual({ lat: 37.7749, lng: -122.4194 })
expect(googleMap.props('zoom')).toBe(12)
})
it('renders the correct number of markers', () => {
const markers = wrapper.findAllComponents(AdvancedMarker)
expect(markers.length).toBe(2)
expect(markers[0].props('options')).toEqual({
position: { lat: 37.7749, lng: -122.4194 },
title: 'Marker 1',
})
expect(markers[1].props('options')).toEqual({
position: { lat: 37.7849, lng: -122.4294 },
title: 'Marker 2',
})
})
it('emits "centered" when the map center changes', async () => {
await wrapper.setProps({
center: { lat: 40.7128, lng: -74.006 },
})
expect(wrapper.emitted('centered')).toBeTruthy()
const emitted = wrapper.emitted('centered')
expect(emitted?.[0]).toEqual([{ lat: 40.7128, lng: -74.006 }])
})
it('adds new markers correctly', async () => {
const newMarkers: TBsbMapMarker[] = [
{ lat: 40.7128, lng: -74.006, title: 'New Marker', color: 'green' },
]
wrapper.vm.setMarkers(newMarkers)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('marked')).toBeTruthy()
const emitted = wrapper.emitted('marked')
expect(emitted?.[0]).toEqual([
[
{ lat: 37.7749, lng: -122.4194, title: 'Marker 1', color: 'red', info: 'Marker 1 info' },
{ lat: 37.7849, lng: -122.4294, title: 'Marker 2', color: 'blue', info: 'Marker 2 info' },
{ lat: 40.7128, lng: -74.006, title: 'New Marker', color: 'green' },
],
])
})
it('deletes markers correctly', async () => {
const markersToDelete: TBsbMapMarker[] = [{ lat: 37.7749, lng: -122.4194 }]
wrapper.vm.delMarkers(markersToDelete)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('marked')).toBeTruthy()
const emitted = wrapper.emitted('marked')
expect(emitted?.[0]).toEqual([
[{ lat: 37.7849, lng: -122.4294, title: 'Marker 2', color: 'blue', info: 'Marker 2 info' }],
])
})
})