JustFly1984 / react-google-maps-api

React Google Maps API
MIT License
1.75k stars 426 forks source link

Does this support Advanced Markers? #3250

Open SaavanNanavati opened 1 year ago

SaavanNanavati commented 1 year ago

I want to add class names to my marker.

Advanced markers support this: https://developers.google.com/maps/documentation/javascript/advanced-markers/html-markers

But I don't see advanced markers available in the repo.

Can you confirm how can I add class names to my marker?

Thanks!

SaavanNanavati commented 1 year ago

@JustFly1984 It seems like you're really active here so just wanted to tag for visibility. Thanks.

dszeto commented 10 months ago

+1 on this. Bumping up.

chris-iliopoulos commented 9 months ago

Same here ... want to use some other marker clustering lib for big datasets that works better with Advanced Markers but I can't seem to find a way to do it. My main issue is the Map ID during initialization so, +1 from me as well...

tcgilbert commented 8 months ago

So I was able to add Advanced Markers by doing the following:

  const { isLoaded } = useJsApiLoader({
    googleMapsApiKey: "<redacted>",
    libraries: ["marker"],
  });
 <GoogleMap
        center={center}
        zoom={13}
        options={options}
        mapContainerStyle={{ width: "100%", height: "100%" }}
        onLoad={onLoad}
        onBoundsChanged={handleBoundsChanged}
      >
        {mapRef.current &&
          locations.map((location) => {
            return (
              <Marker
                key={location.id}
                map={mapRef.current}
                position={{ lat: location.lat, lng: location.lng }}
              >
                <div className="marker">
                  <h2>{location.name}</h2>
                </div>
              </Marker>
            );
          })}
      </GoogleMap>

Here is the custom Marker component:

"use client";
import { FC, useRef, useEffect } from "react";
import { createRoot, Root } from "react-dom/client";

type LatLngLiteral = google.maps.LatLngLiteral;
type Map = google.maps.Map;
type AdvancedMarkerElement = google.maps.marker.AdvancedMarkerElement;

interface MarkerProps {
  map: Map;
  position: LatLngLiteral;
  children: React.ReactNode;
}

const Marker: FC<MarkerProps> = ({ map, position, children }) => {
  const markerRef = useRef<AdvancedMarkerElement>();
  const rootRef = useRef<Root>();
  useEffect(() => {
    if (!rootRef.current) {
      const container = document.createElement("div");
      rootRef.current = createRoot(container);
      markerRef.current = new google.maps.marker.AdvancedMarkerElement({
        position,
        content: container,
      });
    }
  }, []);

  useEffect(() => {
    if (!markerRef.current || !rootRef.current) return;
    rootRef.current.render(children);
    markerRef.current.position = position;
    markerRef.current.map = map;
  }, [map, position, children]);

  return <></>;
};

export default Marker;

This works by essentially translating the jsx/React node (children prop in the above Marker component) into a proper HTML dom element. Tbh I'm not the best at detailing why this works, but I got the insight from this video: https://www.youtube.com/watch?v=8kxYqoY2WwE&t=1019s&ab_channel=GoogleMapsPlatform

JustFly1984 commented 8 months ago

"use client" is only next.js compatible

JustFly1984 commented 8 months ago

@tcgilbert Your PR is welcome, can you please add this component to the library? I would publish new version.

mp3por commented 8 months ago

Hello, I get the following error image I created an ID but I don't know where to put it. Please help.

NOTE: I found the solution here - https://github.com/JustFly1984/react-google-maps-api/issues/3116

Fabioni commented 5 months ago

@tcgilbert can we get that PR?

bradley commented 5 months ago

I dont want to tag them but with tcgilbert's solution Im getting Illegal constructor for the line new google.maps.marker.AdvancedMarkerElement(...). I'm not using typescript, so there is that change, although I cant currently think of why that itself would matter.

rex-smith commented 5 months ago

So I was able to add Advanced Markers by doing the following:

  • When loading @react-google-maps/api, add `libraries: ["marker"]
  const { isLoaded } = useJsApiLoader({
    googleMapsApiKey: "<redacted>",
    libraries: ["marker"],
  });
  • In my Google Map component, I'm iterating through my locations and rendering custom Marker components:
 <GoogleMap
        center={center}
        zoom={13}
        options={options}
        mapContainerStyle={{ width: "100%", height: "100%" }}
        onLoad={onLoad}
        onBoundsChanged={handleBoundsChanged}
      >
        {mapRef.current &&
          locations.map((location) => {
            return (
              <Marker
                key={location.id}
                map={mapRef.current}
                position={{ lat: location.lat, lng: location.lng }}
              >
                <div className="marker">
                  <h2>{location.name}</h2>
                </div>
              </Marker>
            );
          })}
      </GoogleMap>

Here is the custom Marker component:

"use client";
import { FC, useRef, useEffect } from "react";
import { createRoot, Root } from "react-dom/client";

type LatLngLiteral = google.maps.LatLngLiteral;
type Map = google.maps.Map;
type AdvancedMarkerElement = google.maps.marker.AdvancedMarkerElement;

interface MarkerProps {
  map: Map;
  position: LatLngLiteral;
  children: React.ReactNode;
}

const Marker: FC<MarkerProps> = ({ map, position, children }) => {
  const markerRef = useRef<AdvancedMarkerElement>();
  const rootRef = useRef<Root>();
  useEffect(() => {
    if (!rootRef.current) {
      const container = document.createElement("div");
      rootRef.current = createRoot(container);
      markerRef.current = new google.maps.marker.AdvancedMarkerElement({
        position,
        content: container,
      });
    }
  }, []);

  useEffect(() => {
    if (!markerRef.current || !rootRef.current) return;
    rootRef.current.render(children);
    markerRef.current.position = position;
    markerRef.current.map = map;
  }, [map, position, children]);

  return <></>;
};

export default Marker;

This works by essentially translating the jsx/React node (children prop in the above Marker component) into a proper HTML dom element. Tbh I'm not the best at detailing why this works, but I got the insight from this video: https://www.youtube.com/watch?v=8kxYqoY2WwE&t=1019s&ab_channel=GoogleMapsPlatform

Heads up, if the mapRef is null when the component mounts, the markers will not show. Given that the ref updating won't trigger a re-render, I needed to create a new state variable (mapLoaded) to replace mapRef.current in the statement that triggers the Markers to show. Once the mapRef is set in the onLoad callback, I set mapLoaded to true.

alexandermirzoyan commented 4 months ago

More actual the need of AdvancedMarker after deprecation warning from Google

karlcarstensen commented 4 months ago

Please implement this import

alexandermirzoyan commented 4 months ago

Please implement this import

Which import?

karlcarstensen commented 4 months ago

Please implement this import

Which import?

AdvancedMarker

alexandermirzoyan commented 4 months ago

Please implement this import

Which import?

AdvancedMarker

Seems they don't export such component. At which version are you importing that?

karlcarstensen commented 4 months ago

Please implement this import

Which import?

AdvancedMarker

Seems they don't export such component. At which version are you importing that?

Right now only Marker is exported but Google has started throwing a warning because it’s depreciated. AdvancedMarker needs to be exported so that it can be used to remove the warning.

alexandermirzoyan commented 4 months ago

Please implement this import

Which import?

AdvancedMarker

Seems they don't export such component. At which version are you importing that?

Right now only Marker is exported but Google has started throwing a warning because it’s depreciated. AdvancedMarker needs to be exported so that it can be used to remove the warning.

I am also here with the same problem :) I thought you already found that the package has exported component. So let's wait for the contributors.

barabanoveugene commented 4 months ago

@rex-smith Hey! Can you show the code of the Google map and Marker component in full? I don't quite understand what you have changed.

rex-smith commented 4 months ago

@barabanoveugene

  1. Here's the marker component
const Marker = ({
  map,
  position,
  children,
  onClick,
}: MarkerProps) => {
  const markerRef = useRef<AdvancedMarkerElement>();
  const rootRef = useRef<Root>();
  useEffect(() => {
    if (!rootRef.current) {
      const container = document.createElement("div");
      rootRef.current = createRoot(container);
      markerRef.current = new google.maps.marker.AdvancedMarkerElement({
        position,
        content: container,
      });
    }
  }, [position]);

  useEffect(() => {
    if (!markerRef.current || !rootRef.current) return;
    rootRef.current.render(children);
    markerRef.current.position = position;
    markerRef.current.map = map;
    const clickListener = markerRef.current.addListener("click", onClick);
    return () => {
      clickListener.remove();
    };
  }, [map, position, children, onClick]);

  return <></>;
};
  1. Here's the GoogleMap component and onLoad function where the mapRef is initialized. In hindsight, I'm not sure why I didn't just hold the map in state rather than using a ref and keeping "mapLoaded" in state. In the GoogleMap component example, the map is just put into state with setMap(map) instead of mapRef.current = map below. That's how I see it in the example people have been basing this off of too https://github.com/leighhalliday/google-maps-threejs/blob/main/pages/markers.js (although without using the GoogleMap component from this library). In these cases, "map" just replaces "mapLoaded" where I have it below.
const [mapLoaded, setMapLoaded] = useState(false);
const mapRef = useRef<google.maps.Map | null>(null);
const onLoad = useCallback(
    (map: google.maps.Map) => {
      mapRef.current = map;
      setMapLoaded(true);
    },
  );

 <GoogleMap
        center={center}
        zoom={13}
        options={options}
        onLoad={onLoad}
      >
        {mapLoaded &&
          locations.map((location) => {
            return (
              <Marker
                key={location.id}
                map={mapRef.current}
                position={{ lat: location.lat, lng: location.lng }}
              >
                <div className="marker">
                  <h2>{location.name}</h2>
                </div>
              </Marker>
            );
          })}
      </GoogleMap>
JustFly1984 commented 4 months ago

@rex-smith can you please make a PR into the package?

rex-smith commented 4 months ago

@JustFly1984 I just tried, but I can't install the dependencies for some reason

image
JustFly1984 commented 4 months ago

@rex-smith use yarn@1

lovedeep5 commented 3 months ago

So implementation is fine, however it is missing many of the events and even will not support the clusters, I am using this package in a medium size project, and now google api started throwing error that, they deprecated the markers.

So AdvancedMarker with cluster and events supports are must have features.

Fabioni commented 3 months ago

@rex-smith hey, getting the dev setup to work also does not work for me. You could try to make the PR blindly..

hassan-mir commented 3 months ago

If looking to do clusters with AdvancedMarkers, the docs have a useful example: https://developers.google.com/maps/documentation/javascript/marker-clustering

JustFly1984 commented 3 months ago

I'm waiting till I get 500$ to my opencollective to implement AdvancedMarker into this library. Currently got 100$. If you guys croudfund this issue, I'll release new version with AdvancedMarker

hassan-mir commented 3 months ago

also found this lib solving most of my problems: https://visgl.github.io/react-google-maps/docs/api-reference/components/advanced-marker

pct-bchumak commented 3 months ago

@hassan-mir did you add a component from this library or completely redo the whole project?

pct-bchumak commented 3 months ago

@rex-smith Could you share a complete example - of your use of the Marker component using AdvancedMarkerElement?

pct-bchumak commented 3 months ago

@alexandermirzoyan you didn't get a fix on the warning?

alexandermirzoyan commented 3 months ago

@alexandermirzoyan you didn't get a fix on the warning?

Not yet, still waiting for a working solution

hassan-mir commented 3 months ago

@hassan-mir did you add a component from this library or completely redo the whole project?

redid the Map related bits. Was already using a MapProvider, now a simpler ApiProvider, GoogleMap replaced with Map and some minor rework but think it was worth it. Still had to treat the AdvancedMarker as a HTML element to get my animation to work but libs only a few months old, hoping for a better solution soon

Fabioni commented 3 months ago

I solved the problem by 'stealing' the component from visgl (https://github.com/visgl/react-google-maps/blob/main/src/components/advanced-marker.tsx). 500$ for this would have been like the best hourly pay you can imagine haha. I additionally added right-click, clickable and anchorAbove. If you need further explanation, let me know. In general the advanced markers do not feel completely mature, so you might have to fiddle around a bit, but this has nothing to do with the library here. I did remove the possibility to use the pin component (https://github.com/visgl/react-google-maps/blob/main/src/components/pin.tsx), it did work, but I just did not need it.

The actuall contribution to make this work is just adding this to connect the two librarys.

import { MapContext } from '@react-google-maps/api';
// ...
const map = useContext(MapContext);
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
// Based on https://github.dev/visgl/react-google-maps because the original package is not maintained anymore and misses advanced markers

/* eslint-disable complexity */
import React, {
    Children,
    forwardRef,
    useCallback,
    useContext,
    useEffect,
    useImperativeHandle,
    useMemo,
    useState
} from 'react';

import { createPortal } from 'react-dom';

import type { Ref, PropsWithChildren } from 'react';
import { MapContext } from '@react-google-maps/api';

export interface AdvancedMarkerContextValue {
    marker: google.maps.marker.AdvancedMarkerElement;
}

export const AdvancedMarkerContext =
    React.createContext<AdvancedMarkerContextValue | null>(null);

type AdvancedMarkerEventProps = {
    onClick?: (e: MouseEvent) => void;
    onRightClick?: (e: MouseEvent) => void;
    onDrag?: (e: google.maps.MapMouseEvent) => void;
    onDragStart?: (e: google.maps.MapMouseEvent) => void;
    onDragEnd?: (e: google.maps.MapMouseEvent) => void;
};

export type AdvancedMarkerProps = PropsWithChildren<
    Omit<google.maps.marker.AdvancedMarkerElementOptions, 'gmpDraggable'> &
    AdvancedMarkerEventProps & {
        /**
         * className to add a class to the advanced marker element
         * Can only be used with HTML Marker content
         */
        className?: string;
        anchorAbove?: boolean;
        draggable?: boolean;
        clickable?: boolean; // right now this just deactivates the onClick handler but does not change the google maps marker behaviour
    }
>;

export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null;
function useAdvancedMarker(props: AdvancedMarkerProps) {
    const [marker, setMarker] =
        useState<google.maps.marker.AdvancedMarkerElement | null>(null);
    const [contentContainer, setContentContainer] =
        useState<HTMLDivElement | null>(null);

    const map = useContext(MapContext);
    const markerLibrary = google.maps.marker

    const {
        children,
        className,
        anchorAbove,
        onClick,
        onRightClick,
        onDrag,
        onDragStart,
        onDragEnd,
        collisionBehavior,
        draggable,
        clickable,
        position,
        title,
        zIndex
    } = props;

    const numChilds = Children.count(children);

    // create marker instance and add it to the map when map becomes available
    useEffect(() => {
        if (!map || !markerLibrary) return;

        const newMarker = new markerLibrary.AdvancedMarkerElement();
        newMarker.map = map;

        setMarker(newMarker);

        // create container for marker content if there are children
        if (numChilds > 0) {
            const el = document.createElement('div');
            if (!anchorAbove) el.style.transform = 'translate(0, 50%)';
            if (className) el.className = className;

            newMarker.content = el;

            setContentContainer(el);
        }

        return () => {
            newMarker.map = null;
            setMarker(null);
            setContentContainer(null);
        };
        // We do not want to re-render the whole marker when the className changes
        // because that causes a short flickering of the marker.
        // The className update is handled in the useEffect below.
        // Excluding the className from the dependency array onm purpose here
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [map, markerLibrary, numChilds]);

    useEffect(() => {
        if ((className?.includes('translate') || className?.includes('transform')) && !anchorAbove) console.warn("not setting anchorAbove automatically set 'transform: translate(0, 50%)' to center the marker on the position. Set this to false to be able to use custom translate values.");
    }, [className, anchorAbove]);

    // update className of advanced marker element
    useEffect(() => {
        if (!contentContainer) return;
        contentContainer.className = className ?? '';
    }, [contentContainer, className]);

    // bind all marker events
    useEffect(() => {
        if (!marker) return;

        const gme = google.maps.event;

        const controller = new AbortController();
        const { signal } = controller;

        // TODO fix the type mismatch for click and contextmenu events
        // marker.addEventListener("gmp-click", (e: google.maps.marker.AdvancedMarkerClickEvent) => { console.log(e) }); // only available in beta and misses some properties, check https://issuetracker.google.com/issues/331684436

        if (onClick && clickable) gme.addListener(marker, 'click', (e: { domEvent: PointerEvent }) => onClick(e.domEvent));
        if (onRightClick && clickable) marker.content?.addEventListener('contextmenu', onRightClick, { signal }); // setting marker.addEventListener directly onces this is out of beta
        if (onDrag && draggable) gme.addListener(marker, 'drag', onDrag);
        if (onDragStart && draggable) gme.addListener(marker, 'dragstart', onDragStart);
        if (onDragEnd && draggable) gme.addListener(marker, 'dragend', onDragEnd);

        if ((onDrag || onDragStart || onDragEnd) && !draggable) {
            console.warn(
                'You need to set the marker to draggable to listen to drag-events.'
            );
        }

        if ((onClick && !clickable) || (onRightClick && !clickable)) {
            console.warn(
                'You need to set the marker to clickable to listen to click-events.'
            );
        }

        const m = marker;
        return () => {
            gme.clearInstanceListeners(m); // for addListener
            controller.abort(); // for addEventListener
        };
    }, [marker, draggable, onClick, onDragStart, onDrag, onDragEnd, onRightClick, clickable]);

    // update other marker props when changed
    useEffect(() => {
        if (!marker) return;

        if (position !== undefined) marker.position = position;
        if (draggable !== undefined) marker.gmpDraggable = draggable;
        // use marker.gmpClickable once this is resolved https://issuetracker.google.com/issues/331684436
        if (collisionBehavior !== undefined)
            marker.collisionBehavior = collisionBehavior;
        if (zIndex !== undefined) marker.zIndex = zIndex;
        if (typeof title === 'string') marker.title = title;
    }, [marker, position, draggable, collisionBehavior, zIndex, title, clickable]);

    return [marker, contentContainer] as const;
}

const AdvancedMarkerComponent = forwardRef(
    (props: AdvancedMarkerProps, ref: Ref<AdvancedMarkerRef>) => {
        const { children } = props;
        const [marker, contentContainer] = useAdvancedMarker(props);

        const advancedMarkerContextValue: AdvancedMarkerContextValue | null =
            useMemo(() => (marker ? { marker } : null), [marker]);

        useImperativeHandle(ref, () => marker, [marker]);

        if (!marker) {
            return null;
        }

        // we could add other props here to the context, but lets try to achieve this with tailwind group or other means first
        return (
            <AdvancedMarkerContext.Provider value={advancedMarkerContextValue}>
                {contentContainer !== null && createPortal(children, contentContainer)}
            </AdvancedMarkerContext.Provider>
        );
    }
);
AdvancedMarkerComponent.displayName = 'AdvancedMarker';
export const AdvancedMarker = AdvancedMarkerComponent;

// eslint-disable-next-line react-refresh/only-export-components
export function useAdvancedMarkerRef() {
    const [marker, setMarker] =
        useState<google.maps.marker.AdvancedMarkerElement | null>(null);

    const refCallback = useCallback((m: AdvancedMarkerRef | null) => {
        setMarker(m);
    }, []);

    return [refCallback, marker] as const;
}
alexandermirzoyan commented 3 months ago

I solved the problem by 'stealing' the component from visgl (https://github.com/visgl/react-google-maps/blob/main/src/components/advanced-marker.tsx). 500$ for this would have been like the best hourly pay you can imagine haha. I additionally added right-click, clickable and anchorAbove. If you need further explanation, let me know. In general the advanced markers do not feel completely mature, so you might have to fiddle around a bit, but this has nothing to do with the library here. I did remove the possibility to use the pin component (https://github.com/visgl/react-google-maps/blob/main/src/components/pin.tsx), it did work, but I just did not need it.

The actuall contribution to make this work is just adding this to connect the two librarys.

import { MapContext } from '@react-google-maps/api';
// ...
const map = useContext(MapContext);
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
// Based on https://github.dev/visgl/react-google-maps because the original package is not maintained anymore and misses advanced markers

/* eslint-disable complexity */
import React, {
    Children,
    forwardRef,
    useCallback,
    useContext,
    useEffect,
    useImperativeHandle,
    useMemo,
    useState
} from 'react';

import { createPortal } from 'react-dom';

import type { Ref, PropsWithChildren } from 'react';
import { MapContext } from '@react-google-maps/api';

export interface AdvancedMarkerContextValue {
    marker: google.maps.marker.AdvancedMarkerElement;
}

export const AdvancedMarkerContext =
    React.createContext<AdvancedMarkerContextValue | null>(null);

type AdvancedMarkerEventProps = {
    onClick?: (e: MouseEvent) => void;
    onRightClick?: (e: MouseEvent) => void;
    onDrag?: (e: google.maps.MapMouseEvent) => void;
    onDragStart?: (e: google.maps.MapMouseEvent) => void;
    onDragEnd?: (e: google.maps.MapMouseEvent) => void;
};

export type AdvancedMarkerProps = PropsWithChildren<
    Omit<google.maps.marker.AdvancedMarkerElementOptions, 'gmpDraggable'> &
    AdvancedMarkerEventProps & {
        /**
         * className to add a class to the advanced marker element
         * Can only be used with HTML Marker content
         */
        className?: string;
        anchorAbove?: boolean;
        draggable?: boolean;
        clickable?: boolean; // right now this just deactivates the onClick handler but does not change the google maps marker behaviour
    }
>;

export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null;
function useAdvancedMarker(props: AdvancedMarkerProps) {
    const [marker, setMarker] =
        useState<google.maps.marker.AdvancedMarkerElement | null>(null);
    const [contentContainer, setContentContainer] =
        useState<HTMLDivElement | null>(null);

    const map = useContext(MapContext);
    const markerLibrary = google.maps.marker

    const {
        children,
        className,
        anchorAbove,
        onClick,
        onRightClick,
        onDrag,
        onDragStart,
        onDragEnd,
        collisionBehavior,
        draggable,
        clickable,
        position,
        title,
        zIndex
    } = props;

    const numChilds = Children.count(children);

    // create marker instance and add it to the map when map becomes available
    useEffect(() => {
        if (!map || !markerLibrary) return;

        const newMarker = new markerLibrary.AdvancedMarkerElement();
        newMarker.map = map;

        setMarker(newMarker);

        // create container for marker content if there are children
        if (numChilds > 0) {
            const el = document.createElement('div');
            if (!anchorAbove) el.style.transform = 'translate(0, 50%)';
            if (className) el.className = className;

            newMarker.content = el;

            setContentContainer(el);
        }

        return () => {
            newMarker.map = null;
            setMarker(null);
            setContentContainer(null);
        };
        // We do not want to re-render the whole marker when the className changes
        // because that causes a short flickering of the marker.
        // The className update is handled in the useEffect below.
        // Excluding the className from the dependency array onm purpose here
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [map, markerLibrary, numChilds]);

    useEffect(() => {
        if ((className?.includes('translate') || className?.includes('transform')) && !anchorAbove) console.warn("not setting anchorAbove automatically set 'transform: translate(0, 50%)' to center the marker on the position. Set this to false to be able to use custom translate values.");
    }, [className, anchorAbove]);

    // update className of advanced marker element
    useEffect(() => {
        if (!contentContainer) return;
        contentContainer.className = className ?? '';
    }, [contentContainer, className]);

    // bind all marker events
    useEffect(() => {
        if (!marker) return;

        const gme = google.maps.event;

        const controller = new AbortController();
        const { signal } = controller;

        // TODO fix the type mismatch for click and contextmenu events
        // marker.addEventListener("gmp-click", (e: google.maps.marker.AdvancedMarkerClickEvent) => { console.log(e) }); // only available in beta and misses some properties, check https://issuetracker.google.com/issues/331684436

        if (onClick && clickable) gme.addListener(marker, 'click', (e: { domEvent: PointerEvent }) => onClick(e.domEvent));
        if (onRightClick && clickable) marker.content?.addEventListener('contextmenu', onRightClick, { signal }); // setting marker.addEventListener directly onces this is out of beta
        if (onDrag && draggable) gme.addListener(marker, 'drag', onDrag);
        if (onDragStart && draggable) gme.addListener(marker, 'dragstart', onDragStart);
        if (onDragEnd && draggable) gme.addListener(marker, 'dragend', onDragEnd);

        if ((onDrag || onDragStart || onDragEnd) && !draggable) {
            console.warn(
                'You need to set the marker to draggable to listen to drag-events.'
            );
        }

        if ((onClick && !clickable) || (onRightClick && !clickable)) {
            console.warn(
                'You need to set the marker to clickable to listen to click-events.'
            );
        }

        const m = marker;
        return () => {
            gme.clearInstanceListeners(m); // for addListener
            controller.abort(); // for addEventListener
        };
    }, [marker, draggable, onClick, onDragStart, onDrag, onDragEnd, onRightClick, clickable]);

    // update other marker props when changed
    useEffect(() => {
        if (!marker) return;

        if (position !== undefined) marker.position = position;
        if (draggable !== undefined) marker.gmpDraggable = draggable;
        // use marker.gmpClickable once this is resolved https://issuetracker.google.com/issues/331684436
        if (collisionBehavior !== undefined)
            marker.collisionBehavior = collisionBehavior;
        if (zIndex !== undefined) marker.zIndex = zIndex;
        if (typeof title === 'string') marker.title = title;
    }, [marker, position, draggable, collisionBehavior, zIndex, title, clickable]);

    return [marker, contentContainer] as const;
}

const AdvancedMarkerComponent = forwardRef(
    (props: AdvancedMarkerProps, ref: Ref<AdvancedMarkerRef>) => {
        const { children } = props;
        const [marker, contentContainer] = useAdvancedMarker(props);

        const advancedMarkerContextValue: AdvancedMarkerContextValue | null =
            useMemo(() => (marker ? { marker } : null), [marker]);

        useImperativeHandle(ref, () => marker, [marker]);

        if (!marker) {
            return null;
        }

        // we could add other props here to the context, but lets try to achieve this with tailwind group or other means first
        return (
            <AdvancedMarkerContext.Provider value={advancedMarkerContextValue}>
                {contentContainer !== null && createPortal(children, contentContainer)}
            </AdvancedMarkerContext.Provider>
        );
    }
);
AdvancedMarkerComponent.displayName = 'AdvancedMarker';
export const AdvancedMarker = AdvancedMarkerComponent;

// eslint-disable-next-line react-refresh/only-export-components
export function useAdvancedMarkerRef() {
    const [marker, setMarker] =
        useState<google.maps.marker.AdvancedMarkerElement | null>(null);

    const refCallback = useCallback((m: AdvancedMarkerRef | null) => {
        setMarker(m);
    }, []);

    return [refCallback, marker] as const;
}

OMG $500 code just in a comments hahahah.

alexandermirzoyan commented 3 months ago

@Fabioni thank you so much!

pct-bchumak commented 3 months ago

@Fabioni thank you so much!

could you help with this solution?

import { MapContext } from '@react-google-maps/api';
// ...
const map = useContext(MapContext);

when i need to use this part and how i can add my own icon?

Fabioni commented 3 months ago

Hey @pct-bchumak , as far as I know, advanced_markers have no special icon functionality, so you would just use it like any other marker content, like this:

<AdvancedMarker>
    <svg></svg>
</AdvancedMarker>

<AdvancedMarker>
    <i class="fa-solid fa-house"></i>
</AdvancedMarker>
pct-bchumak commented 3 months ago

@Fabioni thanks! And

import { MapContext } from '@react-google-maps/api';
// ...
const map = useContext(MapContext);

this part i need to use in map component?

Fabioni commented 3 months ago

@pct-bchumak this part is already included in the code of advanced marker, that I shared. It was just to clarify what I added.

Just copy and paste the big code I shared into a new file and use AdvancedMarker as a component the same way you would use other react-google-maps components.

jukkahuuskonen commented 3 months ago

... I did remove the possibility to use the pin component (https://github.com/visgl/react-google-maps/blob/main/src/components/pin.tsx), it did work, but I just did not need it...

What do you mean with this removing? I couldn't find anything related to the pin-component in VISGL-advancedMarker either?

Fabioni commented 3 months ago

Hey @jukkahuuskonen, you are right, I actually did NOT remove it. In the following code, a React context is created so that the children of the advanced marker can access the advanced marker component.

<AdvancedMarkerContext.Provider value={advancedMarkerContextValue}>
    {contentContainer !== null && createPortal(children, contentContainer)}
</AdvancedMarkerContext.Provider>

This is only used by the pin component (which you can just copy and paste from the visgl code). The pin component does

// ... 
const advancedMarker = useContext(AdvancedMarkerContext)?.marker;
advancedMarker.content = pinElement.element;

I thought I changed that to the following, but apparently I did not, haha.

<>
    {contentContainer !== null && createPortal(children, contentContainer)}
</>
jukkahuuskonen commented 3 months ago

Hey @jukkahuuskonen, you are right, I actually did NOT remove it. ...


Great, since we actually do have a use for Pin-component.
Fabioni commented 3 months ago

@jukkahuuskonen, nice, you can just use it from visgl :) My opinion: I think the pin component is just for convenience and somehow feels not correct because of its special treatment (it is no html, but is a js instantiated objects added to the marker object). At the end, being able to use any HTML including img and svg means there is no reason to restrict yourself with the google maps pin component in my opinion.

The way I use it is via:

<AdvancedMarker>
    <svg>
        <!-- my custom, more flexible and react handled pin replacement  -->
    </svg>
</AdvancedMarker>
jukkahuuskonen commented 3 months ago

@jukkahuuskonen, nice, just use it :) I think the pin component is just for convenience and somehow feels not correct because of its special treatment. At the end, being able to use any HTML including img and svg means there is no reason to restrict yourself with the google maps pin component in my opinion.

The way I use it is via:

<AdvancedMarker>
    <svg>
        <!-- my custom, more flexible and react handled pin replacement  -->
    </svg>
</AdvancedMarker>

@Fabioni Ah, just got into this advanced marker and didn't realize that the pin is just the standard pin. In that case, we'll probably be using SVG there too.

Thanks again!

jukkahuuskonen commented 3 months ago

Just as a note to everyone. It seems that the unlike the old google marker new advancedMarker doesn't support eg. mouseover/mouseout-events. If you are relying on those with advancedMarkers (eg to show OverlayView like in below example), you will need to move them from the marker-component itself to your custom marker component eg:

/* global google */
import { useCallback, useState } from 'react';
import { OverlayView } from '@react-google-maps/api';
import useDebounceState from 'hooks/useDebounceState';
import MySvgPin from 'shared/theme/assets/svg/MySvgPin.svg?react';
import { AdvancedMarker } from 'shared/components/Maps/AdvancedMarker';

const MyMarker = ({ latLng }) => {
  const [showInfoWindow, setShowInfoWindow] = useDebounceState(false, {
    timeout: 50,
    onlyLast: true,
  });

  const handleMouseOver = useCallback(() => {
    setShowInfoWindow(true);
  }, [setShowInfoWindow]);

  const handleMouseOut = useCallback(() => {
    setShowInfoWindow(false);
  }, [setShowInfoWindow]);

  return (
    <AdvancedMarker
      // onMouseOver={handleMouseOver}  MOVE THESE TO YOUR CUSTOM PIN-COMPONENT
      // onMouseOut={handleMouseOut}    MINE IS AN SVG, BUT SHOULD WORK WITH ANY REGULAR COMPONENT
    >
      <MySvgPin
        onMouseOver={handleMouseOver}
        onMouseOut={handleMouseOut}
      />
      {showInfoWindow ? (
        <OverlayView
          position={latLng}
          style={{ left: '100px', width: '0px', height: '0px' }}
          mapPaneName={OverlayView.FLOAT_PANE}
        >
          <MyInfoComponent />
        </OverlayView>
      ) : null}
    </AdvancedMarker>
  );
};

export default MyMarker;
jukkahuuskonen commented 2 months ago

Current MarkerClusterer doesn't support new advanced markers.

I rewrote those components so that they work (at least on my project) and posted them in discussion here: https://github.com/JustFly1984/react-google-maps-api/discussions/3349

If you need them in your project, feel free to experiment with them and if you have time and energy, maybe create a PR to fix them in this project.