Sunday, June 1, 2014

Salesforce Object Search Langauge (SOSL) and jQuery UI Autocomplete

Introduction 


Salesforce provides a powerful search language SOSL which can be used to perform simple yet powerful text searches across multiple objects. You can define which objects should be searched and exactly which fields on each object should be taken into consideration.
Auto-completion, also sometimes called word completion or auto-suggestion is a web technique used to predict what a user wants to type. In the context of a key word search, auto-completion predicts what the users want to type as they begin typing and makes suggestions.
Exactly how you can achieve this on a Visualforce page using SOSL and jQuery Autocomplete plugin will be the center of this article

Use Case

Perform a simple keyword search on Account, Opportunity and Contact objects. Make suggestions as users type the keyword. The suggestions should indicate if the suggested record belongs to the account, opportunity or contact objects.

Our auto-complete search should look like this;

Search Bar

Auto-complete suggestions


Apex: SOSL


To retrieve a list of suggestions we will call the Controller directly from JavaScript. This means that we will have to implement a remote function on the Controller which can be call directly from JavaScript. This function will be annotated by the @RemoteAction keyword. This process is called JavaScript Remoting.

1:  @RemoteAction  
2:  public static List<Suggestion> getSearchSuggestions(String searchString){  
3:       List<Suggestion> suggestions = new List<Suggestion>();  
4:       List<Account> accounts = new List<Account>();  
5:       List<Opportunity> opportunities = new List<Opportunity>();  
6:       List<Contact> contacts = new List<Contact>();  
7:       List<List<sObject>> searchObjects = [FIND :searchString + '*' IN ALL FIELDS RETURNING Account (Id, Name), Opportunity(Id, Name), Contact(Id, Name)];  
8:       if(!searchObjects.isEmpty()){  
9:            for(List<SObject> objects : searchObjects){  
10:                 for(SObject obj : objects){  
11:                      Schema.DescribeSObjectResult objDescribe = obj.getSObjectType().getDescribe();  
12:                      if(objDescribe.getName().equals('Account')){  
13:                           Account acct = (Account)obj;  
14:                           suggestions.add(new Suggestion(acct.name + ' : ' + objDescribe.getName(), acct.Id));  
15:                      } else if(objDescribe.getName().equals('Opportunity')){  
16:                           Opportunity opp = (Opportunity)obj;  
17:                           suggestions.add(new Suggestion(opp.name + ' : ' + objDescribe.getName(), opp.Id));  
18:                      } else if(objDescribe.getName().equals('Contact')){  
19:                           Contact con = (Contact)obj;  
20:                           suggestions.add(new Suggestion(con.name + ' : ' + objDescribe.getName(), con.Id));  
21:                      }  
22:                 }  
23:            }  
24:       }  
25:       return suggestions;  
26:  }  

Each time the remote function is called, it gets the keyword with which to perform the search using SOSL. The SOSL query has a few interesting keywords that are worth mentioning at this point;

  • FIND: this is a construct unique to SOSL and is similar to SELECT in SQL or SOQL. It specifies the word or phrase to search for. The wildcards * and ? can be used with search phrases to match zero or more characters or exactly one character at the middle or end of a search term respectively. 
  • IN : specifies what fields to examine when performing the search. Here ALL FIELDS means all fields on the objects to be searched will be considered. Examples of other valid search groupings are NAME FIELDS, EMAIL FIELDS, PHONE FIELDS and SIDEBAR FIELDS
  • RETURNING: used to restrict the results returned by the search because it can be used specify exactly what objects should be searched and what fields on those objects should be returned.


The search returns a list of list of sObjects. The results for the search are grouped in to lists such that the results for each search object are grouped together in a list and can be accessed by using an index which is determined by it's position in the RETURNING part of the SOSL statement. For example, the search results for Account can be accessed by using the index 0 in the search results e.g. searchObjects[0].

Since we want to build a list of Suggestion Objects to return to the page, we use Schema DescribeSObjectResult to check the type of Object and concatenate it's name with the sObject type. Notice that the Suggestion object takes two parameters and sets a label and a value property for each suggestion.

1:  public class Suggestion {  
2:       public String label { get; private set; }  
3:       public String value { get; private set; }  
4:       public Suggestion (String label, String value){  
5:            this.label = label;  
6:            this.value = value;  
7:       }  
8:  }  


The jQuery Autocomplete supports a couple of data sources. Please check the documentation. One of these sources is an array of objects with label and value properties. The label property is displayed in the suggestion menu. That is why we are concatenating it with the name of the object, such that the user will know exactly which suggestion belongs to which object. The value will be inserted into the input element when the user selects the item. That is why we set the value here to the ID of the specific record. We can then pass it back to the controller as a parameter of an actionFunction and use this Id to retrieve the specific record and send it back to the page.

Visualforce Page


First of all download the JavaScript for the jQuery UI Autocomplete and the CSS and add them to the Static Resources. Then reference them on the page like this;

1:  <apex:stylesheet value="{!URLFOR($Resource.tutorials, '/css/jquery-ui-1.10.3.custom.min.css')}" />  
2:  <apex:includeScript value="{!URLFOR($Resource.tutorials, '/js/jquery-ui-1.10.3.custom.min.js')}" />  


Next build the DOM for the search box as follows;

1:  <span class="search_label">Search String: </span>  
2:  <input class="search_box" id="sessionSearch" value="{!searchTerm}" onkeypress="performSearchOnKeyPress(event, this)"></input>  
3:  <span class="search_icon" onclick="performSearch();"></span>  


Notice that there is an icon placed next to the input box. The user can click on this icon to initiate the retrieval of search results after typing in a keyword.


1:  function performSearch(){  
2:       var searchString = $('#sessionSearch').val();  
3:       if(searchString.length >= 3 ){  
4:            performSessionSearch(searchString);  
5:       }  
6:  }  


In our example a JavaScript function performSearch is called. This function fetches the search key and calls an actionFunction performSessionSearch to perform the search. Note that this is an entirely different aspect of the search.

1:  <apex:actionFunction name="performSessionSearch" action="{!performSessionSearch}" rerender="hiddenDiv">  
2:       <apex:param assignTo="{!searchTerm}" name="searchTerm" value=""/>  
3:  </apex:actionFunction>  


In performSessionSearch , all records which match the search will be retrieved and returned. This is not implemented here, but this is the general idea. Do not confuse this with what happens when the user selects a single entry from the suggestion list. Read on to find out how this situation is handled. Also if the user types in a keyword and hits ENTER, the performSearchOnKeyPress function is called to also initiate the retrieval of all objects which match the key search

1:  function performSearchOnKeyPress(evt, input){  
2:       var code = (evt.keyCode ? evt.keyCode : evt.which);  
3:       if(code == 13){  
4:            var searchString = $("#sessionSearch").val();  
5:            if(searchString.length > 0){  
6:                 performSessionSearch(searchString);  
7:            }  
8:       }  
9:  }  


After this we can then initialize the Autocomplete plugin. When initializing the Autocomplete plugin, there are two options that must be specified;

1:  $(function(){  
2:   $("#sessionSearch").autocomplete({  
3:       minLength: 2,  
4:       source: function(request, response){  
5:            var searchString = request.term;  
6:            AccountBrowserController.getSearchSuggestions(searchString, function(result, event){  
7:                 if(event.status){  
8:                      if(typeof result === 'undefined'){  
9:                           response(['No Match']);  
10:                      } else if (result.length <= 0) {  
11:                           response(['No Match']);  
12:                      } else {  
13:                           response(result);  
14:                      }  
15:                 } else if (event.type === 'exception') {  
16:                      alert('Error Message: ' + event.message);  
17:                      response([]);  
18:                 } else {  
19:                      alert('Error' + event.message);  
20:                      response([]);  
21:                 }  
22:            },  
23:            {escape: true}  
24:            );  
25:       },  
26:       select: function(event, ui){  
27:            if(ui.item.label != 'No Match'){  
28:                 var searchTermValue = ui.item.value;  
29:                 var searchTermLabel = ui.item.label;  
30:                 $("#sessionSearch").val(searchTermLabel);  
31:                 retrieveSearchedSession(searchTermValue, searchTermLabel, false);  
32:            }  
33:            return false;  
34:       },  
35:       focus: function( event, ui ) {  
36:            $("#sessionSearch").val(ui.item.label);  
37:            return false;  
38:       }  
39:   });  
40:  });  



  • Source: it defines the data to use for the suggestions. The source is defined as a function which takes a request and a response. The request holds the search string typed in by the user. The response will receive the data from the controller which will then be suggested to the user.The getSearchSuggestions() remote action function on the controller will take the search string, build Suggestions with label, value pairs, convert these Suggestion objects into JavaScript Array Objects and pass them back to the calling JavaScript function. That is why the second parameter of the getSearchSuggestions() is a callback function which receives the result and status of the call made to the controller. If no error occurred the results are then passed as a parameter to the response function and displayed to the user as suggestions.
  • Select: this determines what will happen, when a suggestion is selected. In our case here, an actionFunction retrieveSearchedSession is called passes the label as well as the value back to the controller. The controller can then retrieve the specific record and send it back to the page


1:  <apex:actionFunction name="retrieveSearchedSession" action="{!retrieveSearchedSession}">  
2:       <apex:param assignTo="{!searchSessionId}" name="searchSessionId" value=""/>  
3:       <apex:param assignTo="{!searchTerm}" name="searchTerm" value=""/>  
4:  </apex:actionFunction>  


Note that, as the user moves his mouse over (hovers) the suggestions, the input field is automatically filled with the value of the suggestion. This behavior is defined in the focus option when initializing the Autocomplete plugin.

Hover (focus) behavior of the autocomplete

Finally the minLength option specifies the number of characters a user most type before a search is performed.

Conclusion

This article looked at an end-to-end implementation of the auto-complete functionality in a Visualforce page using jQuery Autocomplete and Salesforce Object Search Language (SOSL). How to handle the search when a user types in a keyword and clicks on the search icon or presses enter was explained. Also the behavior when the user selects one of the suggestions was also explained in detail. Please take a look at the documentation of jQuery UI Autocomplete for the different possibilities on how to use it. I hope this was a good and helpful introduction. Thanks for reading.

3 comments:

  1. Do you have the full source code. I get an error on the class.. Line 2 Error: Compile Error: unexpected token: 'List' at line 2 column 14

    ReplyDelete
    Replies
    1. Hey Paul, I do not have the full source code for you. However all the code you need is in the lines article above. Without seeing your code, I cannot help you figure out what your problem is.
      Mind you that you first have to create the "Suggestion" class before you can use it in the "getSearchSuggestions" remote action method

      Delete
  2. This comment has been removed by the author.

    ReplyDelete