Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple custom control class #310

Closed
PsychicNoodles opened this issue Jul 25, 2016 · 12 comments
Closed

Add a simple custom control class #310

PsychicNoodles opened this issue Jul 25, 2016 · 12 comments

Comments

@PsychicNoodles
Copy link

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).

Example Implementation

(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>
  }
}

Example Usage

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>
                     } />
  }
}
@ashtonsix
Copy link

ashtonsix commented Aug 9, 2016

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

@tomchentw
Copy link
Owner

@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/

@jamesmfriedman
Copy link

@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 />;
	}
}

@wayspurrchen
Copy link

wayspurrchen commented Nov 13, 2017

@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.

@oluckyman
Copy link

oluckyman commented Dec 6, 2017

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.

@tomchentw
Copy link
Owner

Could any one of you submit a PR? LOL

@ashtonsix
Copy link

@tomchentw - have you considered creating a PR yourself?

@quachsimon
Copy link

quachsimon commented Dec 7, 2017

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

@jamesmfriedman
Copy link

@quachsimon you have to have google maps added via a script tag, or explicitly import it

@quachsimon
Copy link

How would I explicitly import it in the file?

@jamesmfriedman
Copy link

@quachsimon http://bfy.tw/FRuX ;)

@neocotic
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants