Checkout Address Autocomplete Without a Plugin

Learn how to create an address search with autocomplete for your WooCommerce checkout page, using the Google Places API (autocomplete).

We’ll insert a custom text box into the billing address form, straight after the country chooser, so your customers can search for their billing address. Address-matches will show as a pop-up pick-list.

Requirements

The customer will choose their billing country from the standard WooCommerce drop-down/search. The chosen country will constrain the address auto-complete, so the results are more relevant. There’s no point in searching USA addresses when the customer has told you they’re in France.

After a customer has searched for their address, pressing Enter should auto-populate the billing address fields. It should NOT submit the checkout form.

The customer needs to be able to bypass the address-search to type in their address manually, if they want to.

importantMake sure you’re using a custom child theme so you can edit your functions.php file.

importantYou will need a Google JavaScript Maps/Places API Key. If your site has an interactive Google Map on it, you’ve probably already got an API Key you can use for this project.

How It’s Going to Work

When WooCommerce renders the checkout page, it creates an array of all the fields it wants to display (in PHP). Then it loops through this array and renders the HTML. We can hook into this process and inject our own fields into the array. We’ll use the woocommerce_checkout_fields filter to do this.

Here’s what the array of checkout fields looks like (expressed as JSON):

{
	"billing": {
		...
		"billing_country": {
			"type": "country",
			"label": "Country/Region",
			"required": true,
			"class": [ "form-row-wide", "address-field", "update_totals_on_change" ],
			"autocomplete": "country",
			"priority": 40
		},
		"billing_address_1": {
			"label": "Street address",
			"placeholder": "House number and street name",
			"required": true,
			"class": [ "form-row-wide", "address-field" ],
			"autocomplete": "address-line1",
			"priority": 50
		},
		...
	}
	"shipping": {
		...
	},
	"account": [],
	"order": {
		"order_comments": {
			"type": "textarea",
			"class": [ "notes" ],
			"label": "Order notes",
			"placeholder": "Notes about your order, e.g. special notes for delivery."
		}
	}
}

So we just need to create a new billing field that’s similar to billing_address_1 (a regular text field), and we want to inject it into the array straight after the billing_country field. Notice that billing_country has a priority of 40, and billing_address_1 has a priority of 50 – this is how WooCommerce sorts the fields. So we can create our billing_search field with a priority of 45… easy.

After we’ve got the billing_search field injected into the checkout HTML, we’ll create some JavaScript magic to link up to Google and turn billing_search into an auto-complete box. We don’t want to call the Google Places JavaScript from every page of our site, because that would drag down our page-load speed, so we’ll only add it on the checkout page. Makes sense.

Let’s write some code!

The PHP Code

In your custom child theme, create a new folder called “wpt-address-autocomplete”. In this folder, create two empty files, called “address-autocomplete.css” and “address-autocomplete.js” – these are our frontend asset files, and you can leave them empty for now. Now go back to your custom child theme’s main folder and create a file called “wpt-address-autocomplete.php” and paste the following into it:

<?php

/**
 * WP Tutorials Address Autocomplete (WPTAAC)
 *
 * https://wp-tutorials.tech/refine-wordpress/checkout-address-autocomplete-without-a-plugin/
 *
 */

defined('WPINC') || die();

const WPTAAC_GOOGLE_PLACES_API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // << REPLACE THIS
const WPTAAC_DEFAULT_COUTNRY_CODE = 'GB';
const WPTAAC_BILLING_SEARCH_FIELD_PRIORITY = 45;

function wptaac_enqueue_assets() {
	global $wptaac_have_assets_been_enqueued;

	if (is_null($wptaac_have_assets_been_enqueued)) {
		$handle = 'wptaac';
		$base_uri = get_stylesheet_directory_uri();
		$theme_version = wp_get_theme()->get('Version');

		// REF: https://developers.google.com/maps/documentation/places/web-service/autocomplete
		$google_script_url = sprintf(
			'https://maps.googleapis.com/maps/api/js?libraries=places&key=%s',
			esc_attr(WPTAAC_GOOGLE_PLACES_API_KEY)
		);

		// This is our main frontend JavaScript asset, with all our frontend code.
		wp_enqueue_script(
			$handle,
			$base_uri . '/wpt-address-autocomplete/address-autocomplete.js',
			array('jquery'),
			$theme_version,
			true
		);

		// Pass the Google Script URL and a default/fallback country code to the
		// frontend script in a global JavaScript variable called wptaacData.
		// We pick this up in our JavaSrtipt main entry point.
		wp_localize_script(
			$handle,
			'wptaacData',
			array(
				'googleScriptUrl' => $google_script_url,
				'defaultCountry' => WPTAAC_DEFAULT_COUTNRY_CODE,
			)
		);

		// Add our styles.
		wp_enqueue_style(
			$handle,
			$base_uri . '/wpt-address-autocomplete/address-autocomplete.css',
			null,
			$theme_version
		);

		$wptaac_have_assets_been_enqueued = true;
	}
}

function custom_woocommerce_checkout_fields($fields) {
	if (is_admin() || !is_checkout() || wp_doing_ajax()) {
		// ...
	} elseif (!array_key_exists('billing', $fields)) {
		// ...
	} elseif (!array_key_exists('billing_country', $fields['billing'])) {
		// Billing country isn't in the list of checkout fields. Weird.
	} else {
		wptaac_enqueue_assets();

		// Inject the #billing_search field.
		$fields['billing']['billing_search'] = array(
			'type' => 'text',
			'label' => __('Search For Your Address', 'wp-tutorials'),
			'placeholder' => __('Type to search for address', 'wp-tutorials'),
			"priority" => WPTAAC_BILLING_SEARCH_FIELD_PRIORITY,
		);
	}

	return $fields;
}
add_filter('woocommerce_checkout_fields', 'custom_woocommerce_checkout_fields');

This bit of back-end PHP code should be easy enough to follow… we’ve got two functions in there. One is to enqueue the frontend assets, and the other is to inject our autocomplete/search text box into the billing fields array.

There are some interesting things to note in wptaac_enqueue_assets():

  • You will need to set WPTAAC_GOOGLE_PLACES_API_KEY to your own Google Places API Key.
  • You can also set your fallback/default country code in WPTAAC_DEFAULT_COUTNRY_CODE, which is a two-character ISO country code, like GB, IN, US, DE, etc.
  • We’re using wp_localize_script() to pass some data into our frontend script, in a global JavaScript variable called wptaacData.

Now we need to we need to reference the new code from our child theme, so edit your child theme’s functions.php and add this into it:

// WooCommerce checkout Address autocomplete
require_once dirname(__FILE__) . '/wpt-address-autocomplete.php';

A Bit of Style

In your wpt-address-autocomplete folder, edit address-autocomplete.css and paste the following into it:

/**
 * WP Tutorials Address Autocomplete (WPTAAC)
 *
 * https://wp-tutorials.tech/refine-wordpress/checkout-address-autocomplete-without-a-plugin/
 *
 */
label[for="billing_search"]::before {
	font-family: 'Font Awesome 5 Free';
	font-weight: 900;
	content: '\f002';
	margin-right: 0.5em;
}

This snippet adds a small magnifying glass next to the billing_search label. It depends on Font Awesome 5 being available, but you can style #billing_search however you like.

That should be enough to get our custom field injected into the checkout form. Save everything, put something into your cart/basket and go to the checkout… You should see the new #billing_search field in there.

WooCommerce checkout address search
Our WooCommerce address search field in the checkout

The JavaScript Code

In your custom child theme, go into the wpt-address-autocomplete folder, edit “address-autocomplete.js” and paste the following lump into it.

/**
 * WP Tutorials Address Autocomplete (WPTAAC)
 *
 * https://wp-tutorials.tech/refine-wordpress/checkout-address-autocomplete-without-a-plugin/
 *
 */

/**
 * Google creates a widget... which is not a WordPress widget. It's a generic
 * term for a "thing". In this case, it's the thing that links our on-screen
 * text box to the Google Places API.
 */
var addressAutocompleteWidget = null;

/**
 * Main entry point. Instantiate the Google Places API script and listen for
 * changes of #billing_country
 */
(function($) {
	'use strict';
	$(window).on('load', function() {
		// console.log(`WPTAAC: Load`);

		const searchElement = $('#billing_search');
		if (searchElement.length == 0) {
			console.log('Missing element: #billing_search');
		} else if (typeof wptaacData === 'undefined') {
			// Don't do anything unless wptaacData has been passed from the backend
			// using wp_localize_script().
		} else {
			// Keep track of the last time we changed the billing country, so we
			// know if the user has selected a new country, or chosen the same
			// country again.
			wptaacData.lastSelectedCountry = null;

			// Disable the address-earch box until the Google scripts have loaded.
			$(searchElement).prop('disabled', true);

			// Uncomment this to verify that your Google Places API Key is coming
			// through into the script properly.
			// console.log(`WPTAAC: Init: ${wptaacData.googleScriptUrl}`);

			// Create a new script element, set the Google Places API URL,
			// with the callback set to our createAddressAutocomplete() function.
			var googleScriptElement = $('<script async />');
			$(googleScriptElement).prop('src', `${wptaacData.googleScriptUrl}&callback=createAddressAutocomplete`);
			$('head').append(googleScriptElement);

			// WooCommerce triggers the "updated_checkout" event when the customer
			// changes things like the billing country. We use this to change the
			// scope/restrictions for our autocomplete boc, to make the suggested
			// addresses more relevant for the customer.
			$(document.body).on('updated_checkout', function(event) {
				setAddressLookupCountryRestriction();
			});
		}
	});
})(jQuery);

