Google maps Places API V3 autocomplete - select first option on enter
Asked Answered
J

20

77

I have successfuly implemented Google Maps Places V3 autocomplete feature on my input box as per http://web.archive.org/web/20120225114154/http://code.google.com:80/intl/sk-SK/apis/maps/documentation/javascript/places.html. It works nicely, however I would love to know how can I make it select the first option from the suggestions when a user presses enter. I guess I would need some JS magic, but I am very much new to JS and don't know where to start.

Jinajingle answered 23/10, 2011 at 9:54 Comment(1)
nice question. i was thinking about this today. my only worry is forcing the user to do anything they maybe wont want to do. Some big websites don't force selection of the first option is there are more than one option visible, they only force when there is only one option showing, which seems better to me.Monosepalous
D
47

I had the same issue when implementing autocomplete on a site I worked on recently. This is the solution I came up with:

$("input").focusin(function () {
    $(document).keypress(function (e) {
        if (e.which == 13) {
            var firstResult = $(".pac-container .pac-item:first").text();

            var geocoder = new google.maps.Geocoder();
            geocoder.geocode({"address":firstResult }, function(results, status) {
                if (status == google.maps.GeocoderStatus.OK) {
                    var lat = results[0].geometry.location.lat(),
                        lng = results[0].geometry.location.lng(),
                        placeName = results[0].address_components[0].long_name,
                        latlng = new google.maps.LatLng(lat, lng);

                        $(".pac-container .pac-item:first").addClass("pac-selected");
                        $(".pac-container").css("display","none");
                        $("#searchTextField").val(firstResult);
                        $(".pac-container").css("visibility","hidden");

                    moveMarker(placeName, latlng);

                }
            });
        } else {
            $(".pac-container").css("visibility","visible");
        }

    });
});

http://jsfiddle.net/dodger/pbbhH/

Dobsonfly answered 16/11, 2011 at 19:3 Comment(5)
The problem with this method is it doesn't return the same address as the first autocomplete result. It performs a geocode on the display address of the first autocomplete result which may be an entirely different address. For example, the first autocomplete result for input "jfk" is the correct address (JFK Access Road, New York, NY, United States) but the geocode result for the address "JFK Airport, New York, NY" (which is the display address of the first autocomplete) will produce the address of what appears to be a hotel (144-02 135th Ave, Queens, NY 11436, USA).Lapin
In my case, I did the following: var firstResult = $(".pac-container .pac-item:first").text();var stringMatched = $(".pac-container .pac-item:first").find(".pac-item-query").text(); firstResult = firstResult.replace(stringMatched, stringMatched + " "); And that solved the issueSect
Can't you just do .click() on that first item to trigger the same processing as you would.Perky
Can you please tell me how can i add space after ".pac-item-query" because now when i click on ENTER the space is removedMatelote
Thank you! thank you!! thank you!!! I didn't know you can use Geocoder.geocode() to get the address_components. You've saved me hours of work! Thank you again!Orson
E
178

Here is a solution that does not make a geocoding request that may return an incorrect result: http://jsfiddle.net/amirnissim/2D6HW/

It simulates a down-arrow keypress whenever the user hits return inside the autocomplete field. The event is triggered before the return event so it simulates the user selecting the first suggestion using the keyboard.

Here is the code (tested on Chrome and Firefox) :

<script src='https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'></script>
<script src="https://maps.googleapis.com/maps/api/js?sensor=false&libraries=places"></script>
<script>
    var pac_input = document.getElementById('searchTextField');

    (function pacSelectFirst(input) {
        // store the original event binding function
        var _addEventListener = (input.addEventListener) ? input.addEventListener : input.attachEvent;

        function addEventListenerWrapper(type, listener) {
            // Simulate a 'down arrow' keypress on hitting 'return' when no pac suggestion is selected,
            // and then trigger the original listener.
            if (type == "keydown") {
                var orig_listener = listener;
                listener = function(event) {
                    var suggestion_selected = $(".pac-item-selected").length > 0;
                    if (event.which == 13 && !suggestion_selected) {
                        var simulated_downarrow = $.Event("keydown", {
                            keyCode: 40,
                            which: 40
                        });
                        orig_listener.apply(input, [simulated_downarrow]);
                    }

                    orig_listener.apply(input, [event]);
                };
            }

            _addEventListener.apply(input, [type, listener]);
        }

        input.addEventListener = addEventListenerWrapper;
        input.attachEvent = addEventListenerWrapper;

        var autocomplete = new google.maps.places.Autocomplete(input);

    })(pac_input);
</script>
Endoscope answered 28/7, 2012 at 17:6 Comment(0)
D
47

I had the same issue when implementing autocomplete on a site I worked on recently. This is the solution I came up with:

$("input").focusin(function () {
    $(document).keypress(function (e) {
        if (e.which == 13) {
            var firstResult = $(".pac-container .pac-item:first").text();

            var geocoder = new google.maps.Geocoder();
            geocoder.geocode({"address":firstResult }, function(results, status) {
                if (status == google.maps.GeocoderStatus.OK) {
                    var lat = results[0].geometry.location.lat(),
                        lng = results[0].geometry.location.lng(),
                        placeName = results[0].address_components[0].long_name,
                        latlng = new google.maps.LatLng(lat, lng);

                        $(".pac-container .pac-item:first").addClass("pac-selected");
                        $(".pac-container").css("display","none");
                        $("#searchTextField").val(firstResult);
                        $(".pac-container").css("visibility","hidden");

                    moveMarker(placeName, latlng);

                }
            });
        } else {
            $(".pac-container").css("visibility","visible");
        }

    });
});

http://jsfiddle.net/dodger/pbbhH/

Dobsonfly answered 16/11, 2011 at 19:3 Comment(5)
The problem with this method is it doesn't return the same address as the first autocomplete result. It performs a geocode on the display address of the first autocomplete result which may be an entirely different address. For example, the first autocomplete result for input "jfk" is the correct address (JFK Access Road, New York, NY, United States) but the geocode result for the address "JFK Airport, New York, NY" (which is the display address of the first autocomplete) will produce the address of what appears to be a hotel (144-02 135th Ave, Queens, NY 11436, USA).Lapin
In my case, I did the following: var firstResult = $(".pac-container .pac-item:first").text();var stringMatched = $(".pac-container .pac-item:first").find(".pac-item-query").text(); firstResult = firstResult.replace(stringMatched, stringMatched + " "); And that solved the issueSect
Can't you just do .click() on that first item to trigger the same processing as you would.Perky
Can you please tell me how can i add space after ".pac-item-query" because now when i click on ENTER the space is removedMatelote
Thank you! thank you!! thank you!!! I didn't know you can use Geocoder.geocode() to get the address_components. You've saved me hours of work! Thank you again!Orson
Q
42

A working answer for 2020.

I've combined the best answers on this page and written it in straightforward ES6. No jQuery, 2nd API request, or IIFE needed.

Basically, we simulate a ↓ (down-arrow) keypress whenever the user hits return inside the autocomplete field.

First, assuming in your HTML you have something like <input id="address-field">, set up the identification of your address field like this:

const field = document.getElementById('address-field') 

const autoComplete = new google.maps.places.Autocomplete(field)

autoComplete.setTypes(['address'])

Then add this on the next line:

enableEnterKey(field)

And then elsewhere in your script, to keep this functionality separate in your code if you'd like to, add the function:

  function enableEnterKey(input) {

    /* Store original event listener */
    const _addEventListener = input.addEventListener

    const addEventListenerWrapper = (type, listener) => {
      if (type === 'keydown') {
        /* Store existing listener function */
        const _listener = listener
        listener = (event) => {
          /* Simulate a 'down arrow' keypress if no address has been selected */
          const suggestionSelected = document.getElementsByClassName('pac-item-selected').length
          if (event.key === 'Enter' && !suggestionSelected) {
            const e = new KeyboardEvent('keydown', { 
              key: 'ArrowDown', 
              code: 'ArrowDown', 
              keyCode: 40, 
            })
            _listener.apply(input, [e])
          }
          _listener.apply(input, [event])
        }
      }
      _addEventListener.apply(input, [type, listener])
    }

    input.addEventListener = addEventListenerWrapper
  }

You should be good to go. Essentially, the function captures each keypress in the input field and if it's an enter, simulates instead a down-arrow keypress. It also stores and rebinds listeners and events to maintain all functionality of your Google Maps Autocomplete().

With thanks to earlier answers for much of this code, particular amirnissim and Alexander Schwarzman.

Quarterage answered 3/4, 2018 at 1:14 Comment(13)
You forgot to declare enableEnterKey as a function. function enableEnterKey(input) {} Other than that it works flawlessly, thank you!Shanty
Added that, thanks. Could also use const enableEnterKey = (input) => { if you want to use an arrow function. It depends on whether you're using classes and constructors or not, but either way it's more complete now.Quarterage
Tweaked to handle tab keys also, by adding a range, i.e.if (event.which >= 9 && event.which <= 13 && !suggestion_selected) {Pancreatotomy
@Pancreatotomy That's a nice addition if you want additional keys to workQuarterage
Your solution is for 2018, why do we still need attachEvent?Oatmeal
And plus why do you use .which? It should be .key = "Enter" and e.key="Down arrow". "If you are staying in vanilla Javascript, please note keyCode is now deprecated and will be dropped:" https://mcmap.net/q/54415/-keycode-vs-whichOatmeal
Good point @Shadrix, I've removed the IE8-only attachEvent.Quarterage
Best answer hereErin
@Shadrix And now updated to modern browser key identifiers as well.Quarterage
This was not working for me at the beginning. I changed the way the mocked keydown event is created and the solution worked as a charm: const e = new KeyboardEvent("keydown", { key: "ArrowDown", code: "ArrowDown", keyCode: 40 });Deenadeenya
@Deenadeenya Thanks, added that improvement. Agreed that's a more modern and universal way of mocking the event.Quarterage
In Typscript I had to add this Object.defineProperty(e, 'keyCode', { get: () => 40, }); because keyCode is not defined on KeyboardEventInit.Motor
To enable this event onBlur I've stored the _listener from the keyDown and applied it to blur.Oran
K
23

Here is an example of a real, non-hacky, solution. It doesn't use any browser hacks etc, just methods from the public API provided by Google and documented here: Google Maps API

The only downside is that additional requests to Google are required if the user doesn't select an item from the list. The upside is that the result will always be correct as the query is performed identically to the query inside the AutoComplete. Second upside is that by only using public API methods and not relying on the internal HTML structure of the AutoComplete widget, we can be sure that our product won't break if Google maps changes.

var input = /** @type {HTMLInputElement} */(document.getElementById('searchTextField'));
var autocomplete = new google.maps.places.Autocomplete(input);  
// These are my options for the AutoComplete
autocomplete.setTypes(['(cities)']);
autocomplete.setComponentRestrictions({'country': 'es'});

google.maps.event.addListener(autocomplete, 'place_changed', function() {
    result = autocomplete.getPlace();
    if(typeof result.address_components == 'undefined') {
        // The user pressed enter in the input 
        // without selecting a result from the list
        // Let's get the list from the Google API so that
        // we can retrieve the details about the first result
        // and use it (just as if the user had actually selected it)
        autocompleteService = new google.maps.places.AutocompleteService();
        autocompleteService.getPlacePredictions(
            {
                'input': result.name,
                'offset': result.name.length,
                // I repeat the options for my AutoComplete here to get
                // the same results from this query as I got in the 
                // AutoComplete widget
                'componentRestrictions': {'country': 'es'},
                'types': ['(cities)']
            },
            function listentoresult(list, status) {
                if(list == null || list.length == 0) {
                    // There are no suggestions available.
                    // The user saw an empty list and hit enter.
                    console.log("No results");
                } else {
                    // Here's the first result that the user saw
                    // in the list. We can use it and it'll be just
                    // as if the user actually selected it
                    // themselves. But first we need to get its details
                    // to receive the result on the same format as we
                    // do in the AutoComplete.
                    placesService = new google.maps.places.PlacesService(document.getElementById('placesAttribution'));
                    placesService.getDetails(
                        {'reference': list[0].reference},
                        function detailsresult(detailsResult, placesServiceStatus) {
                            // Here's the first result in the AutoComplete with the exact
                            // same data format as you get from the AutoComplete.
                            console.log("We selected the first item from the list automatically because the user didn't select anything");
                            console.log(detailsResult);
                        }
                    );
                }
            }
        );
    } else {
        // The user selected a result from the list, we can 
        // proceed and use it right away
        console.log("User selected an item from the list");
        console.log(result);
    }
});
Kai answered 6/7, 2013 at 16:48 Comment(3)
your solution is not correct as it breaks when place_changed event is not fired. That is when none option is selected. that means when user presses enter or tab keys or even click elsewhere than in an option.Contemptible
Has anyone found a way to update the 'place' of the autocomplete object with this implementation? Currently, if this is used autocomplete.getPlace() results in undefined.Chalco
To answer Ulad's question, simply move the AutocompleteService code outside of the 'place_changed' event handler, and instead set state that 'place_changed' will set to true if the user selects a value. If the state has not changed when expected (I'm using on bur on the input field), then fire the AutocompleteService with input as search key. The main problem I've found with using the service however is that the results returned are not guaranteed to be the same or in the same order as the completion API, so you must implement your own prediction UI with the service resultsTass
S
10

It seems there is a much better and clean solution: To use google.maps.places.SearchBox instead of google.maps.places.Autocomplete. A code is almost the same, just getting the first from multiple places. On pressing the Enter the the correct list is returned - so it runs out of the box and there is no need for hacks.

See the example HTML page:

http://rawgithub.com/klokan/8408394/raw/5ab795fb36c67ad73c215269f61c7648633ae53e/places-enter-first-item.html

The relevant code snippet is:

var searchBox = new google.maps.places.SearchBox(document.getElementById('searchinput'));

google.maps.event.addListener(searchBox, 'places_changed', function() {
  var place = searchBox.getPlaces()[0];

  if (!place.geometry) return;

  if (place.geometry.viewport) {
    map.fitBounds(place.geometry.viewport);
  } else {
    map.setCenter(place.geometry.location);
    map.setZoom(16);
  }
});

The complete source code of the example is at: https://gist.github.com/klokan/8408394

Skimp answered 13/1, 2014 at 21:25 Comment(2)
it works exactly the same as Autocomplete for pressing Enter so... it doesn't work.Hochman
It should be noted that you can't set componentRestrictions options for SearchBox. So if you need to restrict the suggestions to a specific country this is not an alternative.Alchemize
M
9

For Google Places Autocomplete V3, the best solution for this is two API requests.

Here is the fiddle

The reason why none of the other answers sufficed is because they either used jquery to mimic events (hacky) or used either Geocoder or Google Places Search box which does not always match autocomplete results. Instead, what we will do is is uses Google's Autocomplete Service as detailed here with only javascript (no jquery)

Below is detailed the most cross browser compatible solution using native Google APIs to generate the autocomplete box and then rerun the query to select the first option.

<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=places&language=en"></script>

Javascript

// For convenience, although if you are supporting IE8 and below
// bind() is not supported
var $ = document.querySelector.bind(document);

function autoCallback(predictions, status) {
    // *Callback from async google places call
    if (status != google.maps.places.PlacesServiceStatus.OK) {
        // show that this address is an error
        pacInput.className = 'error';
        return;
    }

    // Show a successful return
    pacInput.className = 'success';
    pacInput.value = predictions[0].description;
}


function queryAutocomplete(input) {
    // *Uses Google's autocomplete service to select an address
    var service = new google.maps.places.AutocompleteService();
    service.getPlacePredictions({
        input: input,
        componentRestrictions: {
            country: 'us'
        }
    }, autoCallback);
}

function handleTabbingOnInput(evt) {
    // *Handles Tab event on delivery-location input
    if (evt.target.id == "pac-input") {
        // Remove active class
        evt.target.className = '';

        // Check if a tab was pressed
        if (evt.which == 9 || evt.keyCode == 9) {
            queryAutocomplete(evt.target.value);
        }
    }
}

// ***** Initializations ***** //
// initialize pac search field //
var pacInput = $('#pac-input');
pacInput.focus();

// Initialize Autocomplete
var options = {
    componentRestrictions: {
        country: 'us'
    }
};
var autocomplete = new google.maps.places.Autocomplete(pacInput, options);
// ***** End Initializations ***** //

// ***** Event Listeners ***** //
google.maps.event.addListener(autocomplete, 'place_changed', function () {
    var result = autocomplete.getPlace();
    if (typeof result.address_components == 'undefined') {
        queryAutocomplete(result.name);
    } else {
        // returns native functionality and place object
        console.log(result.address_components);
    }
});

// Tabbing Event Listener
if (document.addEventListener) {
    document.addEventListener('keydown', handleTabbingOnInput, false);
} else if (document.attachEvent) { // IE8 and below
    document.attachEvent("onsubmit", handleTabbingOnInput);
}

// search form listener
var standardForm = $('#search-shop-form');
if (standardForm.addEventListener) {
    standardForm.addEventListener("submit", preventStandardForm, false);
} else if (standardForm.attachEvent) { // IE8 and below
    standardForm.attachEvent("onsubmit", preventStandardForm);
}
// ***** End Event Listeners ***** //

HTML

<form id="search-shop-form" class="search-form" name="searchShopForm" action="/impl_custom/index/search/" method="post">
    <label for="pac-input">Delivery Location</label>
        <input id="pac-input" type="text" placeholder="Los Angeles, Manhattan, Houston" autocomplete="off" />
        <button class="search-btn btn-success" type="submit">Search</button>
</form>

The only gripe is that the native implementation returns a different data structure although the information is the same. Adjust accordingly.

Mammalogy answered 22/1, 2015 at 23:35 Comment(3)
Good call on the AutocompleteService API. I've used in conjunction with the blur event to account for mobile users tapping out.Bedizen
That is the cleanest and most reliable answer on this question. If you need some more details about the address like GPS coordinates (and get the same object as the autocomplete), you can make another call to getDetails() on Google.maps.places.PlacesServiceFoghorn
Just a warning: getDetails is expensive. The additional monetary cost is 6 times as high than for Autocomplete itself.Prieto
E
3

Regarding to all your answers, I have created a solution that works perfectly for me.

/**
 * Function that add the google places functionality to the search inputs
 * @private
 */
function _addGooglePlacesInputsAndListeners() {
    var self = this;
    var input = document.getElementById('searchBox');
    var options = {
        componentRestrictions: {country: "es"}
    };

    self.addInputEventListenersToAvoidAutocompleteProblem(input);
    var searchBox = new google.maps.places.Autocomplete(input, options);
    self.addPlacesChangedListener(searchBox, self.SimulatorMapStorage.map);
}

/**
 * A problem exists with google.maps.places.Autocomplete when the user write an address and doesn't selectany options that autocomplete gives him so we have to add some events to the two inputs that we have to simulate the behavior that it should have. First, we get the keydown 13 (Enter) and if it's not a suggested option, we simulate a keydown 40 (keydownArrow) to select the first option that Autocomplete gives. Then, we dispatch the event to complete the request.
 * @param input
 * @private
 */
function _addInputEventListenersToAvoidAutocompleteProblem(input) {
    input.addEventListener('keydown', function(event) {
        if (event.keyCode === 13 && event.which === 13) {
            var suggestion_selected = $(".pac-item-selected").length > 0;
            if (!suggestion_selected) {
                var keyDownArrowEvent = new Event('keydown');
                keyDownArrowEvent.keyCode = 40;
                keyDownArrowEvent.which = keyDownArrowEvent.keyCode;

                input.dispatchEvent(keyDownArrowEvent);
            }
        }
    });
}
<input id="searchBox" class="search-input initial-input" type="text" autofocus>

Hope that it can help to someone. Please, feel free to discuss the best way to do.

Encourage answered 15/11, 2018 at 12:51 Comment(0)
P
2

I just want to write an small enhancement for the answer of amirnissim
The script posted doesn't support IE8, because "event.which" seems to be always empty in IE8.
To solve this problem you just need to additionally check for "event.keyCode":

listener = function (event) {
  if (event.which == 13 || event.keyCode == 13) {
    var suggestion_selected = $(".pac-item.pac-selected").length > 0;
    if(!suggestion_selected){
      var simulated_downarrow = $.Event("keydown", {keyCode:40, which:40})
      orig_listener.apply(input, [simulated_downarrow]);
    }
  }
  orig_listener.apply(input, [event]);
};

JS-Fiddle: http://jsfiddle.net/QW59W/107/

Peso answered 6/12, 2013 at 11:17 Comment(0)
O
2

How about this?

$("input").keypress(function(event) {
  var firstValue = null;
  if (event.keyCode == 13 || event.keyCode == 9) {
    $(event.target).blur();
    if ($(".pac-container .pac-item:first span:eq(3)").text() == "") {
      firstValue = $(".pac-container .pac-item:first .pac-item-query").text();
    } else {
      firstValue = $(".pac-container .pac-item:first .pac-item-query").text() + ", " + $(".pac-container .pac-item:first span:eq(3)").text();
    }
    event.target.value = firstValue;
  } else
    return true;
});
Otocyst answered 15/7, 2014 at 6:41 Comment(0)
S
2

None of these answers seemed to work for me. They'd get the general location but wouldn't actually pan to the actual place I searched for. Within the .pac-item you can actually get just the address (name of place excluded) by selecting $('.pac-item:first').children()[2].textContent

So here is my solution:

$("#search_field").on("keyup", function(e) {
    if(e.keyCode == 13) {
        searchPlaces();
    }
});

function searchPlaces() {
    var $firstResult = $('.pac-item:first').children();
    var placeName = $firstResult[1].textContent;
    var placeAddress = $firstResult[2].textContent;

    $("#search_field").val(placeName + ", " + placeAddress);

    var geocoder = new google.maps.Geocoder();
    geocoder.geocode({"address":placeAddress }, function(results, status) {
        if (status == google.maps.GeocoderStatus.OK) {
            var lat = results[0].geometry.location.lat(),
                lng = results[0].geometry.location.lng(),
                placeName = results[0].address_components[0].long_name,
                latlng = new google.maps.LatLng(lat, lng);

            map.panTo(latlng);
        }
    });
}

I know this question was already answered but figured I'd throw in my 2 cents just in case anyone else was having the same problem as me.

Spiritism answered 12/8, 2014 at 1:21 Comment(1)
$('.pac-item:first') - it's not necessarily the first item in the list that's the one selected by the user unfortunatelyPublea
O
1

@benregn @amirnissim I think the selection error comes from:

var suggestion_selected = $(".pac-item.pac-selected").length > 0;

The class pac-selected should be pac-item-selected, which explains why !suggestion_selected always evaluate to true, causing the incorrect location to be selected when the enter key is pressed after using 'keyup' or 'keydown' to highlight the desired location.

Overland answered 30/10, 2013 at 18:59 Comment(0)
L
1

I did some work around this and now I can force select 1st option from google placces using angular js and angular Autocomplete module.
Thanks to kuhnza
my code

<form method="get" ng-app="StarterApp"  ng-controller="AppCtrl" action="searchresults.html" id="target" autocomplete="off">
   <br/>
    <div class="row">
    <div class="col-md-4"><input class="form-control" tabindex="1" autofocus g-places-autocomplete force-selection="true"  ng-model="user.fromPlace" placeholder="From Place" autocomplete="off"   required>
    </div>
        <div class="col-md-4"><input class="form-control" tabindex="2"  g-places-autocomplete force-selection="true"  placeholder="To Place" autocomplete="off" ng-model="user.toPlace" required>
    </div>
    <div class="col-md-4"> <input class="btn btn-primary"  type="submit" value="submit"></div></div><br /><br/>
    <input class="form-control"  style="width:40%" type="text" name="sourceAddressLat" placeholder="From Place Lat" id="fromLat">
    <input class="form-control"  style="width:40%"type="text" name="sourceAddressLang" placeholder="From Place Long" id="fromLong">
    <input class="form-control"  style="width:40%"type="text" name="sourceAddress" placeholder="From Place City" id="fromCity">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddressLat" placeholder="To Place Lat" id="toLat">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddressLang" placeholder="To Place Long"id="toLong">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddress"placeholder="To Place City" id="toCity">
</form>

Here is a Plunker
Thank you.

Lamellirostral answered 26/7, 2015 at 5:9 Comment(0)
I
1

I investigated this a bit since I have the same Issue. What I did not like about the previous solutions was, that the autocomplete already fired the AutocompleteService to show the predictions. Therefore, the predictions should be somewhere and should not be loaded again.

I found out that the predictions of place inkl. place_id is stored in

Autocomplete.gm_accessors_.place.Kc.l

and you will be able to get a lot of data from the records [0].data. Imho, it's faster and better to get the location by using the place_id instead of address data. This very strange object selection appears not very good to me, tho.

Do you know, if there is a better way to retrieve the first prediction from the autocomplete?

Insubordinate answered 18/12, 2017 at 13:16 Comment(2)
That's very dangerous indeed, as currently the field names have been changed. At this moment, there's an array of prediction objects in autocomplete.gm_accessors_.place.Zc.predictions, where autocomplete is the autocomplete object. The prediction objects have human-readable keys. I wish there was a documented accessor to get at this array. Maybe there is.Prieto
I was forced to do something similar (typescript) let formattedPediction = autocomplete.gm_accessors_?.place?.We?.formattedPrediction;Publea
G
1

Working Solution that listens to if the user has started to navigate down the list with the keyboard rather than triggering the false navigation each time

https://codepen.io/callam/pen/RgzxZB

Here are the important bits

// search input
const searchInput = document.getElementById('js-search-input');

// Google Maps autocomplete
const autocomplete = new google.maps.places.Autocomplete(searchInput);

// Has user pressed the down key to navigate autocomplete options?
let hasDownBeenPressed = false;

// Listener outside to stop nested loop returning odd results
searchInput.addEventListener('keydown', (e) => {
    if (e.keyCode === 40) {
        hasDownBeenPressed = true;
    }
});

// GoogleMaps API custom eventlistener method
google.maps.event.addDomListener(searchInput, 'keydown', (e) => {

    // Maps API e.stopPropagation();
    e.cancelBubble = true;

    // If enter key, or tab key
    if (e.keyCode === 13 || e.keyCode === 9) {
        // If user isn't navigating using arrows and this hasn't ran yet
        if (!hasDownBeenPressed && !e.hasRanOnce) {
            google.maps.event.trigger(e.target, 'keydown', {
                keyCode: 40,
                hasRanOnce: true,
            });
        }
    }
});

 // Clear the input on focus, reset hasDownBeenPressed
searchInput.addEventListener('focus', () => {
    hasDownBeenPressed = false;
    searchInput.value = '';
});

// place_changed GoogleMaps listener when we do submit
google.maps.event.addListener(autocomplete, 'place_changed', function() {

    // Get the place info from the autocomplete Api
    const place = autocomplete.getPlace();

    //If we can find the place lets go to it
    if (typeof place.address_components !== 'undefined') {          
        // reset hasDownBeenPressed in case they don't unfocus
        hasDownBeenPressed = false;
    }

});
Galanti answered 24/8, 2018 at 11:1 Comment(2)
This only real solution with readable code and in vanilla JavaScript. Thanks Callam!Indo
This seems to give an error TypeError: a.stopPropagation is not a function error Any thoughts on this?Poniard
L
1

@Alexander 's solution is the one which I was looking for. But it was causing an error - TypeError: a.stopPropagation is not a function.

So I made the event with KeyboardEvent. Here's the working code and Javascript version is very convenient for React.js projects. I also used this for my React.js project.

(function selectFirst(input) {
  let _addEventListener = input.addEventListener
    ? input.addEventListener
    : input.attachEvent;

  function addEventListenerWrapper(type, listener) {
    if (type === 'keydown') {
      console.log('keydown');

      let orig_listener = listener;
      listener = event => {
        let suggestion_selected =
          document.getElementsByClassName('pac-item-selected').length > 0;

        if (event.keyCode === 13 && !suggestion_selected) {
          let simulated_downarrow = new KeyboardEvent('keydown', {
            bubbles: true,
            cancelable: true,
            keyCode: 40
          });

          orig_listener.apply(input, [simulated_downarrow]);
        }

        orig_listener.apply(input, [event]);
      };
    }

    _addEventListener.apply(input, [type, listener]);
  }

  if (input.addEventListener) input.addEventListener = addEventListenerWrapper;
  else if (input.attachEvent) input.attachEvent = addEventListenerWrapper;
})(addressInput);

this.autocomplete = new window.google.maps.places.Autocomplete(addressInput, options);

Hope this can help someone, :)

Lothaire answered 9/7, 2019 at 1:5 Comment(1)
I used this solution in React with the package (react-google-maps/api). I added this solution in onLoad prop of Autocomplete and had this working smoothly..Botulinus
N
1

This is as simple as I can get it (last tested Feb 2024):

let inputElement = ... // document.getElementById or similar
 
// NOTE: Must be `keydown` (not `keyup`) to run before the listener added by gmaps
inputElement.addEventListener('keydown', (event) => {
    if (event.key != 'Enter') {
        return;
    }

    // NOTE: This is brittle. See the previous answers and note that the class has changed at least once already
    if (document.querySelector('.pac-item-selected')) {
        // The user already selected a result using the arrow keys - we shouldn't override it
        return;
    }

    // Simulate ArrowDown key to select the first autocomplete suggestion
    inputElement.dispatchEvent(
        new KeyboardEvent('keydown', {
            key: 'ArrowDown',
            code: 'ArrowDown',
            // NOTE: gmaps checks `keyCode`. `key` or `code` are only included for future proofing
            keyCode: 40
        })
    );
});

let autocomplete = new google.maps.places.Autocomplete(inputElement);
Natala answered 26/2, 2024 at 1:56 Comment(0)
C
0

Building on amimissim's answer, I present a slight alternative, utilising Google's API to handle the events in a cross browser way (amimissim's solution doesn't seem to work in IE8).

