Skip to content

Add support for places/Autocomplete service#768

Open
gergely-ujvari wants to merge 2 commits intotomchentw:masterfrom
gergely-ujvari:introduce-autocomplete-support
Open

Add support for places/Autocomplete service#768
gergely-ujvari wants to merge 2 commits intotomchentw:masterfrom
gergely-ujvari:introduce-autocomplete-support

Conversation

@gergely-ujvari
Copy link
Copy Markdown

Introduces a wrapper component around Autocomplete service

Example usage snippet:

import * as React from "react";
import { GoogleMap } from "react-google-maps";
import Autocomplete from "react-google-maps/lib/components/places/Autocomplete";

const INPUT_STYLE = {
  boxSizing: `border-box`,
  MozBoxSizing: `border-box`,
  border: `1px solid transparent`,
  width: `240px`,
  height: `32px`,
  marginTop: `27px`,
  padding: `0 12px`,
  borderRadius: `1px`,
  boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`,
  fontSize: `14px`,
  outline: `none`,
  textOverflow: `ellipses`,
};

const options = {
    componentRestrictions: {
        country: "de",
    },
};

export const Map = () =>{
  return (
         <GoogleMap>
              <Autocomplete
                  controlPosition={ google.maps.ControlPosition.TOP_CENTER}
                  inputPlaceholder="Search on the map..."
                  inputStyle={INPUT_STYLE}
                  inputClassName="google-autocomplete-input"
                  options={options}
             />
      </GoogleMap>
  );
}

@gergely-ujvari gergely-ujvari force-pushed the introduce-autocomplete-support branch from 27c324a to 44950ac Compare February 9, 2018 10:53
@gergely-ujvari gergely-ujvari force-pushed the introduce-autocomplete-support branch from 44950ac to 806b155 Compare February 9, 2018 10:54
@djnsu
Copy link
Copy Markdown

djnsu commented Feb 14, 2018

Cool! Can anybody merge this into the project?

@neocotic
Copy link
Copy Markdown

neocotic commented Apr 6, 2018

I agree that we need this. I really want Autocomplete. In fact, I started using this library to rewrite an app I had previously written in react that is primarily focused around the Autocomplete control. However, I'm not sure sure that this solution is even working. I've copied the code locally and I'm getting errors. It also seems that you're creating an input element. Could this not be specified as a child?

I would love for this to work. Perhaps I'm missing something.

@neocotic
Copy link
Copy Markdown

neocotic commented Apr 6, 2018

I wrote the following in my local project. Granted, I'm mostly unfamiliar with the structure of this project and I've very much just started playing around with React, but it seems to work for me:

/* global google */

import canUseDOM from 'can-use-dom';
import invariant from 'invariant';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

import {
  construct,
  componentDidMount,
  componentDidUpdate,
  componentWillUnmount,
} from 'react-google-maps/lib/utils/MapChildHelper';

import { MAP } from 'react-google-maps/lib/constants';

const AUTO_COMPLETE = '__SECRET_AUTO_COMPLETE_DO_NOT_USE_OR_YOU_WILL_BE_FIRED';

/**
 * A wrapper around `google.maps.places.Autocomplete` on the map
 *
 * @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#Autocomplete
 */
export class Autocomplete extends PureComponent {
  static propTypes = {
    /**
     * Where to put `<Autocomplete>` inside a `<GoogleMap>`
     *
     * @example google.maps.ControlPosition.TOP_LEFT
     * @type number
     */
    controlPosition: PropTypes.number,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    defaultBounds: PropTypes.any,

    /**
     * @type ComponentRestrictions
     */
    defaultComponentRestrictions: PropTypes.any,

    /**
     * @type AutocompleteOptions
     */
    defaultOptions: PropTypes.any,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    bounds: PropTypes.any,

    /**
     * @type ComponentRestrictions
     */
    componentRestrictions: PropTypes.any,

    /**
     * @type AutocompleteOptions
     */
    options: PropTypes.any,

    /**
     * function
     */
    onPlaceChanged: PropTypes.func,
  }

  static contextTypes = {
    [MAP]: PropTypes.object,
  }

  state = {
    [AUTO_COMPLETE]: null,
  }

  componentWillMount() {
    if (!canUseDOM || this.containerElement) {
      return;
    }
    invariant(google.maps.places, 'Did you include "libraries=places" in the URL?');
    this.containerElement = document.createElement('div');
    this.handleRenderChildToContainerElement();
    if (React.version.match(/^16/)) {
      return;
    }
    this.handleInitializeAutocomplete();
  }

  componentDidMount() {
    let autocomplete = this.state[AUTO_COMPLETE];
    if (React.version.match(/^16/)) {
      autocomplete = this.handleInitializeAutocomplete();
    }
    componentDidMount(this, autocomplete, eventMap);
    this.handleMountAtControlPosition();
  }

  componentWillUpdate(nextProp) {
    if (this.props.controlPosition !== nextProp.controlPosition) {
      this.handleUnmountAtControlPosition();
    }
  }

  componentDidUpdate(prevProps) {
    componentDidUpdate(this, this.state[AUTO_COMPLETE], eventMap, updaterMap, 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;
    }
  }

  handleInitializeAutocomplete() {
    /*
     * @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#Autocomplete
     */
    const autocomplete = new google.maps.places.Autocomplete(this.containerElement.querySelector('input'),
        this.props.options);
    construct(Autocomplete.propTypes, updaterMap, this.props, autocomplete);
    this.setState({
      [AUTO_COMPLETE]: autocomplete,
    });
    return autocomplete;
  }

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

  /**
   * Returns the bounds to which query predictions are biased.
   * @type LatLngBounds
   * @public
   */
  getBounds() {
    return this.state[AUTO_COMPLETE].getBounds();
  }

  /**
   * Returns the query selected by the user, or `null` if no place has been found yet, to be used with `place_changed` event.
   * @type PlaceResultnullplaces_changed
   * @public
   */
  getPlace() {
    return this.state[AUTO_COMPLETE].getPlace();
  }
}

export default Autocomplete;

const isValidControlPosition = _.isNumber;

const eventMap = {
  onPlaceChanged: 'place_changed'
};

const updaterMap = {
  bounds(instance, bounds) {
    instance.setBounds(bounds);
  }
};

I might have missed somethings, but it's based almost completely on the existing SearchBox component.

@oshalygin
Copy link
Copy Markdown
Collaborator

I'll merge this over the weekend, let me just test this out locally first.

@oshalygin
Copy link
Copy Markdown
Collaborator

This is great we'll merge this once I sync with Tom and we can push it to the upstream

@stenrdj
Copy link
Copy Markdown

stenrdj commented Jul 17, 2018

Hello @oshalygin , this feature is so important , i wish if you could push it to public soon as possible . any update about this feature please?

@bidva
Copy link
Copy Markdown

bidva commented Aug 13, 2018

It is nice to have same way for StandaloneSearchBox as well.

@BartoGabriel
Copy link
Copy Markdown

Will this feature be incorporated into the project?

@tanmoy-ono
Copy link
Copy Markdown

tanmoy-ono commented Sep 21, 2018

Please add this feature. Country restriction is very important. It will be great if incorporated into StandaloneSearchBox.

@tanmoy-ono
Copy link
Copy Markdown

I wrote the following in my local project. Granted, I'm mostly unfamiliar with the structure of this project and I've very much just started playing around with React, but it seems to work for me:

/* global google */

import canUseDOM from 'can-use-dom';
import invariant from 'invariant';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

import {
  construct,
  componentDidMount,
  componentDidUpdate,
  componentWillUnmount,
} from 'react-google-maps/lib/utils/MapChildHelper';

import { MAP } from 'react-google-maps/lib/constants';

const AUTO_COMPLETE = '__SECRET_AUTO_COMPLETE_DO_NOT_USE_OR_YOU_WILL_BE_FIRED';

/**
 * A wrapper around `google.maps.places.Autocomplete` on the map
 *
 * @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#Autocomplete
 */
export class Autocomplete extends PureComponent {
  static propTypes = {
    /**
     * Where to put `<Autocomplete>` inside a `<GoogleMap>`
     *
     * @example google.maps.ControlPosition.TOP_LEFT
     * @type number
     */
    controlPosition: PropTypes.number,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    defaultBounds: PropTypes.any,

    /**
     * @type ComponentRestrictions
     */
    defaultComponentRestrictions: PropTypes.any,

    /**
     * @type AutocompleteOptions
     */
    defaultOptions: PropTypes.any,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    bounds: PropTypes.any,

    /**
     * @type ComponentRestrictions
     */
    componentRestrictions: PropTypes.any,

    /**
     * @type AutocompleteOptions
     */
    options: PropTypes.any,

    /**
     * function
     */
    onPlaceChanged: PropTypes.func,
  }

  static contextTypes = {
    [MAP]: PropTypes.object,
  }

  state = {
    [AUTO_COMPLETE]: null,
  }

  componentWillMount() {
    if (!canUseDOM || this.containerElement) {
      return;
    }
    invariant(google.maps.places, 'Did you include "libraries=places" in the URL?');
    this.containerElement = document.createElement('div');
    this.handleRenderChildToContainerElement();
    if (React.version.match(/^16/)) {
      return;
    }
    this.handleInitializeAutocomplete();
  }

  componentDidMount() {
    let autocomplete = this.state[AUTO_COMPLETE];
    if (React.version.match(/^16/)) {
      autocomplete = this.handleInitializeAutocomplete();
    }
    componentDidMount(this, autocomplete, eventMap);
    this.handleMountAtControlPosition();
  }

  componentWillUpdate(nextProp) {
    if (this.props.controlPosition !== nextProp.controlPosition) {
      this.handleUnmountAtControlPosition();
    }
  }

  componentDidUpdate(prevProps) {
    componentDidUpdate(this, this.state[AUTO_COMPLETE], eventMap, updaterMap, 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;
    }
  }

  handleInitializeAutocomplete() {
    /*
     * @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#Autocomplete
     */
    const autocomplete = new google.maps.places.Autocomplete(this.containerElement.querySelector('input'),
        this.props.options);
    construct(Autocomplete.propTypes, updaterMap, this.props, autocomplete);
    this.setState({
      [AUTO_COMPLETE]: autocomplete,
    });
    return autocomplete;
  }

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

  /**
   * Returns the bounds to which query predictions are biased.
   * @type LatLngBounds
   * @public
   */
  getBounds() {
    return this.state[AUTO_COMPLETE].getBounds();
  }

  /**
   * Returns the query selected by the user, or `null` if no place has been found yet, to be used with `place_changed` event.
   * @type PlaceResultnullplaces_changed
   * @public
   */
  getPlace() {
    return this.state[AUTO_COMPLETE].getPlace();
  }
}

export default Autocomplete;

const isValidControlPosition = _.isNumber;

const eventMap = {
  onPlaceChanged: 'place_changed'
};

const updaterMap = {
  bounds(instance, bounds) {
    instance.setBounds(bounds);
  }
};

I might have missed somethings, but it's based almost completely on the existing SearchBox component.

Hi,
Tried to use this as a library just like StandaloneSearchBox, but giving the following error:

Failed to compile.

./node_modules/react-google-maps/lib/components/places/Autocomplete.js
Module parse failed: Unexpected token (27:19)
You may need an appropriate loader to handle this file type.
| /
| export class Autocomplete extends PureComponent {
| static propTypes = {
| /
*
| * Where to put <Autocomplete> inside a <GoogleMap>

@neocotic
Copy link
Copy Markdown

@tanmoy-ono What are you using to compile? This looks more like your project setup rather than an issue with the code itself.

@tanmoy-ono
Copy link
Copy Markdown

@neocotic Following code I am using for StandaloneSearchBox, tried same way.

export const AutoCompleteSearchBox = compose(
    withProps({
      googleMapURL:googleMapUrl,
      loadingElement: <div style={{ height: `100%` }} />,
      containerElement: <div style={{ height: `400px`, top:'3px' }} />,
    }),
    lifecycle({
       componentWillMount() {
         const refs = {}
         this.setState({
           types: ['(regions)'],
           componentRestrictions: {country: "bd"},
           onSearchBoxMounted:ref =>{ refs.searchBox = ref; },
           onPlacesChanged:()=>{ 
             const places = refs.searchBox.getPlaces(); 
             this.props.onPlacesChanged(places); 
           },
         })
         const options = {
          types: ['(regions)'],
          componentRestrictions:{ country: 'bd' }
          }
       },
     }),
     withScriptjs  
   )(props =>
     <div data-standalone-searchbox="">
       <StandaloneSearchBox 
            ref={props.onSearchBoxMounted} 
            bounds={props.bounds} 
            onPlacesChanged={props.onPlacesChanged} 
            controlPosition={ window.google.maps.ControlPosition.TOP_LEFT}
        >
       <TextField 
            className={props.inputClass}
            placeholder={props.inputPlaceholder}
            label={props.inputLabel}
            name={props.inputName} 
            value={props.inputValue}
            onChange={props.inputOnChange}
            helperText={props.inputHelperText}
            error={props.inputError}
        />
       </StandaloneSearchBox>
     </div>
   );

@bidva
Copy link
Copy Markdown

bidva commented Jan 3, 2019

Guys do you have plan to merge this anytime soon?

@JustFly1984
Copy link
Copy Markdown

@bidva this lib is unmaintained for a year, please consider new replacement react-google-maps-api https://github.com/JustFly1984/react-google-maps-api https://www.npmjs.com/package/react-google-maps-api

@bidva
Copy link
Copy Markdown

bidva commented Jan 3, 2019

@bidva this lib is unmaintained for a year, please consider new replacement react-google-maps-api https://github.com/JustFly1984/react-google-maps-api https://www.npmjs.com/package/react-google-maps-api

Thanks for your suggestion but as much as I see this package doesn't support the single search box('Autocomplete'). Do you have a plan to add any time soon?

@JustFly1984
Copy link
Copy Markdown

JustFly1984 commented Jan 3, 2019

@ gergely-ujvari @bidva issues and pull requests are welcome https://github.com/JustFly1984/react-google-maps-api/issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants