Home > Software engineering >  How to create your own custom Google address autocomplete in BigCommerce One Step Checkout
How to create your own custom Google address autocomplete in BigCommerce One Step Checkout

Time:09-23

How do I add a "place_change" event listener in a separate callback as I cannot edit the built in BigCommerce callback, I want to be able to fire the event listener on selection of an autocomplete option without instantiating a Map or an autocomplete object on an input as there is already one generated by the BigCommerce built in callback.

I have to write custom JavaScript code in the script manager in BigCommerce to add the Suburb address component to Google places autocomplete. I am doing this by using the mutation observer to detect when the shipping address form components are loaded into the DOM on the one step checkout and this works well.

What my script is doing is:

  1. setting a pattern for HTML5 form validation to make sure the address selected must start with a number and case insensitive which includes letters A to Z, numbers (0-9) and hyphen "-" and forward slash "/" as well as Maori accented characters for Maori vowels ā, ē, ī, ō, ū
  2. Adds Suburb to autocomplete, as there is no way for me to amend the BigCommerce code for adding an address component to Google autocomplete, I need to write my own code to pass the address selected from autocomplete and return Suburb to auto populate my custom field Suburb on the shipping form

I have had issues setting the correct event listener to fire at the right time, I ended up using the blur event and then setting a 1 second timer, although this works, it does feel very hacky and I would prefer to use the "place_changed" event instead, I am just not sure how to set an event listener for this outside of the callback for autocomplete, which I don't have access to. I wrote my own callback, but not sure how to use "place_change" event without loading a map or invoking autocomplete on an input.

Trying to simulate and test concepts in JSFiddle

Dirty timer method from JSFiddle

...
// Invoke autocomplete custom listener via JavaScript initAutocomplete_custom();
document.addEventListener('DOMContentLoaded', function() {
  var js_file = document.createElement('script');
  js_file.type = 'text/javascript';
  js_file.src = 'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=geometry&callback=initAutocomplete_custom';
  document.getElementsByTagName('head')[0].appendChild(js_file);
});
...

...
//custom call back to geometry library 
function initAutocomplete_custom() {
  // When the user selects an address from the dropdown, populate the address
  // fields in the form.
  const input = document.getElementById('addressLine1Input');
  input.addEventListener('blur', (e) => {
    getAddressComponent_test();
  });
}
...

...
function getAddressComponent_test() {
  document.getElementById("sublocality_level_1").value = '';
  document.getElementById("sublocality_level_1").disabled = false;

  var delayInMilliseconds = 1000; //1 second
  setTimeout(function() {
    //your code to be executed after 1 second
    var address = document.getElementById("addressLine1Input").value;


    var geocoder = new google.maps.Geocoder();

    geocoder.geocode({
      'address': address
    }, function(results, status) {

      if (status == google.maps.GeocoderStatus.OK) {
        
     
     if (results[0].address_components[2].short_name !== undefined){
            var suburb = results[0].address_components[2].short_name;
        document.getElementById("sublocality_level_1").value = suburb;
        console.log(suburb);
      }

      } else {
        console.log("Invalid Address");
      }
    });
  }, delayInMilliseconds);
}
...

My BigCommerce code from the script manager


