Currently, it is not entirely clear how to add custom control elements to the map. You can dig through the SearchBox and SearchBoxCreator source to figure it out, but that seems like an unnecessary amount of work for potential devs when a working implementation is quite simple (plus, it has some intricacies due to using an input element).
(this can be submitted as a PR as well, but it's very short and simple)
import { Component } from 'react';
export default class CustomControl extends Component {
addToMap(root) {
const { mapHolderRef, controlPosition } = this.props;
mapHolderRef.getMap().controls[controlPosition].push(root);
}
render() {
<div ref={this.addToMap}>{this.props.children}</div>
}
}
import { Component } from 'react';
import { GoogleMapLoader, GoogleMap, CustomControl } from 'react-google-maps';
export default class UsageExample extends Component {
render() {
<GoogleMapLoader containerElement={<div style={{height: '100%'}} />}
googleMapElement={
<GoogleMap defaultZoom={3} defaultCenter={{ lat: -25.363882, lng: 131.044922 }}>
<CustomControl controlPosition{google.maps.ControlButton.RIGHT_BOTTOM}>
<p>Hello, World</p>
</CustomControl>
</GoogleMap>
} />
}
}
I defined MapControl like:
import React from 'react'
import ReactDOM from 'react-dom'
// Enables custom elements within <GoogleMap>
// Children shouldn't change height between renders
export const MapControl = React.createClass({
componentDidMount() { this._render() },
componentDidUpdate() { this._render() },
componentWillUnmount() {
const {mapHolderRef, controlPosition} = this.props
const index = mapHolderRef.getMap().controls[controlPosition].getArray().indexOf(this.el)
mapHolderRef.getMap().controls[controlPosition].removeAt(index)
},
_render() {
const {mapHolderRef, controlPosition, children} = this.props
ReactDOM.render(
<div
ref={el => {
const controlSet = mapHolderRef.getMap().controls[controlPosition]
if (!this.renderedOnce) {
this.el = el
controlSet.push(el)
} else if (el && this.el && el !== this.el) {
this.el.innerHTML = '';
[].slice.call(el.childNodes).forEach(child => this.el.appendChild(child))
}
this.renderedOnce = true
}}
>
{children}
</div>,
document.createElement('div')
)
},
render() {
return <noscript />
},
})
export default MapControl
This plays nicely when React needs to re-render or unmount controls
@PsychicNoodles @ashtonwar this could be easily done in 6.0.0 since we wrote it. Would you like to submit a PR?
Also, 6.0.0 is released on npm beta tag now. We also have a new demo page. Feel free to try it:
https://tomchentw.github.io/react-google-maps/
@ashtonsix this was a lifesaver for me. For anyone that needs it, here is a tweak of it implemented in ES6, and using the same internal conventions for referencing the map via the context api so that you don't have to pass in a reference to it.
import React from 'react';
import PropTypes from 'prop-types';
import { render } from 'react-dom';
import { MAP } from 'react-google-maps/src/lib/constants';
class MapControl extends React.Component {
static contextTypes = {
[MAP]: PropTypes.object
}
static propTypes = {
controlPosition: PropTypes.number
}
static defaultProps = {
controlPosition: google.maps.ControlPosition.TOP_LEFT
}
componentDidMount() {
this.map = this.context[MAP];
this._render();
}
componentDidUpdate() {
this._render();
}
componentWillUnmount() {
const {controlPosition} = this.props;
const index = this.map.controls[controlPosition].getArray().indexOf(this.el);
this.map.controls[controlPosition].removeAt(index);
}
_render() {
const {controlPosition, children} = this.props;
render(
<div ref={el => {
if (!this.renderedOnce) {
this.el = el;
this.map.controls[controlPosition].push(el);
} else if (el && this.el && el !== this.el) {
this.el.innerHTML = '';
[].slice.call(el.childNodes).forEach(child => this.el.appendChild(child));
}
this.renderedOnce = true;
}}>
{children}
</div>,
document.createElement('div')
);
}
render() {
return <noscript />;
}
}
@jamesmfriedman Bless you. This was the only way I could get this working properly without directly interacting with the scary ref.context.__SECRET_MAP_DO_NOT_USE_OR_YOU_WILL_BE_FIRED value in ^9.2.2. The only fix I needed for this version was to remove src from the MAP import.
If inside <MapControl> you use <Link> or i18n or theme or any other context based component it will not work. To fix it replace render to unstable_renderSubtreeIntoContainer in the snipped above (and add this as first argument):
// instead of
render(<div ref={...}>...</div>, document.createElement('div'))
// do
unstable_renderSubtreeIntoContainer(this, <div ref={...}>...</div>, document.createElement('div'))
UPDATE:
unstable_renderSubtreeIntoContainer made my components inside ignore shouldComponentUpdate method. So I rewrote MapContainer component using react-portal to be able use shouldComponentUpdate.
Could any one of you submit a PR? LOL
@tomchentw - have you considered creating a PR yourself?
Is @jamesmfriedman way the go to way of adding custom controls at the moment? I tried to look for a custom controls but could not find it in the docs
I am receiving a google is not defined when putting it into it's own file. I'm on v9.4.1
@quachsimon you have to have google maps added via a script tag, or explicitly import it
How would I explicitly import it in the file?
@quachsimon http://bfy.tw/FRuX ;)
Is this still not possible? I'll admit that I'm still learning React and this library, but I've created the following based on the code for existing components:
import canUseDOM from 'can-use-dom';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import { componentWillUnmount } from 'react-google-maps/lib/utils/MapChildHelper';
import { MAP } from 'react-google-maps/lib/constants';
/**
* A wrapper around any component as a control on the map
*
* @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/#control
*/
export class Control extends PureComponent {
static propTypes = {
/**
* Where to put `<Control>` inside a `<GoogleMap>`
*
* @example google.maps.ControlPosition.TOP_LEFT
* @type number
*/
controlPosition: PropTypes.number
}
static contextTypes = {
[MAP]: PropTypes.object,
}
componentWillMount() {
if (!canUseDOM || this.containerElement) {
return;
}
this.containerElement = document.createElement('div');
this.handleRenderChildToContainerElement();
if (React.version.match(/^16/)) {
return;
}
}
componentDidMount() {
this.handleMountAtControlPosition();
}
componentWillUpdate(nextProp) {
if (this.props.controlPosition !== nextProp.controlPosition) {
this.handleUnmountAtControlPosition();
}
}
componentDidUpdate(prevProps) {
if (this.props.children !== prevProps.children) {
this.handleRenderChildToContainerElement();
}
if (this.props.controlPosition !== prevProps.controlPosition) {
this.handleMountAtControlPosition();
}
}
componentWillUnmount() {
componentWillUnmount(this);
this.handleUnmountAtControlPosition();
if (React.version.match(/^16/)) {
return;
}
if (this.containerElement) {
ReactDOM.unmountComponentAtNode(this.containerElement);
this.containerElement = null;
}
}
handleRenderChildToContainerElement() {
if (React.version.match(/^16/)) {
return;
}
ReactDOM.unstable_renderSubtreeIntoContainer(this, React.Children.only(this.props.children), this.containerElement);
}
handleMountAtControlPosition() {
if (isValidControlPosition(this.props.controlPosition)) {
this.mountControlIndex = -1 + this.context[MAP].controls[this.props.controlPosition].push(
this.containerElement.firstChild);
}
}
handleUnmountAtControlPosition() {
if (isValidControlPosition(this.props.controlPosition)) {
const child = this.context[MAP].controls[this.props.controlPosition].removeAt(this.mountControlIndex);
if (child !== undefined) {
this.containerElement.appendChild(child);
}
}
}
render() {
if (React.version.match(/^16/)) {
return ReactDOM.createPortal(React.Children.only(this.props.children), this.containerElement);
}
return false;
}
}
export default Control;
const isValidControlPosition = _.isNumber;
It seems like overkill to me, but it works.
Most helpful comment
@ashtonsix this was a lifesaver for me. For anyone that needs it, here is a tweak of it implemented in ES6, and using the same internal conventions for referencing the map via the context api so that you don't have to pass in a reference to it.