Example of Auto-Complete in MAF using Google Places

This requirement came up in a customer project recently and I thought it was worth blogging due to some of the issues found and also the ease with which it was implemented - once you have the workaround!

Background

To provide some context; we were building an in-store member sign up mobile app that can be used to validate and create new memberships directly into their CRM application. This replaces the current paper-based system, reducing turn-around time and double data entry.  So, some good benefits, but one of the issues this does introduce is then losing the ability to enforce consistency on addresses that are entered. This is currently controlled in the CRM through an embedded address validation service that is used when the Customer Service Reps enter the address details. Ideally we would want to replicate this experience in the app.

The reason for looking at using the Google Places API was that the address validation system they currently use charges per request. The customer was interested in investigating what cheaper options exist and whether these alternatives could provide the required functionality.

Google Places

Details about Google Places can be found at https://developers.google.com/places/. The important thing to remember is that when running MAF, the app is actually working as a web application. This means we are using the Places Javascript library and the licensing details relevant to our use are herehttps://developers.google.com/maps/documentation/javascript/usage. Essentially we are good for up to 25,000 requests a day (which is more than adequate for our needs!).

 

Building the sample App

Let’s step through building an app containing a welcome page and an address entry page. The intention is to show:-

  • How to build a page with address entry from Google Places
  • How to correctly embed the required Javascript
  • Show how the styling can be controlled

 

Step 1. Create the MAF app

We’re going to set up an app with a single feature, which navigates to a couple of pages. Step through the new MAF app wizard – I have called my app PlacesTest and given it an application prefix of com.rubiconred.test.placestest. Let’s create a feature called welcome and then set it to be an amx page flow.

1. Create MAF Feature

 

2. Create Task Flow

 

Set the default page to welcome and then add another page called address. Link the two with an action called addAddress.

3. Edit Task Flow

We’ll update the default secondary button to go to the address page.

4. Add Button

 

Before creating the Address page we’ll need to create a Managed Bean to store our address data onto. Create a java class called AddressBean and set the fields as follows:

 

public class AddressBean {

   private String address1;

   private String address2;

   private String suburb;

   private String state;

   private String country;

   private String postcode;

   private boolean overrideAddress;

   public AddressBean() {

    super();

  }

}

 

Add the getter/setters and tick ‘Notify Listeners when property changes’ to ensure that changes to the objects are reflected back onto the UI automatically.

Next we’ll go to the task flow definition and set this bean to be into the pageFlowScope.

5. Add Managed Bean

 

Finally, let’s create the address page. Here we’ll need to have the field for entering the address and then a mapping for each of the managed beans fields.

 

<amx:view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amx="http://xmlns.oracle.com/adf/mf/amx"

xmlns:dvtm="http://xmlns.oracle.com/adf/mf/amx/dvt">

  <amx:panelPage id="pp1">

    <amx:facet name="header">

<amx:outputText value="Address Entry" id="ot1"/>

    </amx:facet>

    <amx:facet name="primary">

<amx:commandButton id="cb1" action="__back"/>

    </amx:facet>

<amx:panelGroupLayout id="pgenteraddress" layout="horizontal">

<amx:inputText label="" id="autocomplete" value="" hintText="Enter Address To Search Here"/>

</amx:panelGroupLayout>

<amx:panelFormLayout id="pfaddressfields">

    <amx:inputText label="Address 1" id="it1" value="#{pageFlowScope.addressBean.address1}"/>

    <amx:inputText label="Address 2" id="it2" value="#{pageFlowScope.addressBean.address2}"/>

    <amx:inputText label="Suburb" id="it3" value="#{pageFlowScope.addressBean.suburb}"/>

    <amx:inputText label="State" id="it4" value="#{pageFlowScope.addressBean.state}"/>

    <amx:inputText label="Postcode" id="it5" value="#{pageFlowScope.addressBean.postcode}"/>

    <amx:inputText label="Country" id="it6" value="#{pageFlowScope.addressBean.country}"/>

</amx:panelFormLayout>

</amx:panelPage>

</amx:view>

 

OK – at this point we have an app with a welcome screen and the ability to navigate back and forth to an address page. On this address page we have a field we’ll use for the autocomplete component and then a form containing the fields we’ll be updating.

 

Step 2. Add Places API

Google provide an out of the box example of how to implement address autocomplete and this can be found at https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete-addressform . Essentially the approach will be to copy the javascript code provided into a new javascript file and then add it to the feature.  On the page with the address it’ll also need to include the reference to the javascript library, i.e.