<script>
(function(win) {
    'use strict';
    
    var listeners = [], 
    doc = win.document, 
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;
    
    function ready(selector, fn) {
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if (!observer) {
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }
        
    function check() {
        // Check the DOM for elements matching a stored selector
        for (var i = 0, len = listeners.length, listener, elements; i < len; i  ) {
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for (var j = 0, jLen = elements.length, element; j < jLen; j  ) {
                element = elements[j];
                // Make sure the callback isn't invoked with the 
                // same element more than once
                if (!element.ready) {
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;
            
})(this);

ready('#checkoutShippingAddress', function(element) {
    
    // Hit checkoutShippingAddress console flag
    console.log("You're on the shipping step!");

    // Invoke autocomplete custom listener via JavaScript initAutocomplete_custom(); must be done via JavaScript
    document.addEventListener('DOMContentLoaded', function () {
        var js_file = document.createElement('script');
        js_file.type = 'text/javascript';
        js_file.src = 'https://maps.googleapis.com/maps/api/js?key=<API_key_placeholder>&libraries=geometry&callback=initAutocomplete_custom';
        document.getElementsByTagName('head')[0].appendChild(js_file);  
    });

    /** @start HTML5 form validation **/ 

    // Target autocomplete form input
    let fulladdress = document.getElementById('addressLine1Input');

    // Address validation must start with a number, case insensitive which includes letters A to Z, numbers (0-9), hyphen "-", forward slash "/" as well as Maori accented characters for Maori vowels ā, ē, ī, ō, ū
    fulladdress.setAttribute("pattern", "\\d[/a-zA-ZĀ-ū0-9\\s',-]*");

    // event listener to clear error message for input
    fulladdress.addEventListener('input', () => {
        fulladdress.setCustomValidity('');
        fulladdress.checkValidity();
    });

    // event listener to invoke validation and show error message if needed
    fulladdress.addEventListener('invalid', () => {
        fulladdress.setCustomValidity('No PO Box or Private Bag, address must start with a number, e.g. 1/311 Canaveral Drive');
    });
    
    /** @end HTML5 form validation **/

    function initAutocomplete_custom() {
        // When the user selects an address from the dropdown, populate the address
        // fields in the form.
        const input = document.getElementById('addressLine1Input');
        input.addEventListener('blur', (e) => {  
            getAddressComponent_test();  
        });
    }

    function getAddressComponent_test() {
        var delayInMilliseconds = 1000; //1 second
        setTimeout(function() {
            var address = document.getElementById("addressLine1Input").value;
            var geocoder = new google.maps.Geocoder();
            
            geocoder.geocode( { 'address': address}, function(results, status){
                
                if (status==google.maps.GeocoderStatus.OK){

                    if (results[0].address_components[2].short_name !== undefined){
                        var suburb = results[0].address_components[2].short_name;
                        document.getElementById("addressLine2Input").value = suburb;
                        console.log(suburb);
                    }

                } else{ console.log("Invalid Address"); }
            });
        }, delayInMilliseconds);
    }

});   
</script>

CodePudding user response:

This works, the best solution is to disable the BigCommerce built in Google autocomplete and write your own.

I still have an issue I am working through the BigCommerce *required fields. CITY and POSTAL CODE do autocomplete successfully, but the BigCommerce form validation which I think is via React sees them as empty even though they are populated and throws a form input empty and required error: (figuring out how I resolve that, there is no way to turn off those fields being required, I tried that)

<script>
(function(win) {
    'use strict';
    
    var listeners = [], 
    doc = win.document, 
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;
    
    function ready(selector, fn) {
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if (!observer) {
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }
        
    function check() {
        // Check the DOM for elements matching a stored selector
        for (var i = 0, len = listeners.length, listener, elements; i < len; i  ) {
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for (var j = 0, jLen = elements.length, element; j < jLen; j  ) {
                element = elements[j];
                // Make sure the callback isn't invoked with the 
                // same element more than once
                if (!element.ready) {
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;
            
})(this);


    var autocomplete;

    var componentForm = {
        subpremise: 'short_name',
        street_number: 'short_name',
        route: 'long_name',
        sublocality_level_1: 'short_name',
        locality: 'long_name',
        administrative_area_level_1: 'short_name',
        country: 'short_name',
        postal_code: 'short_name'
    };

    var componentInputs = {
        sublocality_level_1: 'addressLine2Input',
        locality: 'cityInput',
        administrative_area_level_1: 'provinceInput',
        country: 'countryCodeInput',
        postal_code: 'postCodeInput'
    };

    var regex = /^.*(po\s*box|private\s*bag).*$|^\d[\/a-zĀ-ū0-9\s\,\'\-]*$/i;

    function initAutocomplete() {
        // Create the autocomplete object, restricting the search to geographical
        // location types.
        autocomplete = new google.maps.places.Autocomplete(
            /** @type {!HTMLInputElement} */
            (document.getElementById('addressLine1Input')), {
            types: ['geocode']
            });
        
        // When the user selects an address from the dropdown, populate the address
        // fields in the form.
        autocomplete.addListener('place_changed', fillInAddress);
        
    }

    // Break out out address components from autocomplete
    function fillInAddress() {
        // Get the place details from the autocomplete object.
        var place = autocomplete.getPlace();
        var short_address = "";
        var subprem = "";
      
        // Get each component of the address from the place details
        // and fill the corresponding field on the form.
        for (var i = 0; i < place.address_components.length; i  ) {

            var addressType = place.address_components[i].types[0];
            var str = document.getElementById("addressLine1Input").value;
            var match = str.match(regex);
          
            // Check if address type is in components we are looking for
            if (componentForm[addressType]) {
                
                var val = place.address_components[i][componentForm[addressType]];
                
                // Set generic address components in form inputs
                if (componentInputs[addressType]){
                    document.getElementById(componentInputs[addressType]).value = val;
                }
                
                // Build short address string
                if(addressType == 'street_number') {
                
                    short_address = val;
                
                } else if (addressType == 'route') {
                
                    short_address  = " "   val;
                    short_address = short_address.trim();
                
                } else if (addressType == 'subpremise') {
            
                if (val !== undefined) {
                    var numberPattern = /\d /g;
                    var arr_match = val.match( numberPattern );
                    subprem = arr_match[0]   "/";
                }
              
            }
            
            // Validate full address selected
            if (match && !match[1]) {
                // If address is valid set to short address after breaking out the components
                document.getElementById('addressLine1Input').value = (subprem   short_address);
            }
            
          }
        }
      
        // Run validation rules and UI prompts
        validate();
      
    }

ready('#checkoutShippingAddress', function(element) {

    // Invoke autocomplete custom listener via JavaScript initAutocomplete(); must be done via JavaScript
    var js_file = document.createElement('script');
    js_file.type = 'text/javascript';
    
    js_file.src = 'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete';
    document.getElementsByTagName('head')[0].appendChild(js_file);  
    
    // Hit checkoutShippingAddress console flag
    console.log("You're on the shipping step!");

    /** @start HTML5 form validation **/ 

    // Target autocomplete form input
    let fulladdress = document.getElementById('addressLine1Input');
    fulladdress.onblur = validate;

    // Address validation must start with a number, case insensitive which includes letters A to Z, numbers (0-9), hyphen "-", forward slash "/" as well as Maori accented characters for Maori vowels ā, ē, ī, ō, ū
    fulladdress.setAttribute("pattern", "\\d[/a-zA-ZĀ-ū0-9\\s',-]*");

    // event listener to clear error message for input
    fulladdress.addEventListener('input', () => {
        fulladdress.setCustomValidity('');
        fulladdress.checkValidity();
    });

    // event listener to invoke validation and show error message if needed
    fulladdress.addEventListener('invalid', () => {
        fulladdress.setCustomValidity('No PO Box or Private Bag, address must start with a number, e.g. 1/311 Canaveral Drive');
    });
    
    /** @end HTML5 form validation **/

    function validate() {

        // Get address value for validation
        var str = document.getElementById("addressLine1Input").value;
        var match = str.match(regex);
        var lbl = document.getElementById('addressLine1Input-label');
        var short_address = document.getElementById('addressLine1Input');
    
        // If address is valid
        if (match && !match[1]) {
    
            short_address.setAttribute('title', 'Address appears to be valid');
            short_address.setCustomValidity('');
        
            if (lbl.style.removeProperty) {
                lbl.style.removeProperty('color');
            } else {
                lbl.style.removeAttribute('color');
            }  
        
            lbl.innerHTML = "Address (No PO Box)";
        
        // else address is invalid
        } else {
        
            short_address.setCustomValidity('No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive');
            short_address.setAttribute('title', 'No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive');
            short_address.focus();
        
            lbl.style.color = '#b22222';
            lbl.innerHTML = "No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive";
        
        } 
    
    }

    // Bias the autocomplete object to the user's geographical location,
    // as supplied by the browser's 'navigator.geolocation' object.
    function geolocate() {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function(position) {
                var geolocation = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
                };
                var circle = new google.maps.Circle({
                center: geolocation,
                radius: position.coords.accuracy
                });
                autocomplete.setBounds(circle.getBounds());
            });
        }
    }

});   
</script>

CodePudding user response:

This works perfectly in Chrome, Edge, Firefox and Safari, hope it helps other people too, it also resolves the issue of how to programmatically fill input elements built with React.

The answer is simply turn off the BigCommerce Google Address AutoComplete and write your own. I hope this helps other people and saves them time.

Simply add the script to your BigCommerce Script Manager targeting the Checkout

<script>
(function(win) {
    'use strict';
    
    var listeners = [], 
    doc = win.document, 
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;
    
    function ready(selector, fn) {
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if (!observer) {
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }
        
    function check() {
        // Check the DOM for elements matching a stored selector
        for (var i = 0, len = listeners.length, listener, elements; i < len; i  ) {
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for (var j = 0, jLen = elements.length, element; j < jLen; j  ) {
                element = elements[j];
                // Make sure the callback isn't invoked with the 
                // same element more than once
                if (!element.ready) {
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;
            
})(this);


    var autocomplete;

    var componentForm = {
        subpremise: 'short_name',
        street_number: 'short_name',
        route: 'long_name',
        sublocality_level_1: 'short_name',
        locality: 'long_name',
        administrative_area_level_1: 'short_name',
        postal_code: 'short_name'
    };

    var componentInputs = {
        sublocality_level_1: 'addressLine2Input',
        locality: 'cityInput',
        administrative_area_level_1: 'provinceInput',
        postal_code: 'postCodeInput'
    };

    var regex = /^.*(po\s*box|private\s*bag).*$|^\d[\/a-zĀ-ū0-9\s\,\'\-]*$/i;

    function initAutocomplete() {
        // Create the autocomplete object, restricting the search to geographical
        // location types.
        autocomplete = new google.maps.places.Autocomplete(
            /** @type {!HTMLInputElement} */
            (document.getElementById('addressLine1Input')), {
            types: ['geocode']
            });
        
        // When the user selects an address from the dropdown, populate the address
        // fields in the form.
        autocomplete.addListener('place_changed', fillInAddress);
        
    }

    // Break out out address components from autocomplete
    function fillInAddress() {
        // Get the place details from the autocomplete object.
        var place = autocomplete.getPlace();
        var short_address = "";
        var subprem = "";
        document.getElementById('checkout-shipping-continue').disabled = 'disabled';
      
        // Get each component of the address from the place details
        // and fill the corresponding field on the form.
        for (var i = 0; i < place.address_components.length; i  ) {

            var addressType = place.address_components[i].types[0];
            var str = document.getElementById("addressLine1Input").value;
            var match = str.match(regex);
          
            // Check if address type is in components we are looking for
            if (componentForm[addressType]) {
                
                var val = place.address_components[i][componentForm[addressType]];
                
                // Set generic address components in form inputs
                if (componentInputs[addressType]){
                    // document.getElementById(componentInputs[addressType]).value = val;
                    setNativeValue(document.getElementById(componentInputs[addressType]), val);
                    document.getElementById(componentInputs[addressType]).dispatchEvent(new Event('input', { bubbles: true }));
                }
                
                // Build short address string
                if(addressType == 'street_number') {
                
                    short_address = val;
                
                } else if (addressType == 'route') {
                
                    short_address  = " "   val;
                    short_address = short_address.trim();
                
                } else if (addressType == 'subpremise') {
            
                if (val !== undefined) {
                    var numberPattern = /\d /g;
                    var arr_match = val.match( numberPattern );
                    subprem = arr_match[0]   "/";
                }
              
            }
            
            // Validate full address selected
            if (match && !match[1]) {
                // If address is valid set to short address after breaking out the components
                // document.getElementById('addressLine1Input').value = (subprem   short_address);
                var str_addr = subprem   short_address;
                setNativeValue(document.getElementById('addressLine1Input'), str_addr);
                document.getElementById('addressLine1Input').dispatchEvent(new Event('input', { bubbles: true }));
                document.getElementById('addressLine1Input').focus();
                document.getElementById('addressLine1Input').blur();
            }
            
          }
        }
      
        // Run validation rules and UI prompts
        validate();
      
    }

    function validate() {

        // Get address value for validation
        var str = document.getElementById("addressLine1Input").value;

        if (str.indexOf(',') > -1) { 
            var arr_str = str.split(','); 
            str = arr_str[0];
            setNativeValue(document.getElementById('addressLine1Input'), str);
            document.getElementById('addressLine1Input').dispatchEvent(new Event('input', { bubbles: true }));
        }

        var match = str.match(regex);
        var lbl = document.getElementById('addressLine1Input-label');
        var short_address = document.getElementById('addressLine1Input');
        var shippingform = document.querySelectorAll('[data-form-type="shipping"]');

        // If address is valid
        if (match && !match[1]) {
            
            if(shippingform.length > 0){
                shippingform[0].setAttribute("onsubmit", "return validateMyForm(true);");
            }

            document.getElementById('checkout-shipping-continue').disabled = false;

            short_address.setAttribute('title', 'Address appears to be valid');
            short_address.setCustomValidity('');

            if (lbl.style.removeProperty) {
                lbl.style.removeProperty('color');
            } else {
                lbl.style.removeAttribute('color');
            }  

            lbl.innerHTML = "Address (No PO Box)";

        // else address is invalid
        } else {

            if(shippingform.length > 0){
                shippingform[0].setAttribute("onsubmit", "return validateMyForm(false);");
            }

            document.getElementById('checkout-shipping-continue').disabled = 'disabled';
            
            short_address.focus();
            short_address.setCustomValidity('No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive');
            short_address.setAttribute('title', 'No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive');

            lbl.style.color = '#b22222';
            lbl.innerHTML = "No PO Box or Private Bag address must start with a number, e.g. 1/311 Canaveral Drive";

        } 

    }

    function setNativeValue(element, value) {
        const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
        const prototype = Object.getPrototypeOf(element);
        const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;

        if (valueSetter && valueSetter !== prototypeValueSetter) {
            prototypeValueSetter.call(element, value);
        } else {
            valueSetter.call(element, value);
        }
    }

    function validateMyForm(answer){
        return answer;
    }


ready('#checkoutShippingAddress', function(element) {

    // Invoke autocomplete custom listener via JavaScript initAutocomplete(); must be done via JavaScript
    var lib = 'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete';

    if(!isLoadedScript(lib)){
        var js_file = document.createElement('script');
        js_file.type = 'text/javascript';
        js_file.src = lib;
        document.getElementsByTagName('head')[0].appendChild(js_file);
    }
    
    // Hit checkoutShippingAddress console flag
    console.log("You're on the shipping step!");

    /** @start HTML5 form validation **/ 

    // Target autocomplete form input
    let fulladdress = document.getElementById('addressLine1Input');

    fulladdress.addEventListener('blur',  () => {
        validate();
    });

    // Address validation must start with a number, case insensitive which includes letters A to Z, numbers (0-9), hyphen "-", forward slash "/" as well as Maori accented characters for Maori vowels ā, ē, ī, ō, ū
    fulladdress.setAttribute("pattern", "\\d[/a-zA-ZĀ-ū0-9\\s',-]*");
    fulladdress.setAttribute("onblur", "validate()");

    // event listener to clear error message for input
    fulladdress.addEventListener('input', () => {
        fulladdress.setCustomValidity('');
        fulladdress.checkValidity();
    });

    // event listener to invoke validation and show error message if needed
    fulladdress.addEventListener('invalid', () => {
        fulladdress.setCustomValidity('No PO Box or Private Bag, address must start with a number, e.g. 1/311 Canaveral Drive');
    });

    
    /** @end HTML5 form validation **/

    // Detect if library loaded
    
    function isLoadedScript(lib) {
        var script = document.querySelectorAll('[src="'   lib   '"]');

        if(script.length > 0){
            script[0].remove();
        }
        
        return document.querySelectorAll('[src="'   lib   '"]').length > 0;
    }

    // Bias the autocomplete object to the user's geographical location,
    // as supplied by the browser's 'navigator.geolocation' object.
    function geolocate() {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function(position) {
                var geolocation = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
                };
                var circle = new google.maps.Circle({
                center: geolocation,
                radius: position.coords.accuracy
                });
                autocomplete.setBounds(circle.getBounds());
            });
        }
    }

});   
</script>
  • Related