I also had to change pac-item.pac-selected to pac-item-refresh.pac-selected as it seems the results div class has changed. This makes pressing ENTER on a suggestion work (rather than selecting the next one down).

var input = document.getElementById('MyFormField');
var autocomplete = new google.maps.places.Autocomplete(input);
google.maps.event.addListener(autocomplete, 'keydown', function(event) {
    var suggestion_selected = $(".pac-item-refesh.pac-selected").length > 0;
    if (event.which == 13 && !suggestion_selected) {
        var simulated_downarrow = $.Event("keydown", {
                    keyCode: 40,
                    which: 40
        });
        this.apply(autocomplete, [simulated_downarrow]);
    }
    this.apply(autocomplete, [event]);
});
Cohort answered 9/10, 2013 at 0:29 Comment(0)
C
0

Just a pure javascript version (without jquery) of the great amirnissim's solution:

listener = function(event) {
      var suggestion_selected = document.getElementsByClassName('.pac-item-selected').length > 0;
      if (event.which === 13 && !suggestion_selected) {
        var e = JSON.parse(JSON.stringify(event));
        e.which = 40;
        e.keyCode = 40;
        orig_listener.apply(input, [e]);
      }
      orig_listener.apply(input, [event]);
    };
Creosote answered 12/12, 2016 at 13:26 Comment(2)
Minor bug here. getElementsByClassName('.pac-item-selected') should be getElementsByClassName('pac-item-selected')Quarterage
Thanks for the javascript version. This is the one which I was looking for. But It's causing TypeError: a.stopPropagation is not a function errorLothaire
P
0
    /// <reference types="@types/googlemaps" />
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, NgZone, OnInit, Output, ViewChild} from '@angular/core';
import {MapsAPILoader, MouseEvent} from '@agm/core';
import { Address } from 'src/@core/interfaces/address.model';
import { NotificationService } from 'src/@core/services/notification.service';
// import {} from 'googlemaps';
declare var google: any;

// @ts-ignore

@Component({
  selector: 'app-search-address',
  templateUrl: './search-address.component.html',
  styleUrls: ['./search-address.component.scss']
})
export class SearchAddressComponent implements OnInit {

  @Input('label') label: string;
  @Input('addressObj') addressObj: Address = {};
  zoom: number;
  isSnazzyInfoWindowOpened = false;

  private geoCoder;

  // @ts-ignore
  @Output() onAddressSelected = new EventEmitter<any>();
  @Input('defaultAddress') defaultAddress = '';
  @ViewChild('search', {static: true})
  public searchElementRef: ElementRef = null;


  constructor(
    private mapsAPILoader: MapsAPILoader,
    private ngZone: NgZone,
    private notify: NotificationService,
    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef
  ) { }


  ngOnInit() {
    // console.log('addressObj# ', this.addressObj);
    if (this.defaultAddress !== '') {
      this.searchElementRef.nativeElement.value = this.defaultAddress;
    }
    // load Places Autocomplete
    this.mapsAPILoader.load().then(() => {
      if (this.addressObj.address) {
        this.setZoom();
      } else {
        this.setCurrentLocation();
      }
      this.geoCoder = new google.maps.Geocoder;
      const autocomplete = new google.maps.places.Autocomplete(this.searchElementRef.nativeElement, {
        types: ['address']
      });
      autocomplete.setTypes(['(cities)']);
      autocomplete.setComponentRestrictions({'country': 'in'});
      autocomplete.addListener('place_changed', () => {
        this.ngZone.run(() => {
          // get the place result
          const place: google.maps.places.PlaceResult = autocomplete.getPlace();

          // verify result
          if (place.geometry === undefined || place.geometry === null) {
            return;
          }

          // set latitude, longitude and zoom
          this.addressObj.latitude = place.geometry.location.lat();
          this.addressObj.longitude = place.geometry.location.lng();
          this.getAddress(this.addressObj.latitude, this.addressObj.longitude);
          this.zoom = 12;
        });
      });
    });
  }

