Address Autocomplete with Google Maps and Places JavaScript API

This post will focus on this demo I've created on Codepen. It's actually a pretty simple demo, but it illustrates a bunch of different concepts. There's also a few nuances of working with the Google Maps and Places JavaScript APIs that I want to highlight.

What we're building

We're going to be building a basic run-of-the-mill address form you've likely seen on thousands of sites. However, we're going to use the Google Maps and Places JavaScript APIs to autofill the form when the user types in either the street address or postal code fields, which will either perform an exact address autocomplete or postal code autocomplete, respectively. We're also going to use the HTML5 Geolocation API to get the user's current location, and with that information, both set the bounds for our autocomplete searches and automatically fill in as much of the address as possible. For this latter part, we'll use the Google Geocoding API to reverse geocode the user's location and get an approximate address.

The HTML

<fieldset class="address">  
    <legend>Address</legend>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            Address
        </label>
        <div class="col-sm-6 col-md-4">
            <input class="form-control places-autocomplete" type="text" id="Street" name="Street" placeholder="" value="" autocomplete="address-line1">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            Apt/Suite #
        </label>
        <div class="col-sm-6 col-md-4">
            <input class="form-control" type="text" id="Street2" name="Street2" value="" autocomplete="address-line2">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            Postal/Zip Code
        </label>
        <div class="col-sm-2 col-md-2">
            <input class="form-control places-autocomplete" type="text" id="PostalCode" name="PostalCode" placeholder="" value="" autocomplete="postal-code">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            City
        </label>
        <div class="col-sm-4 col-md-3">
            <input class="form-control" type="text" id="City" name="City" value="" autocomplete="address-level2">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            Country
        </label>
        <div class="col-sm-4 col-md-3">
            <input class="form-control" type="text" id="Country" name="Country" value="" autocomplete="country">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-sm-2 col-md-3">
            State/Territory
        </label>
        <div class="col-sm-4 col-md-3">
            <input class="form-control" type="text" id="State" name="State" value="" autocomplete="address-level1">
        </div>
    </div>
</fieldset>  

Our HTML here is much what you would expect, but there's a few things worth pointing out. First, the demo use Bootstrap for styling, so most of the class names are present for that. The exception is the places-autocomplete class applied to both the street address and postal code inputs. This is what our JavaScript will use to select the elements that should have Google's Autocomplete applied.

The autocomplete attributes on all the inputs are unrelated to Google's Autocomplete functionality. These are hints to the browser about how it should autofill the form, itself, if the user has saved address information. Remember, some screen readers still do not support JavaScript and some users choose to disable JavaScript. Everything we're adding here should enhance the user's experience, but the form should still work well without it.

Finally, you'll notice that both the street address and postal code inputs have placeholder="". By default, Google adds a placeholder to autocomplete inputs. Adding a blank placeholder turns this off. You could also choose to add your own custom placeholder, instead, but be advised that the placeholder Google adds is automatically localized (presented in the user's preferred language). If you provide your own placeholder, you're responsible for localizing it (or not).

The JavaScript

The first thing we're going to do is create a namespace and a couple of closures, because we're not heathens.

var GoogleMapsDemo = GoogleMapsDemo || {};

GoogleMapsDemo.Utilities = (function () {

})();

GoogleMapsDemo.Application = (function () {

})();

The Utilities closure will be for helper functions, which for this demo will consist of merely code to work with the HTML5 Geolocation API. The Application closure will hold our core code for this demo.

Working with the HTML5 Geolocation API

GoogleMapsDemo.Utilities = (function () {  
    var _getUserLocation = function (successCallback, failureCallback) {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function (position) {
                successCallback(position);
            }, function () {
                if (failureCallback)
                    failureCallback(true);
            });
         } else {
             if (failureCallback)
                 failureCallback(false);
         }
    };

    return {
        GetUserLocation: _getUserLocation
    }
})();

This is pretty straight-forward, but there's enough boilerplate that I prefer encapsulate this in a helper function. Getting the user's location is an asynchronous process, so we'll need to provide one or more callbacks, depending on whether we want to handle the failure condition or not. In this demo, successful geolocation is not a requirement: it merely adds enhanced functionality if we can get the user's location. As a result, we won't worry about a failure callback here. However, in case you do need to handle the failure condition, the boolean that's passed into the callback corresponds to whether or not the user's browser supports geolocation at all. In the scenario where the user's browser does support geolocation, but we still have a failure, it's because either geolocation has been disabled or the user has denied access for our site. The success callback just gets the position that was retrieved.

As you'll see later, you would utilize this helper function like:

GoogleMapsDemo.Utilities.GetUserLocation(function (position) {  
    // do something on success here
}, function (browserHasGeolocation) {
    // do something on failure here
});

Setting up the Autocompletes

In our demo, we have two fields that utilize Autocomplete from the Google Places API. However, you might need even more. For example, you might have a form that accepts both a shipping and billing address. Regardless, using Autocomplete with multiple fields is kind of a pain, as you need to keep track of both the Autocomplete instance and the input element it's bound to. To make this easier, this demo makes use of scopes in JavaScript to keep the two together in context. That way, we don't need to worry about trying save the references for later. We're also going to encapsulate this code in a function that will optionally accept a pair of latitude and longitude coordinates. If latLng is provided, we'll use it to set the bounds for the Autocomplete, otherwise, we'll ignore it.

GoogleMapsDemo.Application = (function () {

    ...

    var _initAutocompletes = function (latLng) {
        $('.places-autocomplete').each(function () {
            var input = this;
            var isPostalCode = $(input).is('[id$=PostalCode]');
            var autocomplete = new google.maps.places.Autocomplete(input, {
                types: [isPostalCode ? '(regions)' : 'address']
            });
            if (latLng) {
                _setBoundsFromLatLng(autocomplete, latLng);
            }

            autocomplete.addListener('place_changed', function () {
                _placeChanged(autocomplete, input);
            });

            $(input).on('keydown', function (e) {
                // Prevent form submit when selecting from autocomplete dropdown
                // with enter key
                if (e.keyCode === 13 && $('.pac-container:visible').length > 0) {
                    e.preventDefault();
                }
            });
        });
    }

    var _setBoundsFromLatLng = function (autocomplete, latLng) {
        var circle = new google.maps.Circle({
            radius: 40233.6, // 25 mi radius
            center: latLng
        });
        autocomplete.setBounds(circle.getBounds());
    }

    ...

})();

Taking it line by line, first, we select and iterate over elements with a places-autocomplete class. These will obviously be our inputs that we want to have autocomplete functionality. Inside, we set a variable, input, with this, which in this context is one of our input elements we selected. Since this is contextual and the meaning and value changes depending on the context, it's always a good idea to save it to a more descriptive variable when you need to utilize it in multiple different places, as we do here. We're also checking if this is input is for a postal code or not. There's many ways you could do this, but I chose to use an attribute selector. The $= indicates that we want to match elements with id attributes that end with the following string. This allows me to have inputs with ids like BillingPostalCode, ShippingPostalCode, etc. As long as I keep the id suffix the same, this code will work interchangeably. You could just as easily use a class name, data property, etc. though.

Now we get to the actual autocomplete code. We create an instance of google.maps.places.Autocomplete and set it to a variable. The options object we pass in merely restricts the return types we want to see. A ternary is used to make that either (regions), if it's a postal code input, or address, otherwise. Unfortunately, Google doesn't technically allow you to just search postal codes, but (regions) at least restricts it to higher level administrative areas and postal codes. The address type, on the other hand, causes the autocomplete to search for full exact address matches, which we'll need to fill in a street address.

Then, as discussed previously, if latLng was passed in, we'll use it to set the bounds for our autocomplete searches. This causes autocomplete results within the bounds to appear more prominently than those outside of the bounds. The _setBoundsFromLatLng function creates an instance of google.maps.Circle, centered on the user's location with a radius of 25 mi (40,233.6 meters), which should cover an average metro area. We can then get a LatLngBounds object from this, which we can set as the bounds for the autocomplete.

Next, we add our listener for the place_changed event. This is one point where we diverge with what you would find in the official Google API docs. Instead of passing our callback directly, we use an anonymous function that calls our callback. This allows us to pass in the input element we're currently working with, as there's oddly no way to retrieve this in the callback, otherwise. In the Google API docs, their example only utilizes one set of address fields and only one Autocomplete instance, so they never address this situation.

Finally, we bind a keydown event handler to our input. This exists to skirt around a UI issue with Google's Autocomplete implementation. If you begin typing and then click or tap a result in the drop down list, there's no issues. However, if you use the keyboard to navigate down to a result in the list and hit enter to select it, the form is actually submitted, instead. This handler catches that event (based on the keyCode value) and if the autocomplete list is visible, ignores it. That way, you can select the result without also submitting the form.

Updating the address fields

GoogleMapsDemo.Application = (function () {

    ...

    var _placeChanged = function (autocomplete, input) {
        var place = autocomplete.getPlace();
        var $fieldset = $(input).closest('fieldset');
        _updateAddress({
            input: input,
            address_components: place.address_components
        });
    }

    var _updateAddress = function (args) {
        var $fieldset;
        var isPostalCode = false;
        if (args.input) {
            $fieldset = $(args.input).closest('fieldset');
            isPostalCode = $(args.input).is('[id$=PostalCode]');
        } else {
            $fieldset = $(args.fieldset);
        }

        var $street = $fieldset.find('[id$=Street]');
        var $street2 = $fieldset.find('[id$=Street2]');
        var $postalCode = $fieldset.find('[id$=PostalCode]');
        var $city = $fieldset.find('[id$=City]');
        var $country = $fieldset.find('[id$=Country]');
        var $state = $fieldset.find('[id$=State]');

        if (!isPostalCode) {
            $street.val('');
            $street2.val('');
        }
        $postalCode.val('');
        $city.val('');
        $country.val('');
        $state.val('');

        var streetNumber = '';
        var route = '';

        for (var i = 0; i < address_components.length; i++) {
            var component = address_components[i];
            var addressType = component.types[0];

            switch (addressType) {
                case 'street_number':
                    streetNumber = component.long_name;
                    break;
                case 'route':
                    route = component.short_name;
                    break;
                case 'locality':
                    $city.val(component.long_name);
                    break;
                case 'administrative_area_level_1':
                    $state.val(component.long_name);
                    break;
                case 'postal_code':
                    $postalCode.val(component.long_name);
                    break;
                case 'country':
                    $country.val(component.long_name);
                    break;
            }
        }

        if (route) {
            $street.val(streetNumber && route
                        ? streetNumber + ' ' + route
                        : route);
        }
    }

    ...

})();

We actually have two functions here. For this particular demo, we're also going to attempt to autofill the form using address information obtained by reverse geocoding the user's location. The _updateAddress function will be used for this and also changing the fields based on the autocomplete selection. The _placeChanged function is our callback for the place_changed event on the autocomplete. In it, we call autocomplete.getPlace() to get the data for the selected autocomplete result, and then we call _updateAddress with that data and a reference to the input. We'll use this to select the closest fieldset element, which allows us to easily get at all the related address fields without messing with others that may be present on the page, such as when you might be working with multiple addresses.

The _updateAddress function is fairly unremarkable. It takes an args param that is an object composed of an address_components member and either an input or fieldset member. This is because I want to be able to accept either an input or fieldset reference, but JavaScript doesn't allow for named parameters or function overloads, the two ways you might handle this in a statically-typed language like C#.

Autofilling the form based on the user's location

GoogleMapsDemo.Application = (function () {

    ...

    var _autofillFromUserLocation = function (latLng) {
        _reverseGeocode(latLng, function (result) {
            $('.address').each(function (i, fieldset) {
                _updateAddress({
                    fieldset: fieldset,
                    address_components: result.address_components
                });
            });
        });
    };

    var _reverseGeocode = function (latLng, successCallback, failureCallback) {
        _geocoder.geocode({ 'location': latLng }, function(results, status) {
            if (status === 'OK') {
                if (results[1]) {
                    successCallback(results[1]);
                } else {
                    if (failureCallback)
                        failureCallback(status);
                }
            } else {
                if (failureCallback)
                    failureCallback(status);
            }
        });
    };

    ...

})();

There's not much of note here. The _reverseGeocode function is just a helper to abstract away some of the boilerplate with using Google's Geocoding API. Similarly to the HTML5 Geolocation API, this is an asynchronous function, so again, we have success and failure callbacks.

The _autofillFromUserLocation function utilizes _reverseGeocode to get address details from the set of coordindates and then hands this information off to _updateAddress to fill in the relevant fields in the form. We don't care about the failure condition here, because it doesn't matter. If we can't get any address information, we just won't autofill the form: no big deal.

Tying it all together

GoogleMapsDemo.Application = (function () {  
    var _geocoder;

    var _init = function () {
        _geocoder = new google.maps.Geocoder;

        GoogleMapsDemo.Utilities.GetUserLocation(function (position) {
            var latLng = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
            };
            _autofillFromUserLocation(latLng);
            _initAutocompletes(latLng);
        }, function (browserHasGeolocation) {
            _initAutocompletes();
        });
    };

    ...

    return {
        Init: _init
    }
})();

This final bit of code connects all the dots we set up previously. The _init method creates a google.maps.Geocoder instance that we'll be using for our reverse geocoding efforts. Then, our GetUserLocation utility function is used to attempt to get the user's location. On success, we'll attempt to autofill the form with an approximate address based on the location and then initialize our Google Autocompletes (with the user's location). On failure, we just initialize the Google Autocompletes.

Finally, we return the _init function as the only public member of our closure. All of this JavaScript code, now, can be safely tucked away in a JS file. On page, we'll only need the following script tag, which should be placed right before the closing </body> tag, after the JS code we created in this demo has loaded:

<script src="https://maps.googleapis.com/maps/api/js?key=[YOUR_API_KEY]&libraries=places&callback=GoogleMapsDemo.Application.Init"  
        async defer></script>

The Google Maps API script will be loaded asynchronously and deferred until the page has finished rendering. Once it loads, it will hit the callback referenced in the query string, namely GoogleMapsDemo.Application.Init, which will then, of course, execute all the code we set up.

The final code

var GoogleMapsDemo = GoogleMapsDemo || {};

GoogleMapsDemo.Utilities = (function () {  
    var _getUserLocation = function (successCallback, failureCallback) {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function (position) {
                successCallback(position);
            }, function () {
                failureCallback(true);
            });
         } else {
             failureCallback(false);
         }
    };

    return {
        GetUserLocation: _getUserLocation
    }
})();

