How to create custom Lookup in Lightning Component

Hi guys,

Its time to use Lightning Component Framework. This framework is used to build a SPA. SPA is not a massage therapy as you know. 🙂 Here SPA stands for "Single Page Application". In this article, you will learn, how to create a custom lookup in Salesforce Lightning Component? When I was building a Lightning Application, I was facing so much problem to build an application with the lookup. I'll not let you face such a problem with this. Actually lightning lookup has completely different UI from the standard lookup. It looks like an auto-complete dropdown box.

Let's start to work with lightning lookup. First, you have to start to create a Lightning Component where you want to use Lightning Lookup. I have created a custom component where I want to use a lookup named "ContactComponent.cmp".

LookupComponent.cmp:

<aura:component implements="force:appHostable" controller="LookupComponentCtrl" >
    <div class="container YourClass">
        <div class="page-header">
            <h1>Contact Component</h1>
        </div>
        <div class="panel panel-primary">
            <div class="panel-heading">Example for Lookup </div>
            <div class="panel-body">
                <div class="form-horizontal information-box" >
                     <div class="form-group">
                        <label class="control-label col-sm-1">Select Account</label>
                        <div class="col-sm-4">
                            <c:SobjectLookup type="Account"  
                                           value="{!v.contact.Account__c}"
                                           className="form-control "/>  
                        </div>
                     </div>
               </div>
            </div>
        </div>
    </div>
</aura:component>

In the above code snippet, I have created "ContactComponent.cmp" lightning component. Where "Lightning_ContactComponentController" apex controller is used for interacting with the Salesforce database. On this page, I used "SobjectLookup" component to create a custom lookup for lightning. Now we have to create "LookupComponentCtrl" apex controller if you need this.

LookupComponentCtrl:

public class LookupComponentCtrl {
    //Do Something
}

Now we have to create a Lightning Controller for ContactComponent.cmp component. If you want to do something with the Lightning JavaScript controller then you need to create this otherwise you don't need to create this for lookup.

LookupComponentController.js:

({
    doInit : function(component, helper, event) {
        // Do Anything!!
    }
})

We have to create SobjectLookup.cmp, which will be used in LookupComponent.cmp to reuse the lookup component code for multiple lookups used in a single page. In this component, we are using RequireJS and some CSS to support custom lookup components and better look and feel. If you want to download RequireJS click here and if you want to download the "Lgt_InputLookup" zip folder click here and add these files in the static resources.

SobjectLookup.cmp:

<aura:component controller="SobjectLookupController">
    <!-- this code was necessary before the introduction of the "ltng:require" component
        leaving "requireJSLoaded" event and the "requireJSLoader" component useless
    <aura:handler action="{!c.initTypeahead}"  event="c:requireJSLoaded" />
    <c:requireJSLoader src="/resource/RequireJS" />
    -->
    <ltng:require afterScriptsLoaded="{!c.initTypeahead}" scripts="/resource/RequireJS" />
    <aura:handler name="init" value="{!this}" action="{!c.setup}"/>
    
    <link type="text/css" rel="stylesheet"  href="/resource/Lgt_InputLookup/css/bootstrap.min.css"/>
    <link type="text/css" rel="stylesheet" href="/resource/Lgt_InputLookup/css/typeahead.css"/>
   
    <aura:attribute type="String" name="type"  description="SobjectType" required="true"/>
    <aura:attribute type="String" name="value" description="Source/Destination value"/>
    <aura:attribute name="className" type="String" description="class name of the input object" />
    <!-- PRIVATE ATTRS -->
    <aura:attribute name="nameValue" type="String" description="Name of the current lookup 'name' field: loaded by controller" 
                    access="PRIVATE" />
    <aura:attribute name="isLoading" type="Boolean" default="true" description="LoadingComponent"
                    access="PRIVATE"/>
    
    <div class="has-feedback">
      <input id="{!globalId+'_typeahead'}" type="text" value="{!v.nameValue}" 
             onchange="{!c.checkNullValue}" class="{!v.className}" readonly="{!v.isLoading}"/> 
    <span class="glyphicon glyphicon-search form-control-feedback"></span>
  </div>
</aura:component>

Create below apex controller to use it in SobjectLookup.cmp component. "@AuraEnabled" is used for enable method to access with Lightning.