  setZoom() {
    this.zoom = 8;
  }

  // Get Current Location Coordinates
  private setCurrentLocation() {
    if ('geolocation' in navigator) {
      navigator.geolocation.getCurrentPosition((position) => {
        this.addressObj.latitude = position.coords.latitude;
        this.addressObj.longitude = position.coords.longitude;
        this.zoom = 8;
        this.getAddress(this.addressObj.latitude, this.addressObj.longitude);
      });
    }
  }

  markerDragEnd($event: MouseEvent) {
    this.addressObj.latitude = $event.coords.lat;
    this.addressObj.longitude = $event.coords.lng;
    this.getAddress(this.addressObj.latitude, this.addressObj.longitude);
  }

  getAddress(latitude, longitude) {
    this.addressObj.latitude = latitude;
    this.addressObj.longitude = longitude;
    this.geoCoder.geocode({ location: { lat: latitude, lng: longitude } }, (results, status) => {
      if (status === 'OK') {
        if (results[0]) {
          console.log('results ', results);
          this.zoom = 12;
          this.addressObj.address = results[0].formatted_address;
          this.showSnazzyInfoWindow();
          this.addressObj.placeId = results[0].place_id;

          for(let i = 0; i < results[0].address_components.length; i++) {
            if (results[0].address_components[i].types[0] == 'locality') {
              this.addressObj.city = results[0].address_components[i].long_name;
            }
            if (results[0].address_components[i].types[0] == 'administrative_area_level_1') {
              this.addressObj.region = results[0].address_components[i].long_name;
            }
            if (results[0].address_components[i].types[0] == 'country') {
              this.addressObj.country = results[0].address_components[i].long_name;
            }
            if (results[0].address_components[i].types[0] == 'postal_code') {
              this.addressObj.zip = results[0].address_components[i].long_name;
            }
          }

          this.transmitData();
        } else {
          this.notify.showMessage('No results found', 3000, 'OK');
        }
      } else {
        this.notify.showMessage('Google maps location failed due to: ' + status, 3000, 'OK');
      }

    });
  }


