Addresses are very important when it comes to business. It becomes more
important when we are running high profile business and capturing the correct
address information is utmost important. When we go for any address lookup,
what best it can be if the search is able to serve for
auto-fill and auto-predict. Google Map API, specially the Google Place API allows us to
get recommendations based on the user input for the address search and
the place details for selected recommendation. This information can
then be stored for our business purpose.
This blog explains how we can use the Google Place API for
recommendations and place details. We will use Account to
show the demo on how can we use this address tool for capturing the
address details for the Billing Address. This can then be
extended to any custom implementation and any custom object.
Steps involved in the setup:
- API Setup on Google G-Suite Account.
- Create Apex Class: To call the Google Map API to get recommendation and place details.
- Create Lightning Web Component for user experience.
API Setup:
- To use the Google API, we need to have our account set up on Google G-Suite or Google Cloud. Follow the steps as explained under Setup in Cloud Console.
- Once you have your project ready and linked with the billing account, you project is ready to use. You will need to get an API key to be used in order to consume the Google API services. Read Using API Key documentation to get an API Key.
- Make sure you enable the Google Places API.
- This Google Place API has two endpoints
-
Recommendations: This helps us to get the address
recommendation as the user types in for the address search. Use
following GET service from
Google Place API
to get recommendations.
https://maps.googleapis.com/maps/api/place/autocomplete/json?input=SEARCH_TEXT&key=YOUR_API_KEY
Here SEARCH_TEXT is user input for address search and YOUR_API_KEY is the API key that you get as in Step 2.
-
Place Details: This helps us to get the place details with full
information. Use following GET service from
Google Place API
to get place details
https://maps.googleapis.com/maps/api/place/details/json?placeid=PLACE_ID&key=YOUR_API_KEY
Here PLACE_ID is selected place from available options for address recommendations and YOUR_API_KEY is the API key that you get as in Step 2.
Create APEX Class:
Create an APEX class that will call the Google Map API for address details.
In order to consume the Google Place API in our Salesforce application, we
need to follow below steps.
- Lightning components cannot reach out to Places API directly. Google doesn’t allow CORS to their endpoints – so we simply cannot do this. Nor would we want to – because if we do this via JavaScript – we will be exposing our keys. So, we need the server to do the calls.
- We need to set up remote site setting to make a callout to Google Maps API.
- Remote Site Name : GoogleMapAPI
- Remote Site URL: https://maps.googleapis.com
-
Create APEX class named : AddressSearchController
public class AddressSearchController { //Method to call Google Map API and fetch the address recommendations @AuraEnabled public static String getAddressRecommendations(String searchText){ String apiKey = '';//YOUR_API_KEY String result = null; try{ if(searchText != null){ String apiUrl = 'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=' + searchText.replace(' ', '%20') + '&key=' + apiKey; HttpRequest req = new HttpRequest(); req.setMethod('GET'); req.setEndpoint(apiUrl); Http http = new Http(); HttpResponse res = http.send(req); Integer statusCode = res.getStatusCode(); if(statusCode == 200){ result = res.getBody(); } } } catch(exception e){ System.debug(e.getMessage()); } return result; } //Method to call Google Map API and fetch the address details by placeId @AuraEnabled public static String getAddressDetailsByPlaceId(String placeId){ String apiKey = '';//YOUR_API_KEY String result = null; try{ if(placeId != null){ String apiUrl = 'https://maps.googleapis.com/maps/api/place/details/json?placeid=' + placeId.replace(' ', '%20') + '&key=' + apiKey; HttpRequest req = new HttpRequest(); req.setMethod('GET'); req.setEndpoint(apiUrl); Http http = new Http(); HttpResponse res = http.send(req); Integer statusCode = res.getStatusCode(); if(statusCode == 200){ result = res.getBody(); } } } catch(exception e){ System.debug(e.getMessage()); } return result; } //Method to update the address on Account @AuraEnabled public static void updateAddressDetails(String jsonAddress) { Address addressDetail = (Address)JSON.deserialize(jsonAddress, Address.Class); if(addressDetail != null && !String.isBlank(addressDetail.Id)) { Account acc = new Account(); acc.Id = addressDetail.Id; acc.BillingState = addressDetail.state; acc.BillingCity = addressDetail.city; acc.BillingCountry = addressDetail.country; acc.BillingPostalCode = addressDetail.postalCode; acc.BillingStreet = addressDetail.streetNumber; update acc; } } public class Address { public String Id {get; set;} public String city {get; set;} public String country {get; set;} public String state {get; set;} public String postalCode {get; set;} public String subLocal2 {get; set;} public String subLocal1 {get; set;} public String streetNumber {get; set;} public String route {get; set;} } }
Create Lightning Web Component:
Create a lightning web component named addressSearch for a
user experience which will allow a search box to enter for the address search.
This will read the APEX class created above and provide the recommendations
for the place selection. Once the place is selected, the place details API
will get called and all related information is then mapped to
Billing Address on Account record.
addressSearch.html
<template>
<!-- Address Information Header -->
<div class="slds-grid slds-wrap">
<div class="slds-col">
<div class="slds-page-header">
<h1 class="slds-list_horizontal">
<span class="slds-page-header__title slds-truncate" title="Address Information"
style="width: 100%">Address
Information</span>
<lightning-button class="slds-float_right" label="Edit" title="Edit" slot="actions"
onclick={handleModal}></lightning-button>
</h1>
</div>
</div>
</div>
<!-- Address Information Details -->
<div class="addressInformation">
<lightning-record-edit-form record-id={recordId} object-api-name={objectApiName}>
<lightning-layout multiple-rows>
<template for:each={fieldApiNames} for:item="fieldApiName">
<lightning-layout-item key={fieldApiName} flexibility="auto" padding="around-small" size="6"
large-device-size="6" medium-device-size="6">
<lightning-output-field class="slds-form-element_readonly" field-name={fieldApiName}>
</lightning-output-field>
</lightning-layout-item>
</template>
</lightning-layout>
</lightning-record-edit-form>
</div>
<!-- Address Search Modal-->
<div class="openModal" if:true={openModal}>
<section role="dialog" tabindex="-1" class="slds-modal slds-fade-in-open" aria-labelledby="modal-heading-01"
aria-modal="true" aria-describedby="modal-content-id-1">
<div class="slds-modal__container">
<header class="slds-modal__header">
<button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
title="Close" onclick={closeModal}>
<lightning-icon icon-name="utility:close" alternative-text="close" variant="inverse"
size="small">
</lightning-icon>
<span class="slds-assistive-text">Close</span>
</button>
<h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Address Search</h2>
</header>
<div class="slds-modal__content slds-var-p-around_medium" id="modal-content-id-1">
<lightning-input type="search" variant="label-hidden" class="searchAddress" name="searchAddress"
placeholder="Search Address.." onchange={handleChange} value={selectedAddress}>
</lightning-input>
<!-- Address Recommendations -->
<div if:true={hasRecommendations}>
<div class="address-recommendations" role="listbox">
<ul class="slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid"
role="presentation">
<template for:each={addressRecommendations} for:item="addressRecommendation">
<li key={addressRecommendation} role="presentation"
onclick={handleAddressRecommendationSelect}
data-value={addressRecommendation.place_id} class="slds-listbox__item">
<span
class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta"
role="option">
<span class="slds-media__body slds-m-left_xx-small slds-m-bottom_xx-small">
<div class="slds-grid slds-m-bottom_small">
<div class="slds-col slds-size_1-of-10">
<lightning-button-icon size="medium" icon-name="utility:checkin"
class="slds-input__icon" variant="bare">
</lightning-button-icon>
</div>
<div class="slds-m-left_medium slds-col slds-size_8-of-10">
<span
class="slds-listbox__option-text slds-listbox__option-text_entity"><b>{addressRecommendation.main_text}</b></span>
<span
class="slds-listbox__option-text slds-listbox__option-text_entity slds-m-top_xxx-small">{addressRecommendation.secondary_text}</span>
</div>
<div class="slds-col slds-size_1-of-10"></div>
</div>
</span>
</span>
</li>
</template>
</ul>
</div>
</div>
</div>
<footer class="slds-modal__footer">
<lightning-button class="slds-button" label="Cancel" onclick={closeModal} variant="neutral">
</lightning-button>
<lightning-button class="slds-button" label="Save" onclick={saveAddress} variant="brand">
</lightning-button>
</footer>
</div>
</section>
<div class="slds-backdrop slds-backdrop_open"></div>
</div>
<!-- Lightning Spinner -->
<div class="showSpinner" if:true={showSpinner}>
<lightning-spinner alternative-text="Loading" variant="brand"></lightning-spinner>
</div>
</template>
addressSearch.js
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAddressRecommendations from '@salesforce/apex/AddressSearchController.getAddressRecommendations';
import getAddressDetailsByPlaceId from '@salesforce/apex/AddressSearchController.getAddressDetailsByPlaceId';
import updateAddressDetails from '@salesforce/apex/AddressSearchController.updateAddressDetails';
export default class AddressSearch extends LightningElement {
@api recordId;
@api objectApiName;
openModal = false;
showSpinner = false;
fieldApiNames = ['BillingStreet', 'BillingCity', 'BillingState', 'BillingPostalCode', 'BillingCountry'];
addressRecommendations;
selectedAddress = '';
addressDetail = {};
get hasRecommendations() {
return (this.addressRecommendations !== null && this.addressRecommendations.length);
}
handleChange(event) {
event.preventDefault();
let searchText = event.target.value;
if (searchText) this.getAddressRecommendations(searchText);
else this.addressRecommendations = [];
}
getAddressRecommendations(searchText) {
getAddressRecommendations({ searchText: searchText })
.then(response => {
response = JSON.parse(response);
let addressRecommendations = [];
response.predictions.forEach(prediction => {
addressRecommendations.push({
main_text: prediction.structured_formatting.main_text,
secondary_text: prediction.structured_formatting.secondary_text,
place_id: prediction.place_id,
});
});
this.addressRecommendations = addressRecommendations;
}).catch(error => {
console.log('error : ' + JSON.stringify(error));
});
}
handleAddressRecommendationSelect(event) {
event.preventDefault();
let placeId = event.currentTarget.dataset.value;
this.addressRecommendations = [];
this.selectedAddress = '';
getAddressDetailsByPlaceId({ placeId: placeId })
.then(response => {
response = JSON.parse(response);
response.result.address_components.forEach(address => {
let type = address.types[0];
switch (type) {
case 'locality':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.city = address.long_name;
break;
case 'country':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.country = address.long_name;
break;
case 'administrative_area_level_1':
this.selectedAddress = this.selectedAddress + ' ' + address.short_name;
this.addressDetail.state = address.short_name;
break;
case 'postal_code':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.postalCode = address.long_name;
break;
case 'sublocality_level_2':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.subLocal2 = address.long_name;
break;
case 'sublocality_level_1':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.subLocal1 = address.long_name;
break;
case 'street_number':
this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
this.addressDetail.streetNumber = address.long_name;
break;
case 'route':
this.selectedAddress = this.selectedAddress + ' ' + address.short_name;
this.addressDetail.route = address.short_name;
break;
default:
break;
}
});
})
.catch(error => {
console.log('error : ' + JSON.stringify(error));
});
}
handleModal(event) {
event.preventDefault();
this.openModal = true;
this.addressRecommendations = [];
}
closeModal(event) {
event.preventDefault();
this.openModal = false;
this.addressRecommendations = [];
}
saveAddress(event) {
event.preventDefault();
this.openModal = false;
this.showSpinner = true;
this.addressDetail.Id = this.recordId;
updateAddressDetails({ jsonAddress: JSON.stringify(this.addressDetail) })
.then(() => {
const event = new ShowToastEvent({
title: 'Success',
message: 'Account address is updatd successfully.',
variant: 'success'
});
this.dispatchEvent(event);
this.showSpinner = false;
})
.catch(error => {
const event = new ShowToastEvent({
title: 'Error',
message: 'An error has occured in saving the address.',
variant: 'error'
});
this.dispatchEvent(event);
console.log('error : ' + JSON.stringify(error));
this.showSpinner = false;
});
}
}
addressSearch.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
Demo:
Edit the Account Record page, and drag-drop your newly created
component addressSearch below the details section.
If you like this blog content and find inciteful, please comment and let me know.
Very informative Blog
ReplyDeleteThank you for sharing your knowlwdge :)
ReplyDeleteI really enjoy the blog article.Much thanks again.
ReplyDeletespring boot training
best spring boot online course
This comment has been removed by the author.
ReplyDeleteThis is really cool but I am having trouble restricting access to the Google api key based on http referrer or IP address. The apex call does not seem to come from my SF domain. Anyone have suggestions?
ReplyDelete