SobjectLookupController:

public with sharing class SobjectLookupController {
    /*
      Load initial value of given SObject type with ID "value"
     */
    @AuraEnabled
    public static String getCurrentValue(String type, String value){
        if(String.isBlank(type)){
            return null;
        }
        
        ID lookupId = null;
        try{   
            lookupId = (ID)value;
        }catch(Exception e){
            return null;
        }
        
        if(String.isBlank(lookupId)){
            return null;
        }
        
        SObjectType objType = Schema.getGlobalDescribe().get(type);
        if(objType == null){
            return null;
        }

        String nameField = getSobjectNameField(objType);
        String query = 'Select Id, '+nameField+' From '+type+' Where Id = \''+lookupId+'\'';
        List<SObject> objList = Database.query(query);
        if(objList.size()==0) {
            return null;
        }
        return (String)objList[0].get(nameField);
    }
    
    /*
     * Utility class for search results
    */
    public class SearchResult{
        public String value{get;Set;}
        public String id{get;set;}
    }
    
    /*
     * Returns the "Name" field for a given SObject (e.g. Case has CaseNumber, Account has Name)
    */
    private static String getSobjectNameField(SobjectType sobjType){
        
        //describes lookup obj and gets its name field
        String nameField = 'Name';
        Schema.DescribeSObjectResult dfrLkp = sobjType.getDescribe();
        for(schema.SObjectField sotype : dfrLkp.fields.getMap().values()){
            Schema.DescribeFieldResult fieldDescObj = sotype.getDescribe();
            if(fieldDescObj.isNameField() ){
                nameField = fieldDescObj.getName();
                break;
            }
        }
        return nameField;
    }
    
    /*
     * Searchs (using SOSL) for a given Sobject type
     */
    @AuraEnabled
    public static String searchSObject(String type, String searchString){
        if(String.isBlank(type) || String.isBlank(searchString)){
            return null;
        }
        
        SObjectType objType = Schema.getGlobalDescribe().get(type);
        if(objType == null){
            return null;
        }
        
        String nameField = getSobjectNameField(objType);
        searchString = '\'*'+searchString+'*\'';
        String soslQuery = 'FIND :searchString IN NAME FIELDS RETURNING '
                          + type +'(Id, '+nameField+' ORDER BY '+nameField+') LIMIT 20';
        System.debug('SOSL QUERY: '+soslQuery);
        List<List<SObject>> results =  Search.query(soslQuery);
        
        List<SearchResult> output = new List<SearchResult>();
        if(results.size()>0){
            for(SObject sobj : results[0]){
                SearchResult sr = new SearchResult();
                sr.id = (String)sobj.get('Id');
                sr.value = (String)sobj.get(nameField);
                output.add(sr)   ;
            }
        }
        return JSON.serialize(output);
    }
}

Now create JavaScript controller for SobjectLookup.cmp component. Below is the code to use this with SobjectLookup.cmp component.

SobjectLookupController.js:

({
    /*
      Verify component has been loaded with the required params
    */
    setup : function(component, helper, event){
        if(!component.get('v.type') ){
            $A.error("inputLookup component requires a valid SObject type as input: ["+component.getGlobalid()+"]");
            return;
        }
    },
    
    /*
      When RequireJS is loaded, loads the typeahead component
    */
    initTypeahead : function(component, helper, event){
       try{
      //first load the current value of the lookup field and then
      //create typeahead component
            helper.loadFirstValue(component);
        }catch(ex){
            console.log(ex);
        }
    },
    /*
     * When the input field is manually changed, the corresponding value (id) is set to null
     */
    checkNullValue : function(component, helper, event){
        try{            
            $A.run(function(){
              component.set('v.value', null);
            });
        }catch(ex){
            console.log(ex);
        }
  },
})

Now create JavaScript helper for SobjectLookupController.js JavaScript controller. Below is the code to use this with SobjectLookupController.js JavaScript controller.

SobjectLookupHelper.js:

({
    //typeahead already initialized
    typeaheadInitStatus : {},
    
    typeaheadOldValue : {},
    //Creates the typeahead component using RequireJS, jQuery, Bootstrap and Bootstrap Typeahead
    createTypeaheadComponent: function(component){
        
        require.config({
            paths: {
                "jquery": "/resource/Lgt_InputLookup/js/jquery-2.1.1.min.js?",
                "bootstrap": "/resource/Lgt_InputLookup/js/bootstrap.min.js?",
                "boot-typeahead" : "/resource/Lgt_InputLookup/js/typeahead.js?",
            }
        });
        
        var self = this;
        var globalId = component.getGlobalId();
        //loading library sequentially
        require(["jquery"], function($) {
            require(["bootstrap", "boot-typeahead"], function(bootstrap, typeahead) {
                var inputElement = $('[id="'+globalId+'_typeahead"]');

                //inits the typeahead
                inputElement.typeahead({
                    hint: false,
                    highlight: true,
                    minLength: 2
                },
                {
                    name: 'objects',
                    displayKey: 'value',
                    source: self.substringMatcher(component)
                })
                //selects the element
                .bind('typeahead:selected', 
                      function(evnt, suggestion){
                          $A.run(function(){
                              component.set('v.value', suggestion.id);
                              component.set('v.nameValue', suggestion.value);
                          });
                      });
            });//require end
        });//require end
    },
     // Method used by the typeahead to retrieve search results
    substringMatcher : function(component) {
        //usefull to escape chars for regexp calculation
        function escapeRegExp(strng) {
          return strng.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
        }
        
        return function findMatches(qa, cb) {
            qa = escapeRegExp(qa);
            var action = component.get("c.searchSObject");
            var self = this;
      
      action.setParams({
                'type' : component.get('v.type'),
                'searchString' : qa,
            });
            
            action.setCallback(this, function(a) {
                if(a.error && a.error.length){
                    return $A.error('Unexpected error: '+a.error[0].message);
                }
                var result = a.getReturnValue();

                var matcheStr, substrRegex;
                
                
                var matcheStr = [];// an array that will be populated with a substring matches
                
                
                var substrRegex = new RegExp(qa, 'i');// regex used to determine if a string contains the substring - qa
                var strs = JSON.parse(result);
                // iterate through the pool of strings and for any string that
                // contains the substring 'qa', add it to the 'matches' array
                $.each(strs, function(i, str) {
                    if (substrRegex.test(str.value)) {
                        // typeahead jQuery plugin expects suggestion to a 
                        // JavaScript object, refer to typeahead doc for more information
                        matcheStr.push({ value: str.value , id: str.id});
                    }
                });
                if(!strs || !strs.length){
                    
                    $A.run(function(){
                        component.set('v.value', null);
                    });
                }
                cb(matcheStr);
            });
            $A.run(function(){
                $A.enqueueAction(action);
            });
        };
    },
    //Method used on initialization to get the "name" value of the lookup
    loadFirstValue : function(component) {
        //this is necessary to avoid multiple initializations (same event fired again and again)
        if(this.typeaheadInitStatus[component.getGlobalId()]){ 
      return;
        }
        this.typeaheadInitStatus[component.getGlobalId()] = true;
        this.loadValue(component);
           
    },
    //Method used to load the initial value of the typeahead 
    //(used both on initialization and when the "v.value" is changed)
    loadValue : function(component, skipTypeaheadLoading){
        this.typeaheadOldValue[component.getGlobalId()] = component.get('v.value');
        var action = component.get("c.getCurrentValue");
        var self = this;
        action.setParams({
            'type' : component.get('v.type'),
            'value' : component.get('v.value'),
        });
        
        action.setCallback(this, function(a) {
            if(a.error && a.error.length){
                return $A.error('Unexpected error: '+a.error[0].message);
            }
            var result = a.getReturnValue();
            
            component.set('v.isLoading',false);
            component.set('v.nameValue',result);
            if(!skipTypeaheadLoading) self.createTypeaheadComponent(component);
        });
        $A.enqueueAction(action);
    }
})

Now its time to create SobjectLookupRenderer.js JavaScript renderer. This is used, when the field value changed, rerender function will be called.

SobjectLookupRenderer.js:

({
    /*
     * When the v.value field changes its value, the lookup is loaded again
     */
    rerender : function(component, helper){
        this.superRerender();
    //if value changes, triggers the loading method
        if(helper.typeaheadOldValue[component.getGlobalId()] !== component.get('v.value')){
            helper.loadValue(component,true);
        }
    }
})

Related Articles

Responses