  transmitData() {
   // console.log(this.addressObj);
    this.onAddressSelected.emit(this.addressObj);
  }

  toggleSnazzyInfoWindow() {
    this.isSnazzyInfoWindowOpened = !this.isSnazzyInfoWindowOpened;
  }

  showSnazzyInfoWindow() {
    this.isSnazzyInfoWindowOpened = true;
  }

}




<mat-form-field class="full-width pt-2 flex-auto w-full">
  <input matInput [(ngModel)]="addressObj.address" type="text" (keydown.enter)="$event.preventDefault()" placeholder="{{label ? label : 'Location'}}" autocorrect="off" autocapitalize="off" spellcheck="off" type="text" #search>
</mat-form-field>

<agm-map
  [latitude]="addressObj.latitude"
  [longitude]="addressObj.longitude"
  [zoom]="zoom">
  <agm-marker
    [latitude]="addressObj.latitude"
    [longitude]="addressObj.longitude"
    [markerDraggable]="true"
    (dragEnd)="markerDragEnd($event)">
  </agm-marker>

</agm-map>
Poleax answered 5/10, 2020 at 10:13 Comment(0)
O
0

Yet another solution based on Tony Brasunas' answer.

Although the original question yields great answers to me it still lacks some functionality, which people have also mentioned in the comments, when the user clicks away or when the user submits the form.

This introduces two additional events blur and submit. To introduce these events into my favourite solution I refactored a few things.

/**
  * Google Autocomplete auto first select
  * What we really need is a [lat,lng] pair to enhance validation of our form
  * @event keydown : stores the eventListener in memory
  * @event blur : pass the eventListener to a wrapper
  * @event submit : pass the eventListener to a wrapper 
  * @param {Object} input HTMLElement
  */
enableFirstSelection: function (input) {
    const originalEventListener = input.addEventListener;
    const addEventListenerWrapper = (type, listener) => {
        if (type === 'keydown') {
            this._listener = listener;

            listener = (event) => this.createKeyboardEventWrapperByKey(event, 'Enter', this._listener);
        }

        if (type === 'blur' || type === 'submit') {
            const _listener = listener;

            listener = (event) => this.createKeyboardEventWrapperByType(event, 'Enter', this._listener, _listener);
        }
            
        originalEventListener.apply(input, [type, listener]);
    };

    input.addEventListener = addEventListenerWrapper;
},

/**
  * Simulate a 'down arrow' keypress if no address has been selected
  * Re-apply the original event
  * @param {Object} ev wrapped keyboardEvent
  * @param {String} key value of the key represented by the event
  * @param {Object} kbListener stored keyboardEvent listener
  */
createKeyboardEventWrapperByKey: function (ev, key, kbListener) {
    const suggestionSelected = document.getElementsByClassName('pac-item-selected').length;

    if (ev.key === key && !suggestionSelected) {
        const e = new KeyboardEvent('keydown', {
            key: 'ArrowDown', 
            code: 'ArrowDown', 
            keyCode: 40, 
        });

        kbListener.apply(ev.currentTarget, [e]);
    }

    if (ev.type === 'keydown') {
        kbListener.apply(ev.currentTarget, [ev]);
    }
},

/**
  * Simulate a 'down arrow' keypress when the user switches focus
  * Re-apply the original event
  * @param {Object} ev wrapped event
  * @param {String} key value of the key represented by the event
  * @param {Object} kbListener stored keyboardEvent listener
  * @param {Object} typeListener stored typeEvent listener
  */
createKeyboardEventWrapperByType: function (ev, key, kbListener, typeListener) {
    const fakeTypeEvent = { key: key, currentTarget: ev.currentTarget, type: ev.type };
    this.createKeyboardEventWrapperByKey(fakeTypeEvent, key, kbListener);

    typeListener.apply(ev.currentTarget, [ev]);
},

So by calling this.enableFirstSelection(this.autocompleteField); I can now make a selection when the user presses Enter, tabs away, clicks out, triggers the form submit and so on...

The most important explanation to this solution is the requirement of the keydown listener as it is used by the other events to trigger the keyboard listener.
=> this._listener = listener; -vs- const _listener = listener;

Hopefully this is useful to anyone. Thanks to all my predecessors for helping me out on this one.

Oran answered 16/11, 2022 at 22:52 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.