/**
 * This is called by the Google script, after it's loaded. This is where we
 * initialise and create Google's autocomplte widget, based on our
 * #billing_search INPUT control.
 */
function createAddressAutocomplete() {
	const searchElement = document.getElementById('billing_search');

	if (addressAutocompleteWidget) {
		console.log('addressAutocompleteWidget has already been created!');
	} else if (!searchElement) {
		console.log('Missing element: #billing_search');
	} else {
		// Prevent the Enter key from causing a form-submission.
		google.maps.event.addDomListener(searchElement, 'keydown', function(event) {
			if (event.keyCode === 13) {
				event.preventDefault();
			}
		});

		// Create the Options for the Google Places Autocomplete widget.
		// fields: ['address_components', 'geometry', 'icon', 'name'],
		const defaultCountryCode = document.getElementById('billing_country').value;
		if (!defaultCountryCode) {
			defaultCountryCode = wptaacData.defaultCountry;
		}

		const options = {
			componentRestrictions: {
				country: defaultCountryCode
			},
			fields: ['address_components'],
			strictBounds: false,
			types: ['address'],
		};

		// Create the Google Places Autocomplete widget.
		addressAutocompleteWidget = new google.maps.places.Autocomplete(
			searchElement,
			options
		);

		// When the Autocomplete widget raises the place_changed event,
		// call our populateAddressFromAutocomplete() function.
		addressAutocompleteWidget.addListener("place_changed", populateAddressFromAutocomplete);

		// Enable the billing_search input field.
		searchElement.disabled = false;

		setAddressLookupCountryRestriction();
	}
}

function setAddressLookupCountryRestriction() {
	var newCountryCode = document.getElementById('billing_country').value;

	// Fallback to our default country code if newCountryCode is blank.
	if (!newCountryCode) {
		newCountryCode = wptaacData.defaultCountry;
	}

	// If the country code has changed, set the country restriction on the
	// autocomplete widget and set the input focus to the billing_search
	// text box, so the user can start typing an address.
	if (addressAutocompleteWidget && (wptaacData.lastSelectedCountry != newCountryCode)) {
		console.log(`Setting country restriction: ${newCountryCode}`);

		wptaacData.lastSelectedCountry = newCountryCode;
		addressAutocompleteWidget.setComponentRestrictions({ country: newCountryCode });

		document.getElementById('billing_search').focus();
	}
}

/**
 * Get the current selected billing country code, and fall back to our default
 * country code if, for some reason, it's blank.
 */
function getAddressAutocompleteCountryCode() {
	var countryCode = null;

	const countryCodeElement = document.getElementById('billing_country');
	if (countryCodeElement) {
		countryCode = countryCodeElement.value;
	}

	if (!countryCode) {
		countryCode = wptaacData.defaultCountry;
	}

	return countryCode;
}

/**
 * Set all the billing fields to empty strings.
 */
function resetBillingAddress() {
	const billingFields = [
		'billing_address_1',
		'billing_address_2',
		'billing_postcode',
		'billing_city',
		'billing_state',
	];

	billingFields.forEach(function(billingField) {
		var input = document.getElementById(billingField);
		if (input) {
			input.value = '';
		}
	});
}

/**
 * Scan a Google "place" object for a specific component
 * and return either its short_name or long_name.
 */
function getAddressComponent(place, componentType, componentLength) {
	// const place = addressAutocompleteWidget.getPlace();
	if (!componentLength) {
		componentLength = 'short_name';
	}

	var componentValue = null;
	for (const component of place.address_components) {
		if (component.types[0] == componentType) {
			componentValue = component[componentLength];
			break;
		}
	}

	return componentValue;
}

/**
 * Scan the various post code components of a Google place object, pick the
 * first one that has something in it and set the content os #billing_postcode
 */
function setBestPostalCode(place) {
	const postalComponents = [
		'postal_code',
		'postal_code_suffix',
		'postal_code_prefix'
	];

	var input = document.getElementById('billing_postcode');
	if (input) {
		postalComponents.forEach(function(postalComponent) {
			var autoCompleteValue = getAddressComponent(place, postalComponent);
			if (input.value) {
				// There's already something in the post code box.
			} else if (!autoCompleteValue) {
				// Auto-complete has no suggestion for this field.
			} else {
				input.value = autoCompleteValue;
			}
		});
	}
}

/**
 * This is our main handler, which is called each time the user picks an
 * autocomplete suggested address.
 */
function populateAddressFromAutocomplete() {
	// Get the suggested "place" object from the widget.
	const place = addressAutocompleteWidget.getPlace();

	// Uncomment this to see what's in the Google place object.
	// console.log(place);
	// console.log(place.address_components);

	// Set the billing address fields values to empty.
	resetBillingAddress();

	var billingCountryCode = getAddressAutocompleteCountryCode();

	if (!place) {
		console.log('Invalid / missing place from Google API');
	} else if (billingCountryCode == 'US') {
		// Special address handling for the USA.
		document.getElementById('billing_address_1').value = getAddressComponent(place, 'street_number') + ' ' + getAddressComponent(place, 'route', 'long_name');
		document.getElementById('billing_city').value = getAddressComponent(place, 'locality');
		document.getElementById('billing_postcode').value = getAddressComponent(place, 'postal_code');
		document.getElementById('billing_state').value = getAddressComponent(place, 'administrative_area_level_1');

		// Trigger a change on the #billing_state field so that the on-screen
		// state chooser gets updated.
		jQuery('#billing_state').change();
	} else if (billingCountryCode == 'GB') {
		// Special address handling for the United Kingdom (Great Britain).
		document.getElementById('billing_address_1').value = getAddressComponent(place, 'street_number') + ' ' + getAddressComponent(place, 'route', 'long_name');
		document.getElementById('billing_city').value = getAddressComponent(place, 'locality');
		if (!document.getElementById('billing_city').value) {
			document.getElementById('billing_city').value = getAddressComponent(place, 'postal_town');
		}

		document.getElementById('billing_state').value = getAddressComponent(place, 'administrative_area_level_2', 'long_name');

		setBestPostalCode(place);
	} else {
		for (const component of place.address_components) {
			const componentType = component.types[0];

			switch (componentType) {
				case "street_number":
					document.getElementById('billing_address_1').value = component.long_name;
					break;

				case "route":
					document.getElementById('billing_address_1').value += ' ' + component.short_name;
					break;

				case "locality":
					document.getElementById('billing_address_2').value = component.long_name;
					document.getElementById('billing_city').value = component.long_name;
					break;

				case "postal_town":
					document.getElementById('billing_city').value = component.long_name;
					break;

				case "administrative_area_level_1":
					document.getElementById('billing_state').value = component.short_name;
					jQuery('#billing_state').change();
					break;

				case "country":
				default:
					// Either an unhandled or unnecessary address component.
					// console.log(componentType);
					// console.log(component);
					break;
			}
		}

		setBestPostalCode(place);

	}

	// Because we've changed a bunch of the address fields, we need to trigger
	// a re-validate of the form fields.
	jQuery('.validate-required input, .validate-required select').trigger('validate');
}

There’s quite a bit to unpick in here, but it’s thoroughly commented and it should be easy enough to see what each chunk of code does. The main points of note are:

  • The top jQuery section is our “Main entry point“. This is the first lump of code that gets executed, and it’ll only do something if it detects the global JavaScript variable wptaacData, which contains parameters such as our Google Script URL (which contains the API Key). This code creates a new script element and attaches it to the document’s head element. When the script has finished loading, it calls our createAddressAutocomplete() function, which is where we start setting things up properly.
  • The createAddressAutocomplete() function uses the Google code to create a new “widget”, which is NOT a WordPress widget. It’s just a “thing” that links our on-screen #billing_search field to Google’s API. This code also tells the Google widget/thing to call populateAddressFromAutocomplete() when the user selects an address from the pick-list.
  • In populateAddressFromAutocomplete() is where we take the results of the autocomplete and poke the values into the billing fields. Note that we’ve got a bit of code in there to do specific things for the US and GB countries. We might find that lots of other countries benefit from their own specific-case handlers too.
  • The only other things in here are helper functions. I’ve given them meaningful names, so you can figure out what they do easily enough.

Test Test Test

This code runs at a critical point in a website’s primary workflow. You’re capturing user data immediately before a financial transaction. If this script makes the user experience bad in any way, or if it clashes with WooCommerce in the future (because of some unforeseen update to WooCommerce) then you’ll need to know about it. So…

Test the code… Test it a lot.

  • Try lots of different addresses, for different countries.
  • Make sure you’ve got a quick way of disabling the auto-complete code completely (just comment-out the call to require_once() in functions.php).
  • Try it on mobile devices, especially devices with small screens.
  • Test with different character sets, such as Japanese or Cyrillic.
  • Make sure your Google places API Key is restricted to your website’s HTTP Referrer, otherwise somebody might “steal” your API key and you’ll end up with a big bill from Google for a bunch of API calls you didn’t actually make.

That’s it. Happy addressing! 😎 👍

Like This Tutorial?

loading

Let us know

Leave a Comment

Your email address will not be published.