Saturday, July 5, 2014

Custom Lookup functionality on a Visualforce Page

Introduction


The Salesforce.com standard lookup does a good job at looking up related records. For example when changing the owner of an opportunity, the standard lookup can be used to add the new owner of the opportunity by clicking the magnifying glass. A popup window opens with most recently viewed users. The user can then type a name in the popup and search for user whose name matches the search string.



This is pretty handy but also very limiting. The search criteria cannot be modified. You might want the users suggested in the lookup to match a certain complex criteria or you could want to search across multiple fields to find suggested users and not only the name field.

Today I will show you how to build your own custom look up functionality which is powered by your own defined SOQL or SOSL query to retrieve and display visually anything when the loopup is used. Excited? me too.

This example applies only to Visualforce pages. To do this on a standard salesforce detail page is not possible at the moment. If you happen to know how, please share.

Use Case:


Implement a Visualforce page which can be used to change the owner of the opportunity. Add a button to the opportunity detail page - Change Owner - to open up the Visualforce page. Replace the standard owner field with a new custom text field - Opportunity Owner.

Your detail page should look like this;



The new lookup should;

  • be required in order to save changes. Implement proper error handling.
  • initially suggest only users who belong to either the account or opportunity team for that opportunity if the input text with the magnifying glass is left empty. If it is not left empty, perform the search and show the popup window with the search results.
  • search across name, email and phone fields to find users
  • only users with a standard profile type should appear in search results. Other users such as chatter free users can't own records in Salesforce, that is why.
  • display user names, profile information and phone numbers
  • use keyboard ENTER on the popup window to initiate the search
  • show the user a status message on the popup window when searching for results

As you can see the standard lookup cannot do any of the things listed above. Apart from the goodies mentioned in the requirements above, your users will not notice that they are not using the standard Salesforce lookup ... Who wants to hi 5?

Required


  • 2 Visualforce pages
  • 1 Controller Extension
  • 1 Controller

Visualforce Page to edit opportunity owner


The Visualforce page to edit the owner name will look like this




Let's see how this is implemented and how we can move from here to our custom lookup.

Apex


The Controller extension is pretty straight forward. It uses the Standardcontroller to retrieve the opportunity record. When the user clicks on save changes button on the page, the save action method is called. It checks if the opportunity owner field is filled in and issues an error if it is not, else it updates the opportunity record:

 public with sharing class OpportunityOwnerEditExtension {  
      public Opportunity opportunity;  
      public OpportunityOwnerEditExtension(Apexpages.Standardcontroller stdController){       
           this.opportunity = (Opportunity)stdController.getRecord();  
      }  
      public void save(){  
           Apexpages.Message msg = new Apexpages.Message(Apexpages.Severity.Info, 'Changes saved successfully!');  
           Apexpages.Message error_msg = new Apexpages.Message(Apexpages.Severity.Error, 'An error occurred while saving changes !');  
           if(this.opportunity.Opportunity_Owner__c == null || this.opportunity.Opportunity_Owner__c.equals('')){  
                Apexpages.Message errorMessage = new Apexpages.Message(Apexpages.Severity.Error, '<b>Opportunity Owner<b>: Validation Error: Value is required.');  
                Apexpages.addMessage(errorMessage);  
                return;  
           }  
           try{  
                update this.opportunity;  
                Apexpages.addMessage(msg);  
           } catch(Exception e){  
                Apexpages.addMessage(error_msg);  
                System.debug('@@@@ calling save on OpportunityOwnerEditExtension. Following error occured while saving changes: ' + e);  
           }  
      }  
      public void cancel(){  
      }  
 }  


Visualforce


The page on the other hand is a bit more complex. Let's begin with the HTML on the page;

 <apex:form >  
      <apex:outputPanel id="pageMessages" layout="block">  
           <apex:pageMessages escape="false" />  
      </apex:outputPanel>  
      <apex:pageBlock title="Edit Opportunity">  
           <apex:pageBlockButtons >  
                <apex:commandButton value="save changes" action="{!save}" rerender="pageMessages" />  
                <apex:commandButton value="cancel" action="{!cancel}" rerender="pageMessages" />  
           </apex:pageBlockButtons>  
           <apex:outputPanel id="editContainer" layout="block">  
                <apex:pageBlockSection title="Edit Opportunity" columns="2">  
                <apex:outputField value="{!opportunity.name}"/>  
                <apex:pageBlockSectionItem >  
                     <apex:outputPanel layout="inline">  
                          <apex:outputLabel value="{!$ObjectType.Opportunity.Fields.Opportunity_Owner__c.label}" for="lookupcontainer" />  
                     </apex:outputPanel>  
                     <apex:outputPanel layout="inline">  
                          <div class="requiredInput" id="lookupcontainer" >  
                               <div class="requiredBlock"></div>  
                               <input class="opp_{!Opportunity.Id}" type="text" name="name" value="{!Opportunity.Opportunity_Owner__c}" />  
                               <apex:image value="/s.gif" StyleClass="lookupIcon" style="cursor:pointer;" onclick="popupwindow('User', 600, 500)" />  
                          </div>  
                          <apex:inputText style="display:none" styleClass="userId_{!Opportunity.Id}" value="{!Opportunity.ownerId}" />  
                          <apex:inputText style="display:none" styleClass="owner_{!Opportunity.Id}" value="{!Opportunity.Opportunity_Owner__c}" />  
                     </apex:outputPanel>   
                </apex:pageBlockSectionItem>   
                </apex:pageBlockSection>  
           </apex:outputPanel>  
      </apex:pageBlock>  
 </apex:form>  


Let's take a special look at the core of our lookup;

 <apex:outputPanel layout="inline">  
      <div class="requiredInput" id="lookupcontainer" >  
           <div class="requiredBlock"></div>  
           <input class="opp_{!Opportunity.Id}" type="text" name="name" value="{!Opportunity.Opportunity_Owner__c}" />  
           <apex:image value="/s.gif" StyleClass="lookupIcon" style="cursor:pointer;" onclick="popupwindow('User', 600, 500)" />  
      </div>  
      <apex:inputText style="display:none" styleClass="userId_{!Opportunity.Id}" value="{!Opportunity.ownerId}" />  
      <apex:inputText style="display:none" styleClass="owner_{!Opportunity.Id}" value="{!Opportunity.Opportunity_Owner__c}" />  
 </apex:outputPanel>   

The css classes requiredInput and requiredBlock are used to mark our field as required so it looks exactly like a required field on any standard edit page.
The next element is a normal input field identified by the class class="opp_{!Opportunity.Id}". We will use this class and jQuery to change the contents of the field when a User is selected from the popup window. The magnifying glass is added and it's onclick attribute calls the function popupwindow which we will look at in a minute.

There are two hidden fields at the bottom. By the way, you could use apex:hiddenInput tag for these fields. These fields are required to change the values of the opportunity owner by setting the ownerId field to the selected User's Id. Also Opportunity_Owner__c which is the new custom field we created to display the owner is also updated accordingly. Because we do this here, the new values for owner Id and Name will be available and correctly bound to the opportunity record when we call update on the opportunity record in the controller. The javaScript function required to change these values is added to the head section of the page:

 function lookUpSelectedUser(userId, name){  
      popUpLookUp.close();  
      $( 'input[class$="opp_{!Opportunity.Id}"]' ).val(name);  
      $( 'input[class$="userId_{!Opportunity.Id}"]' ).val(userId);  
      $( 'input[class$="owner_{!Opportunity.Id}"]' ).val(name);  
 }  

This function will be called from the new popup window when a User is selected by clicking on the User's name in the popup window. It also closes the popup window.

Next let's look at what happens when the user clicks on the magnifying glass. The popupwindow function which is added to the head section on the same page is called;

 var popUpLookUp;  
 function popupwindow(title, w, h) {  
       var searchString = $(".opp_{!Opportunity.Id}").val();  
       var url = '/apex/OpportunityLookupWindow?searchString=' + searchString + '&accountId={!Opportunity.AccountId}&opportunityId={!Opportunity.Id}';
       var left = (screen.width/2)-(w/2);  
       var top = (screen.height/2)-(h/2);  
       popUpLookUp = window.open(url, 'title', 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);  
       return popUpLookUp;  
 }  

This function retrieves the text typed (searchString) in by the user and calls a second Visualforce page which is the popup window and passes it the search string plus the Account and Opportunity Ids for this opportunity. It then pops up the window right in the middle of the screen. Notice that we assign the popup window to the variable popUpLookUp. This is because we will call popUpLookUp.close() whenever a User is selected from within the popup to close the popup. Now let's look at how the popup operates in detail.

Lookup Functionality

Our Lookup will initially look like this if the user just clicks on the magnifying glass without typing a search text in the edit opportunity page;


Apex


In order to fullfill the requirements specified in the Use Case, we will implement a couple of functions in the controller of the popup window.

When the popup is opened, the constructed first extracts the from  searchStringaccountId and opportunityId from the page parameters.

 public String searchString { get; set; }  
 public String accountId { get; set; }  
 public String opportunityId { get; set; }  
 public List<User> users { get; set; }  
 public OpportunityLookupWindowController(){  
      this.searchString = Apexpages.currentPage().getParameters().get('searchString');  
      this.accountId = Apexpages.currentPage().getParameters().get('accountId');  
      this.opportunityId = Apexpages.currentPage().getParameters().get('opportunityId');  
      this.retrieveTeamMembers();  
 }  

The retrieveTeamMembers function is also called in the constructor to return search results (list of Users) to be displayed on the popup window. If the user did not specify any search parameter, the account and opportunity team members are retrieved and will be bound to the popup window using the users
list property. The lookup window above is what we will see in the popup.

 public void retrieveTeamMembers(){  
      if(this.searchString != null && this.searchString != ''){  
           performSearch();  
      } else {  
           Map<String, User> userMap = new Map<String, User>();  
           Set<Id> userIds = new Set<Id>();  
           for(List<AccountTeamMember> members : [SELECT Id, UserId FROM AccountTeamMember WHERE AccountId = :this.accountId]){  
                for(AccountTeamMember member : members){  
                     userIds.add(member.UserId);  
                }  
           }  
           for(List<OpportunityTeamMember> members : [SELECT Id, UserId FROM OpportunityTeamMember WHERE OpportunityId = :this.opportunityId]){  
                for(OpportunityTeamMember member : members){  
                     userIds.add(member.UserId);  
                }  
           }  
           this.users = [  
                SELECT Id, Name, FirstName, LastName, Email, Phone, Profile.Name, Profile.UserType   
                     FROM User   
                          WHERE Id IN :userIds  
                               AND isActive = true   
                               AND Profile.UserType = 'Standard'   
                               ORDER BY LastName ASC, FirstName ASC];  
      }  
 }  

Notice that only Users with the Standard profile type are selected in accordance with our requirements. This is a very important point. I have come across customer requirements where customers will like to have a custom lookup that shows all active Users even those with Chatter Free licences. This is something you cannot achieve with the standard lookup.

If the user already type a search parameter into the input text field on the opportunity edit owner page, the performSearch action method is called. This method uses dynamic SOQL to query and return the results to display on the popup window. I think at this point you should take a minute to appreciate the power of this functionality. What this means is, you can use dynamic SOQL and SOSL to search and return virtually anything that is searchable to the popup window. How cool is that.

 public void performSearch(){  
      String query = 'SELECT Id, Name, FirstName, LastName, Email, Phone, Profile.Name, Profile.UserType'  
            + ' FROM User'  
            + ' WHERE isActive = true'  
            + ' AND Profile.UserType = \'Standard\'';  
            if(this.searchString != null && this.searchString != ''){  
                query += ' AND (Name LIKE \'%' + String.escapeSingleQuotes(this.searchString) + '%\' OR Phone LIKE \'%' + String.escapeSingleQuotes(this.searchString) + '%\' OR Email LIKE \'%' + String.escapeSingleQuotes(this.searchString) + '%\')';  
            }       
            query += ' ORDER BY LastName ASC, FirstName ASC';  
      this.users = database.query(query);  
 }  

Since we are using dynamic SOQL to incorporate the search parameter entered by the user it is important to prevent SOQL injection by using the escapeSingleQuotes method as done above.

Our popup window looks like this when a search parameter is type in:



Next let's look at the popup window Visualforce code.

Visualforce


When defining your popup window page, do not forget to set showHeader and sideBar attributes to false.
 <apex:page controller="OpportunityLookupWindowController" title="Search"  showHeader="false"  sideBar="false">  

The contents of the page are them put in a form:

 <apex:form >  
      <apex:actionStatus id="searching" startText=" Searching ... " />  
      <apex:actionFunction name="performSearch" action="{!performSearch}" status="searching" rerender="userResults" />  
      <apex:outputPanel layout="block" style="margin:5px;padding:10px;padding-top:2px;">  
           <apex:outputPanel id="top" layout="block" style="margin:5px;padding:10px;padding-top:2px;">  
                <apex:outputLabel value="Search" style="font-weight:Bold;padding-right:10px;" for="txtSearch"/>  
                <apex:inputText id="txtSearch" value="{!searchString}" onkeyPress="performSearchOnKeyPress(event);" />  
                <apex:image value="/s.gif" StyleClass="lookupIcon" style="cursor:pointer;" onclick="performSearch();"/>  
           </apex:outputPanel>  
           <apex:outputPanel style="margin:10px;height:450px;overflow-Y:auto;" layout="block">  
           <apex:pageBlock id="searchResults">  
            <apex:pageBlockTable value="{!users}" var="record" id="userResults">  
                <apex:column headerValue="Name">  
                     <apex:outputLink value="javascript:top.window.opener.lookUpSelectedUser('{!record.Id}', '{!record.Name}');" rendered="{!NOT(ISNULL(record.Id))}">{!record.Name}</apex:outputLink>   
                </apex:column>  
                <apex:column headerValue="Profile">  
                     {!record.Profile.Name}  
                </apex:column>  
                <apex:column headerValue="Email">  
                     {!record.Profile.UserType}  
                </apex:column>  
                <apex:column headerValue="Phone">  
                     {!record.Phone}  
                </apex:column>  
            </apex:pageBlockTable>  
           </apex:pageBlock>  
       </apex:outputPanel>  
      </apex:outputPanel>  
 </apex:form>  

We will highlight the main points from the code above. First we have an actionFunction tag which calls the performSearch action method on the controlller when the magnifying glass next to the input field on the popup window is clicked. The input field is used to bind the searchString property to the controller such that when the performSearch action method is run, the property is already set with the value typed by the user.
One of the requirements we had was that the user should be able to type his search parameter and hit ENTER on the keyboard to initiate the search. The onKeyPress attribute of the input field calls the javaScript function performSearchOnKeyPress which is added to the head section of the page. This cross-browser function checks if the key action was an ENTER (code = 13) and then calls the action Function which calls performSearch on the controller.

 <script type="text/javascript">  
      function performSearchOnKeyPress(evt){  
           var code = (evt.keyCode ? evt.keyCode : evt.which);  
           if(code == 13){  
                performSearch();  
           }  
           if (code == 13) {  
             if (evt.preventDefault) {  
                  evt.preventDefault();  
             } else {  
                  evt.returnValue = false;  
             }  
        }  
      }  
 </script>  

The last and most important piece of the puzzle is how do we pass the selected entry (User) from the lookup window to the Visualforce owner edit page. This is done by using javaScript to call the lookUpSelectedUser function on the parent page (edit owner page). We already discussed what this function does above.

 <apex:outputLink value="javascript:top.window.opener.lookUpSelectedUser('{!record.Id}', '{!record.Name}');" rendered="{!NOT(ISNULL(record.Id))}">{!record.Name}</apex:outputLink>  

Finally notice that we are using an actionStatus tag to indicate to the user that the search is taking place. Notice the text Searching... at the top of the screen when the AJAX request is being made using the actionFunction tag.


And that is it.

Tip


A common require you will come across would be to prevent the user from typing anything into the lookup input field, i.e make the input read-only, atleast for the user and force him to use the lookup to select a valid entry from the popup window (User in our example).

So how would you implement this while still making sure that the javaScript code can still write to the field?

Well you are in luck because the solution design here already takes care of this. Let's take a look again at this section of the code;

 <apex:outputPanel layout="inline">  
      <div class="requiredInput" id="lookupcontainer" >  
           <div class="requiredBlock"></div>  
           <input class="opp_{!Opportunity.Id}" type="text" name="name" value="{!Opportunity.Opportunity_Owner__c}" />  
           <apex:image value="/s.gif" StyleClass="lookupIcon" style="cursor:pointer;" onclick="popupwindow('User', 600, 500)" />  
      </div>  
      <apex:inputText style="display:none" styleClass="userId_{!Opportunity.Id}" value="{!Opportunity.ownerId}" />  
      <apex:inputText style="display:none" styleClass="owner_{!Opportunity.Id}" value="{!Opportunity.Opportunity_Owner__c}" />  
 </apex:outputPanel>   

You probably already asked yourself why we are using a normal HTML input field and then doing the  actual binding of Opportunity_Owner__c in a hidden field (style="display:none"). Well the answer is simply ... more flexibility. There are a couple of HTML input field attributes that are not available for the apex:inputField or apex:inputText fields in Visualforce e.g. readonly. So using a normal HTML input tag gives me a bit more flexibility which comes in very handy in this situation;

 <input class="opp_{!Opportunity.Id}" type="text" name="name" value="{!Opportunity.Opportunity_Owner__c}" readonly="readonly" style="background-color: rgb(235, 235, 228); color: rgb(84, 84, 84);" />  

Look at the above snippet, we have added the readonly and style attributes to make out lookup read-only for the user. However we can still manipulate it using our javaScript function:


Conclusion


In this post I showed you a complete end-to-end solution for implementing a custom lookup for a specific use case. But the concepts and solution design used here can be used to implement a custom lookup for any object on any Visualforce page. You can even update lookup fields on a list of records. To stay with out example, if you wanted to change the owner on a bunch of opportunities at once, all you have to do is use a controller that returns a list of the opportunities whose owner fields you wish to edit, then use the repeat tag when building your Visualforce page. The user can then change the opportunity owner on all opportunities and then finally click the save changes button. All your controller needs to do, is just run an update on the list of opportunities.
As you can see, in this post I have given you a powerful solution or at least a great starting point from which to provide your users with amazing lookup functionality.
Stay tuned for more, until then ... Auf Widersehen.

3 comments:

  1. Hello Terence,

    Can you please email me the complete code of custom lookup it will be very helpful.

    Thanks

    ReplyDelete
  2. please mail me the custom lookup it will be very helpful

    ReplyDelete
  3. please mention complete code its tailored code

    ReplyDelete