visit
The demo is . Feel free to fork it🚀
Google Maps API offers an to place DOM elements on the map. We can use the OverlayView to place the container of a on the map and render a custom react component into the container.
export class Overlay extends google.maps.OverlayView {
container: HTMLElement
pane: keyof google.maps.MapPanes
position: google.maps.LatLng | google.maps.LatLngLiteral
constructor(
container: HTMLElement,
pane: keyof google.maps.MapPanes,
position: google.maps.LatLng | google.maps.LatLngLiteral
) {
super()
this.container = container
this.pane = pane
this.position = position
}
onAdd(): void {
const pane = this.getPanes()?.[this.pane]
pane?.appendChild(this.container)
}
draw(): void {
const projection = this.getProjection()
const point = projection.fromLatLngToDivPixel(this.position)
if (point === null) {
return
}
this.container.style.transform = `translate(${point.x}px, ${point.y}px)`
}
onRemove(): void {
if (this.container.parentNode !== null) {
this.container.parentNode.removeChild(this.container)
}
}
}
Now that we have our custom Overlay class, we can create a React component to:
import { PropsWithChildren, useEffect, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { Overlay } from './Overlay'
type OverlayProps = PropsWithChildren<{
position: google.maps.LatLng | google.maps.LatLngLiteral
pane?: keyof google.maps.MapPanes
map: google.maps.Map
zIndex?: number
}>
export default function OverlayView({
position,
pane = 'floatPane',
map,
zIndex,
children,
}: OverlayProps) {
const container = useMemo(() => {
const div = document.createElement('div')
div.style.position = 'absolute'
return div
}, [])
const overlay = useMemo(() => {
return new Overlay(container, pane, position)
}, [container, pane, position])
useEffect(() => {
overlay?.setMap(map)
return () => overlay?.setMap(null)
}, [map, overlay])
// to move the container to the foreground and background
useEffect(() => {
container.style.zIndex = `${zIndex}`
}, [zIndex, container])
return createPortal(children, container)
}
Let's create a marker using the OverlayView component and a button.
import { useMemo } from "react";
import { Hotel } from "../../types/hotel";
import OverlayView from "../OverlayView";
interface CustomMarkerProps {
hotel: Hotel;
map?: google.maps.Map;
}
export default function CustomMarker({
hotel,
map,
}: CustomMarkerProps) {
const price = useMemo(() => {
return `$ ${hotel.ratesSummary.minPrice.replace(/\.(.*?\d*)/g, '')}`
}, [hotel])
return (
<>
{map && (
<OverlayView
position={{
lat: hotel.location.latitude as number,
lng: hotel.location.longitude as number,
}}
map={map}
styles={{
backgorundColor: 'DarkGray',
color: 'white',
}}
>
{/* use a button as the marker */}
<button onClick={handleClick}>{price}</button>
</OverlayView>
)}
</>
)
}
If you've started your dev server or tried to build the project, you might see a Reference Error saying "google is not defined":
This is because Webpack doesn't recognize "google" at build time. At build time, Google Maps JavaScript API is not yet loaded so Webpack is not able to evaluate the Overlay class at the module level. What we can do is to wrap the class in a to resolve the build error.
export function createOverlay(
container: HTMLElement,
pane: keyof google.maps.MapPanes,
position: google.maps.LatLng | google.maps.LatLngLiteral
) {
class Overlay extends google.maps.OverlayView {
container: HTMLElement
pane: keyof google.maps.MapPanes
position: google.maps.LatLng | google.maps.LatLngLiteral
constructor(
container: HTMLElement,
pane: keyof google.maps.MapPanes,
position: google.maps.LatLng | google.maps.LatLngLiteral
) {
super()
this.container = container
this.pane = pane
this.position = position
}
onAdd(): void {
const pane = this.getPanes()?.[this.pane]
pane?.appendChild(this.container)
}
draw(): void {
const projection = this.getProjection()
const point = projection.fromLatLngToDivPixel(this.position)
if (point === null) {
return
}
this.container.style.transform = `translate(${point.x}px, ${point.y}px)`
}
onRemove(): void {
if (this.container.parentNode !== null) {
this.container.parentNode.removeChild(this.container)
}
}
}
return new Overlay(container, pane, position)
}
Let's refactor the OverlayView component accordingly:
- import { Overlay } from './Overlay'
+ import { createOverlay } from './Overlay'
export default function OverlayView({
position,
pane = 'floatPane',
map,
zIndex,
children,
}: OverlayProps) {
const overlay = useMemo(() => {
- return new Overlay(container, pane, position)
+ return createOverlay(container, pane, position)
}, [container, pane, position])
}
import { useCallback, useMemo } from 'react'
import { Hotel } from '../../types/hotel'
import OverlayView from '../OverlayView'
import { motion } from 'framer-motion'
interface CustomMarkerProps {
hotel: Hotel
map?: google.maps.Map
onClick: (payload: Hotel) => void
highlight?: boolean
}
export default function CustomMarker({
hotel,
map,
onClick,
highlight,
}: CustomMarkerProps) {
const price = useMemo(() => {
return `$ ${hotel.ratesSummary.minPrice.replace(/\.(.*?\d*)/g, '')}`
}, [hotel])
const handleClick = useCallback(() => {
onClick(hotel)
}, [onClick, hotel])
return (
<>
{map && (
<OverlayView
position={{
lat: hotel.location.latitude as number,
lng: hotel.location.longitude as number,
}}
map={map}
// when users select it, move the marker to the foreground
zIndex={highlight ? 99 : 0}
>
{/* appearance transition */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 20,
delay: Math.random() * 0.3,
}}
>
<button
onClick={handleClick}
// button state toggle
styles={{
backgorundColor: highlight ? 'white' : 'DarkGray',
color: highlight ? 'black' : 'white',
}}
>
{price}
</button>
</motion.div>
</OverlayView>
)}
</>
)
}