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:
- 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 ā, ē, ī, ō, ū
- 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>