Giners / mui-places-autocomplete

Google Material Design (Material-UI) styled React component using Google Maps Places Autocomplete
MIT License
34 stars 26 forks source link

Latitude / Longitude data in returned object? #34

Closed charliedotau closed 6 years ago

charliedotau commented 6 years ago

Hi there,

Love the component; it meets all but one of my requirements, as I will explain. I've been looking for something to replace React GeoSuggest. Feature wise React GeoSuggest is perfect, but its just not MUI friendly.

Latitude / Longitude?

It's just come to my attention that mui-places-autocomplete does not include Lat / Lng data for the selected suggestion. This is a deal-breaker for me; Lat Lng is really the only data I actually need.

React GeoSuggest provides this data, which is super useful. I wonder if this explains why their component assumes the user's API Key also has access to the Google Maps Geocoding API?

Would you consider adding such a feature?

Love your work. Thanks

Giners commented 6 years ago

Hi @charliedotau,

Thanks for the positive feedback! :smile: I would be more than happy to add geocoding features into <MUIPlacesAutocomplete> for you. 😄

Honestly when I started <MUIPlacesAutocomplete> all I needed as well was the lat/long of places that people selected/searched. I have since implemented that functionality into my project that uses <MUIPlacesAutocomplete> but haven't had time to add it to <MUIPlacesAutocomplete> so that it is natively supported.

Can I gather any additional requirements from you, if you have any, around lat/long? I will see if they are reasonable to include with the requirements of my current implementation of how I get lat/long from results returned by <MUIPlacesAutocomplete>.

It may take me a week plus to get this implemented in <MUIPlacesAutocomplete> as I have been swamped with work and two little kids lately. 😄 But in the mean time here is my implementation of how to get geocoding working with <MUIPlacesAutocomplete>. Maybe it can be used as a stopgap until the functionality is implemented natively in <MUIPlacesAutocomplete> (or you can just use it if you like and don't want to wait 😄).

export class CreateListingFormLocation extends React.Component {
  // Helper method to parse the results of a 'geocode()' request so that an address for a listing
  // can be extracted. We use the results of the 'geocode()' request vs. the selected suggestion as
  // the selected suggestion can't reliably guarantee that all of the components to compose an
  // address will be present whereas the geocode result ought to (hopefully).
  //
  // Returns a non-null object with the parsed 'geocode()' request results if an address can be
  // composed from it. If an address can't be composed then 'null' is returned.
  static parseGeocodeResults(results) {
    // Reduce the geocode results to a single set of details about the listing
    return results.reduce(
      // The Google Maps JavaScript API returns objects with property identifiers that are snake
      // cased. We will disable ESLint errors for this and in other places in the function to avoid
      // it yelling at us.
      // eslint-disable-next-line camelcase
      (listingDetails, { address_components, geometry }) => {
        // In the event that there could be multiple results that would allow us to compose our
        // listings address we will just use the first result. Although I'm unsure of if and when
        // this could actually happen...
        if (listingDetails) {
          return listingDetails
        }

        const reducedDetails = address_components.reduce((
          { streetNumber, route, city, state, zip, country },
          // eslint-disable-next-line camelcase
          { long_name, types },
        ) => {
          const accumulatedDetails = {
            streetNumber,
            route,
            city,
            state,
            zip,
            country,
          }

          // Just make an new var that is camelCased so we don't have to deal with ESLint errors
          // when assigning 'long_name' to the listing detail property
          // eslint-disable-next-line camelcase
          const longName = long_name

          // The main address is composed of a route for sure but possibly a street # as well
          if (!streetNumber && types.includes('street_number')) {
            accumulatedDetails.streetNumber = longName
          } else if (!route && types.includes('route')) {
            accumulatedDetails.route = longName
          } else if (
            !city &&
            types.includes('locality') &&
            types.includes('political')
          ) {
            accumulatedDetails.city = longName
          } else if (
            !state &&
            types.includes('administrative_area_level_1') &&
            types.includes('political')
          ) {
            accumulatedDetails.state = longName
          } else if (!zip && types.includes('postal_code')) {
            accumulatedDetails.zip = longName
          } else if (
            !country &&
            types.includes('country') &&
            types.includes('political')
          ) {
            accumulatedDetails.country = longName
          }

          return accumulatedDetails
        }, {})

        // Check that we have all the details necessary to construct an address for our listing. We
        // don't need a street number specificially for the listing but if there is one be sure to
        // use it.
        const {
          streetNumber,
          route,
          city,
          state,
          zip,
          country,
        } = reducedDetails

        if (route && city && state && zip && country) {
          return {
            addressMain: streetNumber ? `${streetNumber} ${route}` : route,
            city,
            state,
            zip,
            country,
            cords: {
              lat: geometry.location.lat(),
              long: geometry.location.lng(),
            },
          }
        }

        // If we got to this point we haven't yet found a geocode result that provides all the
        // necessary details for a listing. Return 'null' which is the inital value of our accumulator
        // to ensure that our conditional logic at the beginning of the reducing function works (i.e.
        // parses the next geocode result).
        return null
      },
      null,
    )
  }

  constructor() {
    super()

    this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
  }

  // Disable the ESLint rule camelcase on the next line as the suggestions returned from the Google
  // Maps service have properties that are snake cased
  // eslint-disable-next-line camelcase
  onSuggestionSelected({ place_id }) {
    const { change, geocoder } = this.props

    return new Promise((resolve, reject) => {
      geocoder.geocode({ placeId: place_id }, (results, status) => {
        if (status !== window.google.maps.GeocoderStatus.OK) {
          reject(
            new Error(
              `Unable to query for listing details - response status: ${status}`,
            ),
          )

          return
        }

        const listingDetails = CreateListingFormLocation.parseGeocodeResults(
          results,
        )

        if (listingDetails === null) {
          reject(new Error(`Couldn't compose details of the listing`))
          return
        }

        resolve(listingDetails)
      })
    }).then(({ addressMain, city, state, zip, country, cords }) => {
      // charliedotaus business logic here :)
    })
  }

  render() {
    const { handleSubmit } = this.props

    return (
      <CreateListingFormLocationView
        handleSubmit={handleSubmit}
        onSuggestionSelected={this.onSuggestionSelected}
      />
    )
  }
}

Two things to note about the above code snippet. The Google Geocoding API is passed as a prop to this component. I used the pattern so I didn't render <MUIPlacesAutocomplete>/the lat and long logic before the API was downloaded and available for use. If you want you can use this logic to set it as state directly in the component (logic might have to change for React 16.3):

  componentDidMount() {
    // After the component is mounted it is safe to create a new instance of the Geocoder client.
    // That's because at this point the Google Maps JavaScript API has been loaded. Also if we do it
    // before the component is mounted (i.e. in 'componentWillMount()') we won't be safe to render
    // on the server (SSR) as the 'window' object isn't available.
    const geocoder = new window.google.maps.Geocoder()

    // We set the state in the 'componentDidMount()' lifecycle method with care as this pattern can
    // lead to performance issues. We do it here though so that we can provide logic to not render
    // our form which depends on the Geocoder client until it is actually available to use.
    //
    // We disable the ESLint rule 'react/no-did-mount-set-state' as we are aware of the performance
    // issues that setting the state can lead too but the React docs state that any initialization
    // that requires DOM nodes ought to go here:
    // https://reactjs.org/docs/react-component.html#componentdidmount
    // eslint-disable-next-line react/no-did-mount-set-state
    this.setState({ geocoder })
  }

Also I expose the <MUIPlacesAutocomplete> component in a view component (<CreateListingFormLocationView>) where the callback is just passed along to it.

charliedotau commented 6 years ago

thanks @Giners. Wonderful! Thank you.

My requirement is simple: i use the autocompletion component to enable people to search for a location. On selection my map component jumps to to that location on the map. To do that I only need the Lat and Lng, that I then pass to my map component.

Happy to wait.

Thanks again.
Charlie

Giners commented 6 years ago

Hey @charliedotau,

I found some unexpected time and got a head start on adding the geocoding functionality. If you want you can checkout the diff on commit 2e7e134886ea0c0edab050c92e1f75df5bd02710 made to the feature/geocode-suggestion branch or you can wait for the PR/merge to master.

I plan on providing geocoding utility functions to enable consumers of <MUIPlacesAutocomplete> to geocode a selected suggestion at their discretion.

I'll write up some docs/demos when I have time next and then get the PR up.

charliedotau commented 6 years ago

hi @Giners

Looks great. I'll wait for the merge to master.

Thanks again. Love it.

Giners commented 6 years ago

Hey @charliedotau,

PR #36 brought in the geocoding utility functions. Here is demo code showing how to use them: https://github.com/Giners/mui-places-autocomplete/blob/master/demo/DemoGeocodeLatLong.jsx

You also need to enable the Google Maps Geocoding API to make use of them. This is all documented in the README.md. Let me know what you think/if you have any questions.

charliedotau commented 6 years ago

wonderful, thanks @Giners. I hope to give this a try in the next day or so.

Thanks so much, for a great component.

charliedotau commented 6 years ago

just confirming @Giners this is working really nicely. Thanks again.

Giners commented 6 years ago

@charliedotau Glad to hear! :)

Ill look into #35 in the coming week if I have time.