Google address autocomplete

What you need

Add store variables

  1. Open your store in StoreConnect.
  2. Select New in the Store Variables section and add the following variables.
    1. Name - Address Auto-complete Google API Key - google_autocomplete_api_key Value - your Google Maps API key Available in Liquid = true
    2. Name - Address Auto-complete Countries Key - autocomplete_countries Value - 2 character, comma separated ISO code for each country you want to support i.e. US, CA (Google supports up to five at a time) Available in Liquid = true
    3. Name - Default Auto-complete Country Key - default_autocomplete_country Value - 2 character ISO code for one country you want to include Available in Liquid = true
  3. Save.

Update your theme layout template (in Liquid)

  1. Open your theme record in StoreConnect.
  2. Open the applicable Theme Template.
  3. If your theme doesn't already have a theme template, grab it here and create one for your theme: theme.liquid
  4. Add the following code to the very bottom of the theme template Content section. This ensures address fields have loaded before the layout runs.
  5. Select Save.

{%- assign google_autocomplete_api_key = store_variables['google_autocomplete_api_key'] | default: blank -%}
{%- assign autocomplete_countries = store_variables['autocomplete_countries'] | default: 'AU,US' | strip -%}
{%- assign default_autocomplete_country = store_variables['default_autocomplete_country'] | default: 'AU' -%}

// Google Maps loader
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: "{{ google_autocomplete_api_key }}", v: "weekly"});

const allowedCountries = '{{ autocomplete_countries }}'.split(',').map(c => c.trim()).filter(c => c);

let autocomplete;
let address1Field;
let address2Field;
let cityField;
let postalField;
let countryField;
let stateField;

async function initAutocomplete() {
    try {
        // Import the places library
        await google.maps.importLibrary('places');

        // Get form fields
        address1Field = document.querySelector('#shipping_address_line_1');
        address2Field = document.querySelector('#shipping_address_line_2');
        cityField = document.querySelector('#shipping_city__customer_information');
        postalField = document.querySelector('#shipping_postal_code__customer_information');
        countryField = document.querySelector('#shipping_country__customer_information');
        stateField = document.querySelector('#shipping_state__customer_information');

        if (!address1Field) {
            console.error('Address field not found');
            return;
        }

        // Set default country
        if (countryField) {
            countryField.value = '{{ default_autocomplete_country }}';
            countryField.dispatchEvent(new Event('change', { bubbles: true }));
        }

        // Update placeholder
        address1Field.placeholder = 'Start typing your address...';

        // Create autocomplete on the existing input using the Autocomplete class
        autocomplete = new google.maps.places.Autocomplete(address1Field, {
            componentRestrictions: { country: ['{{ default_autocomplete_country }}'] },
            fields: ['address_components', 'geometry'],
            types: ['address']
        });

        // Listen for place selection
        autocomplete.addListener('place_changed', fillInAddress);

        // Listen for country changes
        if (countryField) {
            countryField.addEventListener('change', function() {
                const selectedCountry = this.value;

                if (selectedCountry && allowedCountries.includes(selectedCountry)) {
                    // Update autocomplete country restriction
                    autocomplete.setComponentRestrictions({
                        country: [selectedCountry]
                    });

                    // Clear form fields when country changes
                    if (address1Field) address1Field.value = '';
                    if (address2Field) address2Field.value = '';
                    if (cityField) cityField.value = '';
                    if (postalField) postalField.value = '';
                    if (stateField) stateField.value = '';
                }
            });
        }

    } catch (error) {
        console.error('Failed to initialize autocomplete:', error);
    }
}

function fillInAddress() {
    const place = autocomplete.getPlace();

    if (!place.address_components) {
        console.log('No address components found');
        return;
    }

    let address1 = '';
    let postcode = '';
    let city = '';
    let state = '';

    // Parse address components
    for (const component of place.address_components) {
        const types = component.types;

        if (types.includes('street_number')) {
            address1 = component.long_name + ' ';
        }
        if (types.includes('route')) {
            address1 += component.long_name;
        }
        if (types.includes('locality')) {
            city = component.long_name;
        }
        if (types.includes('administrative_area_level_1')) {
            state = component.short_name;
        }
        if (types.includes('postal_code')) {
            postcode = component.long_name;
        }
    }

    // Fill in the form fields
    address1Field.value = address1.trim();
    if (cityField) cityField.value = city;
    if (postalField) postalField.value = postcode;

    // Set state - give state dropdown time to populate
    if (stateField && state) {
        setTimeout(() => {
            stateField.value = state;
            stateField.dispatchEvent(new Event('change', { bubbles: true }));
        }, 500);
    }

    // Focus on address line 2 for apartment/unit number
    if (address2Field) {
        address2Field.focus();
    }

    console.log('Address filled:', { address1, city, state, postcode });
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initAutocomplete);
} else {
    initAutocomplete();
}

/* Style the autocomplete dropdown */
.pac-container {
    z-index: 10000 !important;
    border-radius: 8px;
    margin-top: 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.3);
    font-family: inherit;
}

.pac-item {
    padding: 10px 12px;
    cursor: pointer;
    font-size: 14px;
    line-height: 1.4;
}

.pac-item:hover {
    background-color: #f5f5f5;
}

.pac-item-query {
    font-size: 15px;
    color: #333;
}

.pac-matched {
    font-weight: 600;
}