Managing Lightning Events in A Visualforce Page

Today, I’m presenting a brief tutorial on incorporating Lightning Components into Visualforce pages and subsequently managing an event from your Lightning Component within your Visualforce page. This assumes you possess fundamental knowledge of Visualforce pages and the capability to create a basic Lightning Component. Before commencing, ensure you have established stylings for your Lightning Design System.

To build this integration, we require a few key elements:

  • An app container (Visualforce page)
  • A Lightning Component with an associated controller and helper class
  • An Apex Controller
  • A Lightning Event

SVG Element

<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>

Search Component

<aura:component controller="SampleController" access="global" >
    <ltng:require styles="{!$Resource.SLDS213 + '/assets/styles/salesforce-lightning-design-system.css'}" />
 
    <!-- Component Init Handler -->
    <aura:handler name="init" value="{!this}" action="{!c.init}"/>
 
    <!-- Attributes -->
    <aura:attribute name="parentRecordId" type="Id" description="Record Id of the Host record (ie if this was a lookup on opp, the opp recid)" access="global"/>
    <aura:attribute name="lookupAPIName" type="String" description="Name of the lookup field ie Primary_Contact__c" access="global"/>
    <aura:attribute name="sObjectAPIName" type="String" required="true" description="The API name of the SObject to search" access="global"/>
    <aura:attribute name="label" type="String" required="true" description="The label to assign to the lookup, eg: Account" access="global"/>
    <aura:attribute name="pluralLabel" type="String" required="true" description="The plural label to assign to the lookup, eg: Accounts" access="global"/>
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" access="global"/>
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS213/assets/icons/custom-sprite/svg/symbols.svg#custom11" description="The static resource path to the svg icon to use." access="global"/>
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" description="The SLDS class to use for the icon." access="global"/>
    <aura:attribute name="searchString" type="String" description="The search string to find." access="global"/>
    <aura:attribute name="required" type="Boolean" description="Set to true if this lookup is required" access="global"/>
    <aura:attribute name="filter" type="String" required="false" description="SOSL filter string ie AccountId = '0014B000003Sz5s'" access="global"/>
 
    <aura:attribute name="callback" type="String" description="Call this to communcate results to parent" access="global" />
 
    <!-- PRIVATE ATTRS -->
    <aura:attribute name="matches" type="SampleController.Result[]" description="The resulting matches returned by the Apex controller." />
 
    <aura:registerEvent name="updateLookup" type="c:LookupEvent" />
 
    <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:PR_SVG class="slds-input__icon" xlinkHref="/resource/SLDS213/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:PR_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:PR_SVG class="slds-button__icon" xlinkHref="/resource/SLDS213/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}" blur="{!c.handleBlur}"/>
            </div>
        </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:PR_SVG class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS213/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">
                    <aura:iteration items="{!v.matches}" var="match">
                        <li class="slds-lookup__item">
                            <a id="{!globalId + '_id_' + match.SObjectId}" role="option" onclick="{!c.select }">
                                <c:PR_SVG class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!match.SObjectLabel}
                            </a>
                        </li>
                    </aura:iteration>
                </ul>
            </div>
        </div>
    </div>
</aura:component>

Search JavaScript Controller

/**
 * (c) Tony Scott. This code is provided as is and without warranty of any kind.
 * Adapted for use in a VF page, removed need for two components, removed events - Caspar Harmer
 *
 * This work by Tony Scott is licensed under a Creative Commons Attribution 3.0 Unported License.
 * http://creativecommons.org/licenses/by/3.0/deed.en_US
 */
({
    /**
     * 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);
    },
 
    /**
     * If the input is requred, check if there is a value on blur
     * and mark the input as error if no value
     */
     handleBlur: function (cmp, event, helper) {
 
         helper.handleBlur(cmp);
     },
 
    init : function(cmp, event, helper){
      try{
        //first load the current value of the lookup field
        helper.init(cmp);
        helper.loadFirstValue(cmp);
 
        }catch(ex){
          console.log(ex);
        }
      }
})

Search JavaScript Helper

/**
 * (c) Tony Scott. This code is provided as is and without warranty of any kind.
 * Adapted for use in a VF page, removed need for two components, removed events - Caspar Harmer
 *
 * This work by Tony Scott is licensed under a Creative Commons Attribution 3.0 Unported License.
 * http://creativecommons.org/licenses/by/3.0/deed.en_US
 */
({
    //lookup already initialized
    initStatus : {},
    init : function (cmp){
      var required = cmp.get('v.required');
      if (required){
        var cmpTarget = cmp.find('lookup-form-element');
        $A.util.addClass(cmpTarget, 'slds-is-required');
      }
    },
 
    /**
     * 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');
 
        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
 
        // We need at least 2 characters for an effective search
        console.log('searchString = ' + searchString);
        if (typeof searchString === 'undefined' || searchString.length &lt; 2)
        {
            // Hide the lookuplist
            //$A.util.addClass(lookupList, 'slds-hide');
            return;
        }
 
        // Show the lookuplist
        console.log('lookupList = ' + lookupList);
        $A.util.removeClass(lookupList, 'slds-hide');
 
        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');
        // Get the filter value, if any
        var filter = cmp.get('v.filter');
 
        // 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, "filter" : filter});
 
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();
            console.log("State: " + state);
 
            // Callback succeeded
            if (cmp.isValid() &amp;&amp; state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();
                console.log("matches: " + matches);
 
                // If we have no matches, return nothing
                if (matches.length == 0)
                {
                    //cmp.set('v.matches', null);
                    return;
                }
 
                // Store the results
                cmp.set('v.matches', matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
 
                if (errors)
                {
                    if (errors[0] &amp;&amp; errors[0].message)
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
 
        // Enqueue the action
        $A.enqueueAction(action);
    },
 
    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the &lt;a&gt; tag)
        var objectId = this.resolveId(event.currentTarget.id);
        // Set the Id bound to the View
        cmp.set('v.recordId', objectId);
 
        // The Object label is the inner text)
        var objectLabel = event.currentTarget.innerText;
 
        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);
 
        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
 
        //This is important.  Notice how i get the event.
        var updateEvent = $A.get("e.c:LookupEvent");
        updateEvent.setParams({"lookupVal": objectId, "lookupLabel": objectLabel});
        updateEvent.fire();
    },
 
    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Clear the Searchstring
        cmp.set("v.searchString", '');
                cmp.set('v.recordId', null);
 
        var func = cmp.get('v.callback');
        console.log(func);
        if (func){
          func({id:'',name:''});
        }
 
        // 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');
 
        // If required, add error css
        var required = cmp.get('v.required');
        if (required){
          var cmpTarget = cmp.find('lookup-form-element');
          $A.util.removeClass(cmpTarget, 'slds-has-error');
        }
    },
 
    handleBlur: function(cmp) {
       var required = cmp.get('v.required');
       if (required){
         var cmpTarget = cmp.find('lookup-form-element');
         $A.util.addClass(cmpTarget, 'slds-has-error');
       }
   },
 
    /**
     * 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);
        }
    },
 
    loadFirstValue : function(cmp){
 
        var action = cmp.get('c.getCurrentValue');
        var self = this;
        action.setParams({
            'type' : cmp.get('v.sObjectAPIName'),
            'value' : cmp.get('v.recordId'),
        });
 
        action.setCallback(this, function(a) {
            if(a.error &amp;&amp; a.error.length){
                return $A.error('Unexpected error: '+a.error[0].message);
            }
            var result = a.getReturnValue();
            cmp.set("v.searchString", result);
 
            if (null!=result){
              // 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');
            }
        });
        $A.enqueueAction(action);
    }
})

Apex Controller

public with sharing class SampleController
{
    @AuraEnabled
    public static String getCurrentValue(String type, String value){
        if(String.isBlank(type))
        {
            System.debug('type is null');
            return null;
        }
 
        ID lookupId = null;
        try
        {
            lookupId = (ID)value;
        }catch(Exception e){
            System.debug('Exception = ' + e.getMessage());
            return null;
        }
 
        if(String.isBlank(lookupId))
        {
            System.debug('lookup is null');
            return null;
        }
 
        SObjectType objType = Schema.getGlobalDescribe().get(type);
        if(objType == null){
            System.debug('objType is null');
            return null;
        }
 
        String nameField = getSobjectNameField(objType);
        String query = 'Select Id, ' + nameField + ' From ' + type + ' Where Id = \'' + lookupId + '\'';
        System.debug('### Query: '+query);
        List&lt;SObject&gt; oList = Database.query(query);
        if(oList.size()==0)
        {
            System.debug('objlist empty');
            return null;
        }
        return (String) oList[0].get(nameField);
    }
 
    /*
       * 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;
    }
    /**
     * 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&lt;Result&gt; results = new List&lt;Result&gt;();
 
        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50';
 
        // Execute the Query
        List&lt;List&lt;SObject&gt;&gt; searchList = search.query(searchQuery);
        System.debug('searchList = ' + searchList);
        System.debug('searchQuery = ' + searchQuery);
 
        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
 
        System.debug('results = ' + results);
        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;
        }
    }
}

The majority of this code is borrowed from Caspar’s post, with a few distinctions. The primary difference lies in the helper’s “handleSelection” method, which now triggers the Lightning event for the selection and assigns values to the event’s properties.

var updateEvent = $A.get("e.c:LookupEvent");
updateEvent.setParams({"lookupVal": objectId, "lookupLabel": objectLabel});
updateEvent.fire();


This is crucial because, rather than transmitting the callback to the Visualforce page, it now anticipates an event to be triggered. The Lookup component also included a reference to an event:

<aura:registerEvent name="updateLookup" type="c:LookupEvent" />

The Lightning event code is quite straightforward. Please note: the type should be APPLICATION instead of COMPONENT.

<aura:event type="APPLICATION" description="Event template">
    <aura:attribute name="lookupVal" type="String" description="Response from calls" access="global" />
    <aura:attribute name="lookupLabel" type="String" description="Response from calls" access="global" />
</aura:event>

At this stage, you’ll have an operational component that triggers an event upon making a selection. The element that has been absent thus far is the Visualforce page:

<apex:page applyBodyTag="false" standardController="Contact" docType="html-5.0" showHeader="true" sidebar="false" standardStylesheets="false">
    <html xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
        <head>
          <apex:includeScript value="/lightning/lightning.out.js" />
          <apex:stylesheet value="{!URLFOR($Resource.SLDS213, 'assets/styles/salesforce-lightning-design-system-vf.css')}"/>
        </head>
        <body >
          <div class="slds">
            <div class="slds-form-element slds-m-top--xx-small">
              <div class="slds-m-right--x-small" id="account_lookup"></div>
            </div>
          </div>
        </body>
        <script>
            var visualForceFunction = function(event)
            {
                var myEventData1 = event.getParam("lookupVal");
                var label = event.getParam("lookupLabel");
                console.log('response data = ' + myEventData1 + ' : ' + label);
            };
 
            // and...
            $Lightning.use("c:expensesAppVF", function()
            {
                $Lightning.createComponent
                (
                    "c:Lookup",
                    {
                         
                        recordId: "{!contact.AccountId}",
                        label: "Account",
                        pluralLabel: "Accounts",
                        sObjectAPIName: "Account"
                    },
                    "account_lookup",
                    function()
                    {
                        $A.eventService.addHandler({ "event": "c:LookupEvent", "handler" : visualForceFunction});
                    }
                );
            });
        </script>
    </html>
</apex:page>

In lieu of receiving a callback, this iteration incorporates a handler to await the firing of the Lightning event when an Account is selected. This lookup can function for any desired object, provided you modify the API names accordingly. You can substitute the console.log in the event handler and, instead, implement tangible functionality, such as saving the values returned from the Lightning Component on the Salesforce record.