<script src="https://maps.googleapis.com/maps/api/js?v=3.exp&signed_in=true&libraries=places"></script>

This could  be achieved by adding  a verbatim to the page,

<amx:verbatim id="v1">

    <script type='text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=places"/>

</amx:verbatim>

or by pointing to the url as part of the include of the javascript as shown below.

6. Javascript Include

 

However – neither of these will work.

What will happen when you deploy the app is that the page will try to load the javascript and end up being replaced with a blank page. The content will be

<html><head><script src="https://maps.gstatic.com/maps-api-v3/api/js/20/11b/intl/en_au/main.js" style=""></script><script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11b/intl/en_au/common.js"></script><script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11b/intl/en_au/util.js"></script><script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11b/intl/en_au/stats.js"></script></head></html>

Due to the single-threaded nature of the Javascript engine, the synchronous loading of external script blocks the page from rendering and hence the issue. This can be confirmed by debugging the page loading through a Web Inspector like Safari Web Inspector. 6b. Javascript Include

Solution

The solution is to load the external javascript using the JQuery getScript function; this loads the contents of the external file using an AJAX request(asynchronous) and then evaluates the script code at run time. Add a verbatim to the page and put the following

<amx:verbatim id="v1">

    <![CDATA[

         <script type="text/javascript">

            /**

             * Loading the google api javascript library asynchronously(through ajax) as the page hangs during synchronous load.

             **/

$.getScript("https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=true&libraries=places&async=2&callback=placesAPILoadedCallback");

            var autocomplete,placeSearch;

            function placesAPILoadedCallback()

            {

               //MAF adds '__inputElement' suffix to the input html component during runtime.

               var addressTextField =document.getElementById('autocomplete__inputElement');

               addressTextField.setAttribute('placeholder', "Enter Customer Address here...");

                  // Set some bounds that wrap Australia

               var defaultBounds = new google.maps.LatLngBounds(new google.maps.LatLng(-43.324423, 108.971466), new google.maps.LatLng(-12.725072, 156.432405));

               var options = {

bounds: defaultBounds,

types: ['geocode']

                };

//Setting up the autocomplete google places feature.

autocomplete = new google.maps.places.Autocomplete(addressTextField,options);

//Adding the listener to fill the address once the search is complete & the place is selected.

google.maps.event.addListener(autocomplete, 'place_changed', function () {

                             var place = autocomplete.getPlace();

            });

      }

    function onFail() {    };

    function onInvokeSuccess(param) { };

</script>

    ]]>

</amx:verbatim>

Note that the autocomplete field has changed from the original Google script to be autocomplete__inputElement – this is because MAF will add its own mappings to the data and turn the input element id from ‘autocomplete’ at design time into  'autocomplete__inputElement' at run time.

 

Step 3. Test the App

Deploy the app and navigate to the address page. You’ll see the background hint text switch from the version in the MAF page definition ‘Enter Address To Search Here’ to the version in the Auto Complete code ‘Enter Customer Address here’. Ideally we would correct these to be the same, however we'll leave for now as it's useful for highlighting an issue later on.

Start typing “1 TEST  “ and a drop down list of addresses will appear. These addresses are Australia based due to the defaultBounds that we set up as part of the script.

7. Address Search

 

Choosing an entry will populate the entry field with the full address. We now need to add in the code we require to populate the fields.

 

Step 4. Getting the Data

We need to update the code, to modify the event listener that handles the selection event. Add the function fillInAddress() as shown below.

//Adding the listener to fill the address once the search is complete & the place is selected.

google.maps.event.addListener(autocomplete, 'place_changed', function () {

var place = autocomplete.getPlace();

// Copy the place result onto the fields

fillInAddress();

});

Then add a new javascript function at the bottom of the script.

function fillInAddress() {

            // Get the place details from the autocomplete object.

            var place = autocomplete.getPlace();

            // Get each component of the address from the place details

            // and fill the corresponding field on the form.

            aFields = ["street_number", "route", "locality", "administrative_area_level_1", "postal_code"];

            aValues = ["","","","",""];

            for (var i = 0;i < place.address_components.length;i++) {

                 var addressType = place.address_components[i].types[0];

                for (var j=0; j<aFields.length; j++) {

                    if (aFields[j]==addressType) {

                    aValues[j] = place.address_components[i].long_name;

                    }

                }

     }

             // pass these to the bean to update

            adf.mf.api.invokeMethod("au.com.fsma.memberkiosk.mobile.beans.NewMemberSupportBean", "updateAddressFromLookup", aValues[0] + " " + aValues[1], aValues[2], aValues[3], aValues[4], onInvokeSuccess, onFail);

}

Essentially this function defines the fields from the Places object that we are interested in and then loops through copying these field values into an array. The MAF javascript library function adf.mf.api.invokeMethod is used to invoke the method updateAddressFromLookup on the AddressBean managed bean.

Next we need to add this new method  to the Address Bean, so go back to the java class and add the following

public void updateAddressFromLookup(String address1, String suburb, String state, String postcode) {

    ValueExpression ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.address1}", String.class);

   ve.setValue(AdfmfJavaUtilities.getAdfELContext(), address1);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.address2}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), "");

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.suburb}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), suburb);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.state}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), state);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.postcode}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), postcode);

 }

And that’s it. On selection of the address from the drop down the event listener will fire and invoke the function fillInAddress that passes data from the page to the managed bean. When we defined the getter/setters earlier, we selected to ‘Notify Listeners when property changes ‘. This means that the call to set the address values will trigger the update of the UI through the firePropertyChange method as shown in the code sample below.

public void setAddress1(String address1) {

        String oldAddress1 = this.address1;

        this.address1 = address1;

        propertyChangeSupport.firePropertyChange("address1", oldAddress1, address1);

}

 

Deploy the code and test by selecting one of the addresses displayed.

 8. Address Selected 

 

It is working although we seem to have lost some of the address details!  This has happened due to the way the Places API will ignore the street number element and instead focuses mainly on the street name.  In this instance it isn't a big issue as we are trying to capture addresses from people – they should know if their address is correct!

 

At this point it is worth pointing out some of the other options available; the type of addresses to retrieve can be changed through the Places options. Currently the setting is geocode but can be restricted to options such as address or establishment.  These will restrict results to precise addresses or business addresses respectively. The bounds was also set to a rough area surrounding Australia; this approach biases results to the bounded region rather than restricted to only the region. This suits what was required but could have been more restrictive and used a country code to only allow Australian addresses.

 

Finally – note that if you don’t use a bound parameter then the user IP Address will be used to bias the results.  More details can be found athttps://developers.google.com/maps/documentation/javascript/places-autocomplete

 

Step 5. Improving the Page

Ideally there are a few more things needed before the page could be seen as ready for use. There is no validation of the data (e.g. meaning addresses without postcodes are possible) and the fields are in edit mode. Ideally this should be more controlled and only allow editing when required. Let’s trying adding some validation and read only mode.

Add the following above the pgentryaddress panel group

<amx:panelGroupLayout id="pgl1" layout="horizontal" inlineStyle="padding-right:8px" halign="end">

<amx:selectBooleanSwitch label="Override Address" id="sbs1" offLabel="No" onLabel="Yes"

                                         value="#{pageFlowScope.addressBean.overrideAddress}"

inlineStyle="color:#c6c6c6"/>

</amx:panelGroupLayout>

This will put a toggle above the field which allows the user to override the address.

Change the fields in the form to enable read only mode when this flag is set to false. Do this by adding the following to each field:-

readOnly="#{pageFlowScope.addressBean.overrideAddress==false}"

Ideally, we should also hide the search box if we are in edit mode. So, update the panel group panel containing the search box as follows:

<amx:panelGroupLayout id="pgenteraddress" layout="horizontal" rendered="#{pageFlowScope.addressBean.overrideAddress==false}">

Redeploy the app and test the toggle.

9. Toggle No              10. Toggle Yes

 

Notice that when you re-enable the toggle back to No, the auto complete code has disappeared. It is now showing the message we set as the default for ADF.  This is happening due to the way MAF updates the screen and loads the search field back in. A quick fix is to add a new function that will invoke the placesAPILoadedCallback javascript function.

 

function resetPlacesField() {

setTimeout(function(){placesAPILoadedCallback()}, 1000);

}

 

This will then be called from the setter for override Address.

    public void setOverrideAddress(boolean overrideAddress) {

        boolean oldOverrideAddress = this.overrideAddress;

        this.overrideAddress = overrideAddress;

        propertyChangeSupport.firePropertyChange("overrideAddress", oldOverrideAddress, overrideAddress);

        if (overrideAddress==false) {

                        AdfmfContainerUtilities.invokeContainerJavaScriptFunction("com.rubiconred.test.placestest.welcome","resetPlacesField", new Object[] {} );

        }

    }

 

The reason for the 1 second delay is to give the search field time to be rendered before trying to add on the autocomplete settings.

Finally, the country field isn’t being retrieved or set. Let’s add that as a parameter to the array in the javascript, and then as a parameter in the AddressBean method.  [Details about what data is available in the PlacesResult object can be found at https://developers.google.com/maps/documentation/geocoding/#Types].

 

  function fillInAddress() {

 

            // Get the place details from the autocomplete object.

            var place = autocomplete.getPlace();

            // Get each component of the address from the place details

            // and fill the corresponding field on the form.

            aFields = ["street_number", "route", "locality", "administrative_area_level_1", "postal_code", "country"];

            aValues = ["","","","",""];

            for (var i = 0;i < place.address_components.length;i++) {

//alert('here and ' + place.address_components[i]);

                var addressType = place.address_components[i].types[0];

                for (var j=0; j<aFields.length; j++) {

                    if (aFields[j]==addressType) {

aValues[j] = place.address_components[i].long_name;

                    }

                }

           }

           // pass these to the bean to update

adf.mf.api.invokeMethod("com.rubiconred.test.placestest.mobile.AddressBean", "updateAddressFromLookup", aValues[0] + " " + aValues[1], aValues[2], aValues[3], aValues[4], aValues[5], onInvokeSuccess, onFail);

}

 

Update the Managed Bean

 

    public void updateAddressFromLookup(String address1, String suburb, String state, String postcode, String country) {

    ValueExpression ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.address1}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), address1);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.address2}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), "");

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.suburb}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), suburb);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.state}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), state);

     ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.postcode}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), postcode);

       ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.addressBean.country}", String.class);

ve.setValue(AdfmfJavaUtilities.getAdfELContext(), country);

    }

Deploy and test to confirm that Country is now coming through OK.

 

11. Country

 

Step 6. Styling the results

There are 7 css classes used to style the autocomplete list. See https://developers.google.com/maps/documentation/javascript/places-autocomplete#place_autocomplete_service for a detailed diagram and explanation. To add styling we will create a new autocomplete.css and add this to the feature references.

12. new css

Create autocomplete.css and add the following content.

 

/* See https://developers.google.com/maps/documentation/javascript/places-autocomplete#place_autocomplete_service */

/*The visual element containing the list of predictions returned by the Place Autocomplete service. This list appears as a dropdown list below the Autocomplete or SearchBox widget.*/

.pac-container {

background-color: rgb(242, 242, 245);

position: absolute!important;

z-index: 1000;

border-top-left-radius: 0px;

border-top-right-radius: 0px;

border-bottom-left-radius: 20px;

border-bottom-right-radius: 20px;

border-top: 1px solid #d9d9d9;

font-family: Arial;

box-shadow: 10px 22px 16px rgba(0,0,0,0.3);

-moz-box-sizing: border-box;

-webkit-box-sizing: border-box;

box-sizing: border-box;

overflow: hidden;

padding: 20px;

}

/* The icon displayed to the left of each item in the list of predictions. */

.pac-icon {

 }

/* The actual definition of the icon offset to the image https://maps.gstatic.com/mapfiles/api-3/images/autocomplete-icons_hdpi.png */

.pac-icon-marker {

background-position: 16px -320px;

}

/*An item in the list of predictions supplied by the Autocomplete or SearchBox widget.*/

.pac-item {

 }

 

/*The item when the user hovers their mouse pointer over it. */

.pac-item:hover {

}

/* The item when the user selects it via the keyboard. Note: Selected items will be a member of this class and of the pac-item class.*/

.pac-item-selected {

    font-weight: bold;

    font-style: italic;

    letter-spacing: 0.5px;

}

/* A span inside a pac-item that is the main part of the prediction. For geographic locations, this contains a place name, like 'Sydney', or a street name and number, like '10 King Street'.

For text-based searches such as 'pizza in New York', it contains the full text of the query. By default, the pac-item-query is colored black.

If there is any additional text in the pac-item, it is outside pac-item-query and inherits its styling from pac-item. It is colored gray by default. The additional text is typically an address.*/

.pac-item-query {

 color:#ED4C4D;

 }

/* The part of the returned prediction that matches the user’s input. By default, this matched text is highlighted in bold text. Note that the matched text may be anywhere within pac-item.

It is not necessarily part of pac-item-query, and it could be partly within pac-item-query as well as partly in the remaining text in pac-item.*/

.pac-matched {

}

Update the maf-feature.xml to add this new css reference.

13. updated MAF feature

 

Deploy the app and test the page.

14. updated drop down

 

Summary

This article has shown how it is possible to add elements from external javascript libraries into MAF and how to use Google Places API to perform address lookups.  Hopefully it has given you some ideas of how to embed these functions into your app and what you can do.  For example, one area to consider would be how to use the location of the device to auto fill the address directly.