Salesforce Lightning Lookup Component

Lightning Yellow Pages

Note: This article has been superceded by: Version 2

I’ve been playing around with Salesforce Lightning components and one of the obvious things that seems to be missing is a UI component to lookup a record in a Salesforce custom object and return its Id.

This was really simple in Visualforce. You could just use an <apex:inputField> tag and bind it to an objects lookup field in your controller. Salesforce would take care of the rest. Lightning, however, is a different beast and centres around building client side apps or components in Javascript and utilising various API’s for dealing with any server side requests.

There’s quite a bit of code here so I’ve added it all to a github repository. (Links at the bottom)

Design

There were a few things to consider:

  • It should work both within a desktop app and within the Salesforce Lightning mobile app.
  • It should be intuitive and have the look and feel of a lightning app.
  • It should be reusable, easily configured and plumbed into another component or app.

Cake

As I said, there’s quite a bit of code so before we begin, like all good cake recipes, let’s look at the finished result (6Mb Animated GIF file, please be patient while it loads):

Ligntning Lookup Component

Ligntning Lookup Component

Look & Feel (using the Salesforce Lightning Design System)

The look and feel comes from the Salesforce Lightning Design System or SLDS for short. This is a prerequisite, so you’ll need to install this before you press ahead and load any of the code into your org. I’m using SLDS092 but you should be able to a later version if you wish. You can install it into your org using the links here. This library provides a whole plethora of CSS, images to make your app and components look like the real deal. It also covers the UI design and how to structure your markup. For the lookup component I’ve used the markup described in the Lookups component of the SLDS. In some cases I’ve replaced the standard HTML tags with lightning framework markup such as <ui:inputText> instead of <input>. I’ve also had to create an <c:svg> component to use instead of the <svg> tag which is not supported in lightning. This is described in the Trailhead module: Build a Lightning App with the Lightning Design System.

Show me the code!

Lightning component bundle: svg

First things first, I need to show you the svg component. You need this so you can render the icons as nice scalable graphics without using the <svg> tag. Now this is pretty much lifted straight from the trailhead module so I’m not going to bore you with how it works. You can check out the trailhead yourself.

Here’s the component markup:

Component

<aura:component >
  <aura:attribute name="class" type="String" description="CSS classname for the SVG element" />
  <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" />
  <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" />
</aura:component>

It has a renderer defined which looks like this:

Renderer

({
  render: function(component, helper) {
    //grab attributes from the component markup
    var classname = component.get("v.class");
    var xlinkhref = component.get("v.xlinkHref");
    var ariaHidden = component.get("v.ariaHidden");

    //return an svg element w/ the attributes
    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('class', classname);
    svg.setAttribute('aria-hidden', ariaHidden);
    svg.innerHTML = '<use xlink:href="'+xlinkhref+'"></use>';
    return svg;
  }
})

Apex Controller: LookupSObjectController

We’re going to need an apex controller on the server side to query the database and return the matching records. To do this create the following aura enabled Apex class:

Apex class

/**
 * Apex Controller for looking up an SObject via SOSL
 */
public with sharing class LookupSObjectController 
{
    /**
     * Aura enabled method to search a specified SObject for a specific string
     */
    @AuraEnabled
    public static Result[] lookup(String searchString, String sObjectAPIName)
    {
        // Sanitze the input
        String sanitizedSearchString = String.escapeSingleQuotes(searchString);
        String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName);

        List<Result> results = new List<Result>();

        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; 

        // Execute the Query
        List<List<SObject>> searchList = search.query(searchQuery);

        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
        
        return results;
    }
    
    /**
     * Inner class to wrap up an SObject Label and its Id
     */
    public class Result
    {
        @AuraEnabled public String SObjectLabel {get; set;}
        @AuraEnabled public Id SObjectId {get; set;}
        
        public Result(String sObjectLabel, Id sObjectId)
        {
            this.SObjectLabel = sObjectLabel;
            this.SObjectId = sObjectId;
        }
    }
}

Notice the @AuraEnabled keyword. This allows the method to be called from the lightning framework. The lookup method constructs a SOSL query using the passed in search string and the objects API name. It returns a list of Results that contain a label and the Id of the matching SObjects.

Lightning component bundle: LookupSObject

On the client side we have a reusable Lightning component. The component contains attributes to specify the SObjects API Name, the label and plural label to apply along with an optional SVG image path and class from the SLDS. If the latter aren’t specified it uses a default icon, in this case a star.

There are two events registered: updateLookupIdEvent and clearLookupIdEvent. The former is fired when the user chooses an entry from the list, the latter is fired when the user clears a selection (via the cross icon.)