GoogleMapsDemo.Application = (function () {  
    var _geocoder;

    var _init = function () {
        _geocoder = new google.maps.Geocoder;

        GoogleMapsDemo.Utilities.GetUserLocation(function (position) {
            var latLng = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
            };
            _autofillFromUserLocation(latLng);
            _initAutocompletes(latLng);
        }, function (browserHasGeolocation) {
            _initAutocompletes();
        });
    };

    var _initAutocompletes = function (latLng) {
        $('.places-autocomplete').each(function () {
            var input = this;
            var isPostalCode = $(input).is('[id$=PostalCode]');
            var autocomplete = new google.maps.places.Autocomplete(input, {
                types: [isPostalCode ? '(regions)' : 'address']
            });
            if (latLng) {
                _setBoundsFromLatLng(autocomplete, latLng);
            }

            autocomplete.addListener('place_changed', function () {
                _placeChanged(autocomplete, input);
            });

            $(input).on('keydown', function (e) {
                // Prevent form submit when selecting from autocomplete dropdown with enter key
                if (e.keyCode === 13 && $('.pac-container:visible').length > 0) {
                    e.preventDefault();
                }
            });
        });
    }

    var _autofillFromUserLocation = function (latLng) {
        _reverseGeocode(latLng, function (result) {
            $('.address').each(function (i, fieldset) {
                _updateAddress({
                    fieldset: fieldset,
                    address_components: result.address_components
                });
            });
        });
    };

    var _reverseGeocode = function (latLng, successCallback, failureCallback) {
        _geocoder.geocode({ 'location': latLng }, function(results, status) {
            if (status === 'OK') {
                if (results[1]) {
                    successCallback(results[1]);
                } else {
                    if (failureCallback)
                        failureCallback(status);
                }
            } else {
                if (failureCallback)
                    failureCallback(status);
            }
        });
    }

    var _setBoundsFromLatLng = function (autocomplete, latLng) {
        var circle = new google.maps.Circle({
            radius: 40233.6, // 25 mi radius
            center: latLng
        });
        autocomplete.setBounds(circle.getBounds());
    }

    var _placeChanged = function (autocomplete, input) {
        var place = autocomplete.getPlace();
        _updateAddress({
            input: input,
            address_components: place.address_components
        });
    }

    var _updateAddress = function (args) {
        var $fieldset;
        var isPostalCode = false;
        if (args.input) {
            $fieldset = $(args.input).closest('fieldset');
            isPostalCode = $(args.input).is('[id$=PostalCode]');
            console.log(isPostalCode);
        } else {
            $fieldset = $(args.fieldset);
        }

        var $street = $fieldset.find('[id$=Street]');
        var $street2 = $fieldset.find('[id$=Street2]');
        var $postalCode = $fieldset.find('[id$=PostalCode]');
        var $city = $fieldset.find('[id$=City]');
        var $country = $fieldset.find('[id$=Country]');
        var $state = $fieldset.find('[id$=State]');

        if (!isPostalCode) {
            $street.val('');
            $street2.val('');
        }
        $postalCode.val('');
        $city.val('');
        $country.val('');
        $state.val('');

        var streetNumber = '';
        var route = '';

        for (var i = 0; i < args.address_components.length; i++) {
            var component = args.address_components[i];
            var addressType = component.types[0];

            switch (addressType) {
                case 'street_number':
                    streetNumber = component.long_name;
                    break;
                case 'route':
                    route = component.short_name;
                    break;
                case 'locality':
                    $city.val(component.long_name);
                    break;
                case 'administrative_area_level_1':
                    $state.val(component.long_name);
                    break;
                case 'postal_code':
                    $postalCode.val(component.long_name);
                    break;
                case 'country':
                    $country.val(component.long_name);
                    break;
            }
        }

        if (route) {
            $street.val(streetNumber && route
                        ? streetNumber + ' ' + route
                        : route);
        }
    }

    return {
        Init: _init
    }
})();

Wrap Up

We've covered a ton of ground in this post, so hopefully you're not too overwhelmed. I should let you know that usage of the Google Places JavaScript API seems to require an API key; the Google Maps JavaScript API works fine without one. I created a key specifically for the Codepen demo, but depending on how much usage it gets, the demo may stop functioning. If that should occur, you need only get your own API key from the Google API Console and change it. Also, feel free to fork my Codepen and create your own experiments; just please change/remove the API key, so this demo can remain functional as much as possible.

If you have any questions, comments or suggestions, hit me up in the comments below.

comments powered by Disqus