Skip to content

Commit

Permalink
feat: implement props for all map-events
Browse files Browse the repository at this point in the history
  • Loading branch information
usefulthink committed Nov 8, 2023
1 parent 6221bb6 commit 5f3fe98
Showing 1 changed file with 142 additions and 36 deletions.
178 changes: 142 additions & 36 deletions src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,129 @@ import {useCallbackRef} from '../libraries/use-callback-ref';
export interface GoogleMapsContextValue {
map: google.maps.Map | null;
}

export const GoogleMapsContext =
React.createContext<GoogleMapsContextValue | null>(null);

/**
* Props for the Google Maps Map Component
* Handlers for all events that could be emitted by map-instances.ß
*/
export type MapProps = google.maps.MapOptions & {
style?: CSSProperties;
/**
* Adds custom style to the map by passing a css class.
*/
className?: string;
/**
* Adds initial bounds to the map as an alternative to specifying the center/zoom of the map.
* Calls the fitBounds method internally https://developers.google.com/maps/documentation/javascript/reference/map?hl=en#Map-Methods
*/
initialBounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
/**
* An id that is added to the map. Needed when using more than one Map component.
* This is also needed to reference the map inside the useMap hook.
*/
id?: string;
/**
* Viewport from deck.gl
*/
viewport?: unknown;
/**
* View state from deck.gl
*/
viewState?: Record<string, unknown>;
/**
* Initial View State from deck.gl
*/
initialViewState?: Record<string, unknown>;
type MapEventProps = {
// map view state events
onBoundsChanged: () => void;
onCenterChanged: () => void;
onHeadingChanged: () => void;
onTiltChanged: () => void;
onZoomChanged: () => void;
onProjectionChanged: () => void;

// mouse / touch / pointer events
onClick: (
event: google.maps.MapMouseEvent | google.maps.IconMouseEvent
) => void;
onDblclick: (event: google.maps.MapMouseEvent) => void;
onContextmenu: (event: google.maps.MapMouseEvent) => void;
onMousemove: (event: google.maps.MapMouseEvent) => void;
onMouseover: (event: google.maps.MapMouseEvent) => void;
onMouseout: (event: google.maps.MapMouseEvent) => void;
onDrag: () => void;
onDragend: () => void;
onDragstart: () => void;

// loading events
onTilesloaded: () => void;
onIdle: () => void;

// configuration events
onIsFractionalZoomEnabledChanged: () => void;
onMapCapabilitiesChanged: () => void;
onMapTypeIdChanged: () => void;
onRenderingtypeChanged: () => void;
};

/**
* Maps the camelCased names of event-props to the corresponding event-types
* used in the maps API.
*/
const propNameToEventType: {[prop in keyof MapEventProps]: string} = {
onBoundsChanged: 'bounds_changed',
onCenterChanged: 'center_changed',
onClick: 'click',
onContextmenu: 'contextmenu',
onDblclick: 'dblclick',
onDrag: 'drag',
onDragend: 'dragend',
onDragstart: 'dragstart',
onHeadingChanged: 'heading_changed',
onIdle: 'idle',
onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed',
onMapCapabilitiesChanged: 'mapcapabilities_changed',
onMapTypeIdChanged: 'maptypeid_changed',
onMousemove: 'mousemove',
onMouseout: 'mouseout',
onMouseover: 'mouseover',
onProjectionChanged: 'projection_changed',
onRenderingtypeChanged: 'renderingtype_changed',
onTilesloaded: 'tilesloaded',
onTiltChanged: 'tilt_changed',
onZoomChanged: 'zoom_changed'
} as const;

type MapEventPropName = keyof MapEventProps;
const eventPropNames = Object.freeze(
Object.keys(propNameToEventType) as MapEventPropName[]
);

/**
* Props for the Google Maps Map Component
*/
export type MapProps = google.maps.MapOptions &
MapEventProps & {
style?: CSSProperties;
/**
* Adds custom style to the map by passing a css class.
*/
className?: string;
/**
* Adds initial bounds to the map as an alternative to specifying the center/zoom of the map.
* Calls the fitBounds method internally https://developers.google.com/maps/documentation/javascript/reference/map?hl=en#Map-Methods
*/
initialBounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
/**
* An id that is added to the map. Needed when using more than one Map component.
* This is also needed to reference the map inside the useMap hook.
*/
id?: string;
/**
* Viewport from deck.gl
*/
viewport?: unknown;
/**
* View state from deck.gl
*/
viewState?: Record<string, unknown>;
/**
* Initial View State from deck.gl
*/
initialViewState?: Record<string, unknown>;
};

/**
* Component to render a Google Maps map
*/
export const Map = (props: PropsWithChildren<MapProps>) => {
const {children, id, className, style, viewState, viewport} = props;

const context = useContext(APIProviderContext) as APIProviderContextValue;
const context = useContext(APIProviderContext);

if (!context) {
throw new Error(
'<Map> can only be used inside an <ApiProvider> component.'
);
}

const [map, mapRef] = useMapInstanceHandlerEffects(props, context);
const [map, mapRef] = useMapInstanceEffects(props, context);
useMapOptionsEffects(map, props);
useMapEvents(map, props);
useDeckGLCameraUpdateEffect(map, viewState);

const isViewportSet = useMemo(() => Boolean(viewport), [viewport]);
Expand Down Expand Up @@ -111,7 +181,7 @@ Map.deckGLViewProps = true;
* ref that will be used to pass the map-container into this hook.
* @internal
*/
function useMapInstanceHandlerEffects(
function useMapInstanceEffects(
props: MapProps,
context: APIProviderContextValue
): readonly [map: google.maps.Map | null, containerRef: Ref<HTMLDivElement>] {
Expand Down Expand Up @@ -144,7 +214,7 @@ function useMapInstanceHandlerEffects(
if (!container || !apiIsLoaded) return;

// remove all event-listeners to minimize memory-leaks
google.maps.event.clearInstanceListeners(container);
google.maps.event.clearInstanceListeners(newMap);

setMap(null);
removeMapInstance(id);
Expand Down Expand Up @@ -182,12 +252,17 @@ function useMapInstanceHandlerEffects(
/**
* Internal hook to update the map-options and view-parameters when
* props are changed.
* @internal
*/
function useMapOptionsEffects(map: google.maps.Map | null, mapProps: MapProps) {
const {center, zoom, heading, tilt, ...mapOptions} = mapProps;

// All of these effects aren't triggered when the map is changed.
// In that case, the values have already been passed to the map constructor.
/* eslint-disable react-hooks/exhaustive-deps --
*
* The following effects aren't triggered when the map is changed.
* In that case, the values will be or have been passed to the map
* constructor as mapOptions.
*/

// update the map options when mapOptions is changed
useEffect(() => {
Expand Down Expand Up @@ -223,6 +298,37 @@ function useMapOptionsEffects(map: google.maps.Map | null, mapProps: MapProps) {

map.setTilt(tilt as number);
}, [tilt]);
/* eslint-enable react-hooks/exhaustive-deps */
}

/**
* Sets up effects to bind event-handlers for all event-props in MapEventProps.
* @internal
*/
function useMapEvents(map: google.maps.Map | null, props: MapEventProps) {
// note: calling a useEffect hook from within a loop is prohibited by the
// rules of hooks, but it's ok here since it's unconditional and the number
// and order of iterations is always strictly the same.
// (see https://legacy.reactjs.org/docs/hooks-rules.html)

for (const propName of eventPropNames) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(
() => {
if (!map) return;
if (!props[propName]) return;

const listener = map.addListener(
propNameToEventType[propName],
props[propName]
);

return () => listener.remove();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[props[propName]]
);
}
}

/**
Expand Down

0 comments on commit 5f3fe98

Please sign in to comment.