Today I’m sharing a quick tutorial on how to add Lightning Components to Visualforce pages and then take an event from your Lightning Component and have your Visualforce page handle it. This assumes you already have basic knowledge of Visualforce pages and the ability to create a basic Lightning Component. Before you start, make sure you’ve defined stylings for your Lightning Design System.
In order to construct this, we need a few elements:
- An app container (Visualforce page)
- A Lightning Component with a related controller and helper class
- An Apex Controller
- A Lightning Event
This example uses Casper Harmer’s code for a Lookup Component. Some functionality was removed, and the Lightning event was added:
SVG 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>
Lookup 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" />
"{!v.searchString}" 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>
Lookup JS 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);
}
}
})
Lookup JS 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 < 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() && 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] && 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 <a> 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 && 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<SObject> 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<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);
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;
}
}
}
Most of this code is from Caspar’s post, but there are a few differences. The first major difference is in the helper’s “handleSelection” method, where it instead activates the Lightning event for the selection and associates values to the event’s properties:
var updateEvent = $A.get("e.c:LookupEvent");
updateEvent.setParams({"lookupVal": objectId, "lookupLabel": objectLabel});
updateEvent.fire();
This is important because instead of passing the callback to the Visualforce page instead we will have it waiting for an event to fire. The Lookup component also contained a reference to an event:
<aura:registerEvent name="updateLookup" type="c:LookupEvent" />
The Lightning event code is very simple. Note: the type needs to 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 point, you will have an active component that will run an event when a selection is made. The missing link so 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>
Instead of receiving a callback, this version adds a handler to wait for the Lightning event to fire when an Account is selected. This lookup could work for any object that you want as long as you change the API name of the objects. You can replace the console.log in the event handler and instead have it do some real functionality, such as actually saving the values returned from the Lightning Component on the Salesforce record.
Hey Damien Phillippi thank you for the wonder post,
I m getting difficulty in finding ‘lookup-form-element’. Could you please help in finding it.
if (required){
var cmpTarget = cmp.find(‘lookup-form-element’);
$A.util.addClass(cmpTarget, ‘slds-is-required’);
}
You don’t need to find it. I think it’s more related to when the lookup is required. I’m not actually fully sure what that section does.
Invalid Aura API
$A.eventService.addHandler()
Can you help me?