Let’s Build an AutoSuggest Lightning Component with Google Places API

I had the challenge of building an input form that brings up a list of cities as you type. One that has the coordinates of each city – because I’ll need to enter that into a map. An “auto-suggest” or “auto-complete” city input – inside a custom form inside Salesforce. It turns out, that it wasn’t so bad.

We’re using Google Places for our API and Lightning Components for our Front End. If you’re not familiar with Lightning, check their documentation.

Our component will look like below.

lightning input autosuggest

Ready to start coding? Let’s start.

API Setup

Let’s get Google going. Remember, each time a user type a character – it will ping Google for suggestions. Also, the suggestions does not contain coordinates for each item, so we will need to ping the API as soon as the user selects a place.

I’m going to assume that you know how to get an API key from your Google account. If not, here is their API documentation.

The first part is getting the suggestions. Below is the URL that we need for that:

https://maps.googleapis.com/maps/api/place/autocomplete/json?input=TERM&key=YOURKEY

Looking at the above, we’re passing in what the user is typing where it says “TERM”.
The second part is when the user selects an item:

https://maps.googleapis.com/maps/api/place/details/json?placeid=PLACEID&key=YOURKEY

The above shows a “placeid” parameter that we get from each suggestion. So when the user clicks – we pass the id to the above URL.

Now that we have our Places API setup. Let’s move forward to our Salesforce code.

Build the Apex Class

One thing to note is that our Lighting 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. In Salesforce, we use Apex.

We also have to add “maps.googleapis.com” to the remote site settings in Salesforce. To do this, go to “Setup” > “Security” > “Remote Site Settings”.

Click on “New Remote Site” and add the required fields:

Remote site settings - Salesflorce

Now, let’s create our Apex Class. Copy the code below and add it to your Class:

@AuraEnabled
	public static string getSuggestions(String input) {
	    String url = 'https://maps.googleapis.com/maps/api/place/autocomplete/json?'
	            + 'input=' + EncodingUtil.urlEncode(input, 'UTF-8')
	            + '&types=(cities)' + getKey();
	    String response = getResponse(url);
	    return response;
	}
@AuraEnabled
	public static string getPlaceDetails(String placeId) {
     	String url = 'https://maps.googleapis.com/maps/api/place/details/json?'
	            + 'placeid=' + EncodingUtil.urlEncode(placeId, 'UTF-8')
	            + getKey();
	    String response = getResponse(url);
	    return response;
	}
	public static string getResponse(string strURL){
		Http h = new Http();
		HttpRequest req = new HttpRequest();
		HttpResponse res = new HttpResponse();
		req.setMethod('GET');
		req.setEndpoint(strURL);
		req.setTimeout(120000);
		res = h.send(req);
		String responseBody = res.getBody();
		return responseBody;
	}
	public static string getKey(){
		string key = 'xxxxxxxxxxxxx';
		string output = '&key=' + key;
		return output;
	}

Note the two methods that have @AuraEnabled: getSuggestions() and getPlaceDetails(). These are the two endpoints we will be calling from our JavaScript in our Lightning component. Both methods are quite explanatory, they simply accept parameters from our front end, go out to Google and return the results.

Don’t forget to add your API key to the method getKey().

Let’s move on.

Our Component

The component (.cmp file) will need a few things. First, we need a couple of attributes to hold our values. One for location and one called “predictions”. The location is the value that will be in our input, while the predictions is what will show directly beneath it – in an unordered list. Note that this is what comes back from our API.  So add that first to your component.

<aura:attribute name="location" type="string" default=""/>
<aura:attribute name="predictions" type="List" default="[]"/>

We’re using “lightning:input” for our form field. We add a label, name, an aura:id, a value and an onchange attribute. The label and name is pretty straightforward, the aura:id – is simply a way for us to reference it in our controller.

The value – binds the input to our attribute “location”. Which we show as “v.location“.

The onchange – we’re attaching to a function in our controller named getCities(), which we refer to as “c.getCities“.

<lightning:input label="Location"
                        name="location"
                        aura:id="location"
                        value="{!v.location}"
                        onchange="{!c.getCities}" />

We’re using an unordered list for our suggestions. So we use the code below:

<aura:if isTrue="{!v.predictions.length > 0}">
 	<ul class="city_predictions">
		<aura:iteration items="{!v.predictions}" var="prediction">
			<li class="slds-listbox__item">
				<a onclick="{!c.getCityDetails}" data-placeid="{!prediction.place_id}">{!prediction.description}</a>
			</li>
		</aura:iteration>
	</ul>
</aura:if>

Notice we wrap it an “aura:if”  – which determines if our predictions object has more than zero length. Remember, predictions is coming straight from the API.

Then we have an “aura:iteration” for each prediction to show up in a list item > anchor tag. Each anchor tag has an “onclick” (which we’re attaching to getCityDetails()) and a “data-placeid” (I’ll explain later).

The Controller

So remember in our component we have 2 methods: getCities() and getCityDetails()? Those both go in our controller.

Our getCities() method is called every time we change the input field. Meaning – every time we type a letter, it calls this method.

First it sets up an object called “params“, which has one item in it called “input“. This is what we’re sending our Apex class through the use of helper.callServer(). This helper is explained in my previous post “Call Apex from Lightning“. Once our Apex responds with some suggestions – we parse it through JSON.parse() and set it to our component attribute called “predictions”.

getCities : function(component, event, helper){
	var params = {
	  "input" : component.get('v.location')
	}
	helper.callServer(component,"c.getSuggestions",function(response){
		var resp = JSON.parse(response);
		console.log(resp.predictions);
		component.set('v.predictions',resp.predictions);
	},params);
}

So our UL of suggestions will only populate if there’s a response from the server.

Finally, getCityDetails() get’s triggered when we choose an item from our UL (or click the anchor tag).
Remember the “data-placeid” attribute that we have for each anchor tag? This is where we will need it.

We grab the value of “data-placeid” (which is the Place Id required for our API) through the event.currentTarget. We then create the “params” object like we did above.

getCityDetails : function(component, event, helper){
	var selectedItem = event.currentTarget;
        var placeid = selectedItem.dataset.placeid;
	var params = {
	   "placeId" : placeid
	}
	helper.callServer(component,"c.getPlaceDetails",function(response){
		var placeDetails = JSON.parse(response);
		component.set('v.location',placeDetails.result.name);
		component.set('v.predictions',[]);
	},params);
}

We then do a helper.callServer() and when finished, we parse then set the v.location (the input value) and v.predictions back to empty.

I specifically needed the coordinates for the selected location, which can easily be grabbed from the response. You can do something similar.

The Final Product:

Below is a short screengrab of the final output. The lightning:input makes our field look better than just a regular input. Also, this is a “barebones” solution for an auto suggest field. You can greatly enhance by adding icons, additional description for each place, maybe some more events (like on tab, enter etc).


Lightning Auto Suggest

14 Comments

  1. Hi Michael, I’m getting this error when implementing this on my own org: helper.callServer is not a function.
    What do you supposed I am missing here? TIA!

    Reply
  2. Hi Michael,
    Many thanks for this elegant solution and great example.
    I, too, was thrown by the helper.callServer hiccup but soon found your page about it.
    I also stumbled on Win.get… functions in the class definition until I realised that it represented the name of the ‘controller’ class we’d created and also that that controller class had to be named in the lightning component.
    e.g.
    I also added implements=”force:appHostable,flexipage:availableForAllPageTypes” so I could add it to pages and, hopefully, mobile devices…
    I added the display of latitude and longitude to the component:
    You selected: {!v.location} – {!v.latitude},{!v.longitude}
    and in method getCityDetails
    component.set(‘v.latitude’,placeDetails.result.geometry.location.lat);
    component.set(‘v.longitude’,placeDetails.result.geometry.location.lng);
    All working now.
    I’ll be using this to verify my addresses before using the new lightning map component which fails if the addresses are invalid. I’ll have to do a check on each address I want to map.
    So, once again, many thanks.

    Reply
  3. Could you explain what “Win.getKey()” is exactly? I know you’re supposed to plugin your key in there but I am getting a “Variable does not exist: Win” error upon saving. Thanks!

    Reply
  4. In this example, I can probably easily get your ‘key’. What security is in place to prevent me from finding your key and then using it myself? Is there configuration on the google side that only allows requests from certain domains?

    Reply
    • The key is in server-side code. It is not viewable by client. I agree that it shouldn’t be hardcoded, there are ways to store this elsewhere that is more secure. But that goes beyond the scope of the tutorial.

      Reply

Leave a Comment.