Search code examples
javascriptgoogle-mapsbigcommerce

How to create your own custom Google address autocomplete in BigCommerce One Step Checkout


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>


Solution

  • 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>