The component uses a keyup event to pass the user entered search string to a controller method. This in turn calls the server side Aura Enabled method.

The markup generated adheres to the SLDS Lookup component design. Note the <ul> tag. This is where the <li> tags will be inserted for the records matching the search.

The component markup looks like this:

Component

<aura:component controller="LookupSObjectController" >
    <!-- Required Scripts/Styles -->
    <!-- Salesforce Lightning Design System : https://www.lightningdesignsystem.com/ -->
    <ltng:require styles="/resource/SLDS092/assets/styles/salesforce-lightning-design-system-ltng.css" />
    
    <!-- Attributes -->
    <aura:attribute name="sObjectAPIName" type="String" required="true" 
        description="The API name of the SObject to search" />
    <aura:attribute name="label" type="String" required="true" 
        description="The label to assign to the lookup, eg: Account" />
    <aura:attribute name="pluralLabel" type="String" required="true" 
        description="The plural label to assign to the lookup, eg: Accounts" />
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS092/assets/icons/custom-sprite/svg/symbols.svg#custom11" 
        description="The static resource path to the svg icon to use." />
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" 
        description="The SLDS class to use for the icon." />
    <aura:attribute name="searchString" type="String" 
        description="The search string to find." />

    <!-- Events -->
    <aura:registerEvent name="updateLookupIdEvent" type="c:UpdateLookupId"/>    
    <aura:registerEvent name="clearLookupIdEvent" type="c:ClearLookupId"/>    
    
    <!-- Lookup Markup : See https://www.lightningdesignsystem.com/components/lookups -->
    <div class="slds"> 
        <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true">
            <!-- This is the Input form markup -->
            <div class="slds-form-element">
                <label class="slds-form-element__label" for="lookup">{!v.label}</label>
                <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right">
                    <c:svg class="slds-input__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                    <!-- This markup is for when an item is currently selected -->
                    <div aura:id="lookup-pill" class="slds-pill-container slds-hide">
                        <span class="slds-pill slds-pill--bare">
                            <span class="slds-pill__label">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString}
                            </span>
                            <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}">
                                <c:svg class="slds-button__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#close" />
                                <span class="slds-assistive-text">Remove</span>
                            </button>
                        </span>
                    </div>
                    <!-- This markup is for when searching for a string -->
                    <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" />
                </div>
            </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="slds-lookup__menu slds-hide" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:svg class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                        &quot;{!v.searchString}&quot; in {!v.pluralLabel}
                    </button>
                </div>
                <ul aura:id="lookuplist-items" class="slds-lookup__list" role="presentation">
                </ul>
            </div>
        </div>
    </div>

</aura:component>

There is an associated controller to handle the events. The search method, as mentioned earlier, is called from the keyup event on the search. The select method is called when the user chooses a result from the rendered list. The clear method is called when the user clears the selected item by clicking the cross icon:

Controller

({
    /**
     * Search an SObject for a match
     */
    search : function(cmp, event, helper) {
        helper.doSearch(cmp);        
    },

    /**
     * Select an SObject from a list
     */
    select: function(cmp, event, helper) {
        helper.handleSelection(cmp, event);
    },
    
    /**
     * Clear the currently selected SObject
     */
    clear: function(cmp, event, helper) {
        helper.clearSelection(cmp);    
    }
})

You’ll notice that each of the methods delegate to a helper. The helper does all the grunt work and looks like this:

Helper

({
    /**
     * Perform the SObject search via an Apex Controller
     */
    doSearch : function(cmp) {
        // Get the search string, input element and the selection container
        var searchString = cmp.get("v.searchString");
        var inputElement = cmp.find('lookup');
        var lookupList = cmp.find("lookuplist");
        var lookupListItems = cmp.find("lookuplist-items");

        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
        lookupListItems.set('v.body', new Array());
        
        // We need at least 2 characters for an effective search
        if (typeof searchString === 'undefined' || searchString.length < 2)
        {
            // Hide the lookuplist
            $A.util.addClass(lookupList, 'slds-hide');
            return;
        }

        // Show the lookuplist
        $A.util.removeClass(lookupList, 'slds-hide');

        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');

        // Create an Apex action
        var action = cmp.get("c.lookup");

        // Mark the action as abortable, this is to prevent multiple events from the keyup executing
        action.setAbortable();

        // Set the parameters
        action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName});
                          
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();

            // Callback succeeded
            if (cmp.isValid() && state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();

                // If we have no matches, return
                if (matches.length == 0)
                {
                    return;
                }
                
                // Render the results
                this.renderLookupComponents(cmp, lookupListItems, matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
                
                if (errors) 
                {
                    if (errors[0] && errors[0].message) 
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
        
        // Enqueue the action                  
        $A.enqueueAction(action);                
    },

    /**
     * Render the Lookup List Components
     */    
    renderLookupComponents : function(cmp, lookupListItems, matches)
    {
        // list Icon SVG Path and Class
        var listIconSVGPath = cmp.get('v.listIconSVGPath');
        var listIconClass = cmp.get('v.listIconClass');

        // Array of components to create
        var newComponents = new Array();
         
        // Add a set of components for each match found
        for (var i=0; i<matches.length; i++)
        {
            // li element
            newComponents.push(["aura:html", {
                "tag" : "li",
                "HTMLAttributes" : {
                    "class" : "slds-lookup__item" 
                }
            }]);

            // a element
            newComponents.push(["aura:html", {
                "tag" : "a",
                "HTMLAttributes" : { 
                    "id" : cmp.getGlobalId() + '_id_' + matches[i].SObjectId, 
                    "role" : "option", 
                    "onclick" : cmp.getReference("c.select") 
                }
            }]);

            // svg component
            newComponents.push(["c:svg", {
                "class" : "slds-icon " + listIconClass + " slds-icon--small",
                "xlinkHref" : listIconSVGPath
            }]);

            // output text component
            // For some reason adding an aura:id to this component failed to record the id for subsequent cmp.find requests
            newComponents.push(["ui:outputText", {
                "value" : matches[i].SObjectLabel
            }]);
        }

        // Create the components
        $A.createComponents(newComponents, function(components, status) {
            // Creation succeeded
            if (status === "SUCCESS")
            {
                // Get the List Component Body
                var lookupListItemsBody = lookupListItems.get('v.body');

                // Iterate the created components in groups of 4, correctly parent them and add them to the list body
                for (var i=0; i<components.length; i+=4)
                {
                    // Identify the releated components
                    var li = components[i];
                    var a = components[i+1];
                    var svg = components[i+2];
                    var outputText = components[i+3];

                    // Add the <a> to the <li>
                    var liBody = li.get('v.body');
                    liBody.push(a);
                    li.set('v.body', liBody);

                    // Add the <svg> and <outputText> to the <a>
                    var aBody = a.get('v.body');
                    aBody.push(svg);
                    aBody.push(outputText);
                    a.set('v.body', aBody);

                    // Add the <li> to the container
                    lookupListItemsBody.push(li);
                }

                // Update the list body
                lookupListItems.set('v.body', lookupListItemsBody);
           }
           else // Report any error
           {
                this.displayToast('Error', 'Failed to create list components.');
           }
        });

    },

    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the <a> tag)
        var objectId = this.resolveId(event.currentTarget.id);

        // The Object label is the 2nd child (index 1)
        var objectLabel = event.currentTarget.children[1].innerText;

        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
                
        // Create the UpdateLookupId event
        var updateEvent = cmp.getEvent("updateLookupIdEvent");
        
        // Populate the event with the selected Object Id
        updateEvent.setParams({
            "sObjectId" : objectId
        });

        // Fire the event
        updateEvent.fire();

        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);

        // Hide the Lookup List
        var lookupList = cmp.find("lookuplist");
        $A.util.addClass(lookupList, 'slds-hide');

        // Hide the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.addClass(inputElement, 'slds-hide');

        // Show the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.removeClass(lookupPill, 'slds-hide');

        // Lookup Div has selection
        var inputElement = cmp.find('lookup-div');
        $A.util.addClass(inputElement, 'slds-has-selection');

    },

    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Create the ClearLookupId event
        var clearEvent = cmp.getEvent("clearLookupIdEvent");
        
        // Fire the event
        clearEvent.fire();

        // Clear the Searchstring
        cmp.set("v.searchString", '');

        // Hide the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.addClass(lookupPill, 'slds-hide');

        // Show the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.removeClass(inputElement, 'slds-hide');

        // Lookup Div has no selection
        var inputElement = cmp.find('lookup-div');
        $A.util.removeClass(inputElement, 'slds-has-selection');
    },

    /**
     * Resolve the Object Id from the Element Id by splitting the id at the _
     */
    resolveId : function(elmId)
    {
        var i = elmId.lastIndexOf('_');
        return elmId.substr(i+1);
    },

    /**
     * Display a message
     */
    displayToast : function (title, message) 
    {
        var toast = $A.get("e.force:showToast");

        // For lightning1 show the toast
        if (toast)
        {
            //fire the toast event in Salesforce1
            toast.setParams({
                "title": title,
                "message": message
            });

            toast.fire();
        }
        else // otherwise throw an alert
        {
            alert(title + ': ' + message);
        }
    }
})

The doSearch method takes care of calling the Apex controller and handling the results. It calls out to the renderLookupComponents method which creates and injects the <li> elements within the <ul> element of the component markup. We render a <a> element within the <li> element to fire the selection event. We make use of the SObjects Id within the rendered element id so that we can easily capture it when the item is selected. The resolveId method splits the SObject Id from the end of the element id.

The handleSelection method takes the selected entry and fires the updateLookupIdEvent before modifying the CSS classes to show the Lookup in its selected state.

The clearSelection method fires the clearLookupIdEvent before modifying the CSS classes to revert back to the search state.

The displayToast is simply an error logging method that renders a toast popup in the Salesforce1 app or an alert in the browser.

Lightning event: UpdateLookupId

This is the event fired when the user selects an item from search results. It has a single attribute that gets set with the Id of the selected record. You’ll notice this is a component event rather than an application event. Be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Update Lookup Id" >
    <aura:attribute name="sObjectId" type="Id" required="true" description="The Id of the selected SObject." />
</aura:event>

Lightning event: ClearLookupId

This is the event fired when the user clears the selected item. It has no attributes. Again, you’ll notice this is a component event rather than an application event. Once again, be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Clear the Lookup" />

Ok, great. Now can I see how you use it?

We’re going to wrap this up with an example Account Lookup component which you can plug straight into Salesforce1 via a Component Tab. Since the component implements flexipage:availableForAllPageTypes you could plug it straight into Lightning Builder if you wished. If you wanted to embed it in a Visualforce page you could do that as well. I won’t go into how to do that here but you’ll find details in the Lightning developer documentation.

Lightning component: AccountLookup

First lets look at the component markup. There’s an attribute to hold the Record Id. This is hooked up to the force:recordView component which is only rendered if an Id is present. We have two event handlers listening for the UpdateLookupId and ClearLookupId events. Note the names tie up to the registered events in the LookupSObject component. These event handlers hook up to the components controller via the handleAccountIdUpdate and handleAccountIdClear methods. The LookupSObject component markup takes the labels and icon resources as attributes.

Component

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" 
        listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account"
        listIconClass="slds-icon-standard-account"
    />

    <!-- Record view -->
    <aura:if isTrue="{!v.recordId!=null}">
        <force:recordView recordId="{!v.recordId}" />
    </aura:if>
</aura:component>

Now lets take a look at the controller. You’ll see the handleAccountIdUpdate method gets the Id from the Event and updates the recordId attribute. The handleAccountIdClear simply nulls the recordId attribute. And that’s it, simples.

Controller

({
    /**
     * Handler for receiving the updateLookupIdEvent event
     */
    handleAccountIdUpdate : function(cmp, event, helper) {
        // Get the Id from the Event
        var accountId = event.getParam("sObjectId");

        // Set the Id bound to the View
        cmp.set('v.recordId', accountId);
    },

    /**
     * Handler for receiving the clearLookupIdEvent event
     */
    handleAccountIdClear : function(cmp, event, helper) {
        // Clear the Id bound to the View
        cmp.set('v.recordId', null);
    }
})

If you want to plug the component into Lightning builder, you’ll need to include a design in the component bundle. Since there are no attributes that need passing into our Account Lookup component it just looks like this:

Design

<design:component> 
</design:component>

Creating the Lightning Component Tab

  • Navigate to: Setup->Create->Tabs
  • Click on New Lightning Component Tab.
  • Select the AccountLookup from the dropdown list.
  • Enter a Tab Name.
  • Choose a Tab Style.

… and you’re nearly good to go.

All that remains is to add it to the Salesforce1 Navigation.

Navigate to: Setup->Mobile Administration->Salesforce1 Navigation

Add the Component tab to your navigation, fire up the Salesforce1 mobile app and give it a whirl!

Notes:

All the code samples assume the default namespace. If you have your own namespace you’ll need to change the references from c:mything to mynamespace:mything.

If you are using a different version of the SLDS, you’ll need to change the paths in the code from SLDS092 to match your version.

Code and Resources:

The code is based on Version 0.92 of the SLDS: https://www.lightningdesignsystem.com/resources/downloads

Github repository containing all the above code (Note this is now Version 2): https://github.com/tscottdev/Lightning-Lookup

Links and stuff:

Salesforce Lightning Design System

Trailhead: Build a Lightning App with the Lightning Design System

 

Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , , ,
Posted in apex, code, force.com, lightning, salesforce
About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: