Salesforce will release it’s Summer 16 upgrade in the weekends of June 4-5 and June 11-12 – and its a very exciting one in the evolution of Lightning Components. Lightning Experience Record Home Pages can be customised and custom Lightning Components can be added to these home pages.
Lightning Component Interfaces
To surface a custom Lightning Component on a Lightning Experience Record Page, the Component must implement the interface flexipage:availableForRecordHome. The Component will also implement a second interface force:hasRecordId that ensures that it receives the ID of the currently displayed record.
Implementing the interface force:hasSObjectName (along with flexipage:availableForRecordHome) allows a Lightning Component to receive the object name (String) of the currently displayed record.
Example: Displaying an Opportunity record’s List of Quotes
Lightning Experience – Opportunity showing a List of Quotes (Custom Lightning Components)
In this example, a Lightning Component retrieves Quote records for an associated OpportunityId when it is rendered. The Lightning Component implements the interfaces to make it available for Lightning Experience Home Pages and receive the Opportunity ID.
The force:hasRecordId interface will automatically populate an aura:attribute with a name of ‘recordId’ with the id of, in this case, the Opportunity. The force:hasSObjectName interface will populate an aura:attribute named ‘sObjectName’ with ‘Opportunity’.
<aura:component controller="OpportunityLightningController"
implements="flexipage:availableForRecordHome,
force:hasRecordId,force:hasSObjectName"
access="global" >
<aura:attribute name="recordId" type="String" />
<aura:attribute name="sObjectName" type="String" />
<aura:attribute name="quotes" type="Quote[]" />
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
<table class="slds-table slds-table--bordered">
<thead>
<tr class="slds-text-heading--label">
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate">Quote Name</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate">Status</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Sub Total ({!$Locale.currency})</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Discount (%)</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Total Price ({!$Locale.currency})</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Tax ({!$Locale.currency})</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Shipping ({!$Locale.currency})</div>
</th>
<th class="slds-is-sortable" scope="col">
<div class="slds-truncate slds-text-align--right">
Grand Total ({!$Locale.currency})</div>
</th>
</tr>
</thead>
<tbody>
<aura:iteration items="{!v.Quotes}" var="quote">
<tr class="slds-hint-parent">
<th class="slds-truncate" scope="row" data-label="Quote Name">
<a href="{! '/' + quote.Id }" target="_blank">{! quote.Name }</a>
</th>
<td class="slds-truncate" scope="row" data-label="Status">
{! quote.Status }
</td>
<td class="slds-text-align--right" data-label="Sub Total">
<ui:outputCurrency value="{!quote.Subtotal}" format="0,000.00" />
</td>
<td class="slds-text-align--right" data-label="Discount (%)">
<ui:outputNumber value="{!quote.Discount}" />
</td>
<td class="slds-text-align--right" data-label="Total Price">
<ui:outputCurrency value="{!quote.TotalPrice}" format="0,000.00" />
</td>
<td class="slds-text-align--right" data-label="Tax">
<ui:outputCurrency value="{!quote.Tax}" format="0,000.00" />
</td>
<td class="slds-text-align--right" data-label="Shipping">
<ui:outputCurrency value="{!quote.ShippingHandling}" format="0,000.00" />
</td>
<td class="slds-text-align--right" data-label="Sub Total">
<ui:outputCurrency value="{!quote.GrandTotal}" format="0,000.00" />
</td>
</tr>
</aura:iteration>
</tbody>
</table>
</aura:component>
The ‘quotes’ aura:attribute is populated by the Javascript controller function ‘doInit’ in the line component.set(‘v.Quotes’, response.getReturnValue());.
Component Controller
({
doInit : function(component, event, helper) {
// create a one-time use instance of the serverEcho action
// in the server-side controller
var action = component.get("c.getQuotes");
action.setParams({ OpportunityId : component.get("v.recordId") });
// Create a callback that is executed after
// the server-side action returns
action.setCallback(this, function(response) {
var state = response.getState();
// The action executed successfully
if (state === "SUCCESS") {
// Quote records have been returned (if any exist)
// The Quotes component can be set with the Quotes returned
component.set('v.Quotes', response.getReturnValue());
}
else if (state === "NEW") {
// The action was created but is not in progress yet
}
else if (state === "RUNNING") {
// The action is in progress
}
else if (state === "ABORTED") {
// The action was aborted
}
else if (state === "INCOMPLETE") {
// The server didn't return a response.
// The server might be down or the client might be offline.
// The framework guarantees that an action's callback is always invoked as
// long as the component is valid. If the socket to the server is never
// successfully opened, or closes abruptly, or any other network
// error occurs, the XHR resolves and the callback is invoked
// with state equal to INCOMPLETE.
}
else if (state === "ERROR") {
// The server returned an error
// generic error handler
var errors = response.getError();
if (errors) {
$A.log("Errors", errors);
if (errors[0] && errors[0].message) {
throw new Error("Error: " + errors[0].message);
}
} else {
throw new Error("Unknown Error");
}
}
});
// A client-side action could cause multiple events,
// which could trigger other events and
// other server-side action calls.
// $A.enqueueAction adds the server-side action to the queue.
$A.enqueueAction(action);
}
})
The Quote records are retrieved using the following Apex Class.
public class OpportunityLightningController {
@AuraEnabled
public static List<Quote> getQuotes(String OpportunityId) {
// Make sure we're not seeing something naughty
if(OpportunityId == null) {
throw new AuraHandledException('No OpportunityId received.');
}
List<Quote> quoteList = [SELECT id, Name, Tax, SubTotal, TotalPrice,
Discount, GrandTotal, Status, ExpirationDate,
ShippingHandling
FROM Quote
WHERE OpportunityId = :OpportunityId];
return quoteList;
}
}
Here is the associated test class. Note that I am throwing an AuraHandledException where I intercept a possible error in the OpportunityLightningController class – I am testing for this below.
@isTest
private class OpportunityLightningController_TEST {
@TestSetup
static void setUpQuote() {
Account a = new Account(Name='Test Inc');
insert a;
Pricebook2 pbk1 = new Pricebook2 (Name='Test Pricebook Entry 1',Description='Test Pricebook Entry 1', isActive=true);
insert pbk1;
Product2 prd1 = new Product2 (Name='Test Product Entry 1',Description='Test Product Entry 1',productCode = 'ABC', isActive = true);
insert prd1;
Id pricebookId = Test.getStandardPricebookId();
PricebookEntry pbe1 = new PricebookEntry (Product2ID=prd1.id,Pricebook2ID=pricebookId,UnitPrice=50, isActive=true);
insert pbe1;
Opportunity opp1 = new Opportunity (Name='Opp1',StageName='Stage 0 - Lead Handed Off',CloseDate=Date.today(),Pricebook2Id = pbe1.Pricebook2Id, AccountId = a.id);
insert opp1;
OpportunityLineItem lineItem1 = new OpportunityLineItem (OpportunityID=opp1.id,PriceBookEntryID=pbe1.id, quantity=4, totalprice=200);
insert lineItem1;
Quote quttest = new Quote (Name = 'qoutetest' , OpportunityId = opp1.id , Pricebook2Id = pricebookId );
insert quttest ;
List<QuoteLineItem> listval = new List<QuoteLineItem>();
QuoteLineItem qutlineitemtest = new QuoteLineItem ();
qutlineitemtest = new QuoteLineItem(QuoteId = quttest.id , Quantity = 3.00 , UnitPrice = 12 , PricebookEntryId = pbe1.id);
insert qutlineitemtest;
}
@isTest
static void getQuotesTest() {
Opportunity opp1 = [SELECT id FROM Opportunity LIMIT 1];
System.assertEquals(1, OpportunityLightningController.getQuotes(opp1.id).size());
}
@isTest
static void throwAnError_TEST() {
try {
OpportunityLightningController.getQuotes(null);
System.assertEquals(1,0); // line should not run
} catch(Exception e) {
System.assertEquals('Script-thrown exception', e.getMessage());
System.assertEquals('System.AuraHandledException', e.getTypeName());
}
}
}
System Administrators can add the Component to the Opportunity Record Home Page by clicking the ‘Edit Page’ option in the settings menu. This will open the Lightning App Builder to edit the page layout.
Lightning Experience – Editing Opportunity Record Home Page
A Custom Tab can be added to the main panel by clicking on the panel and using the associated options on the Right Hand Side of the App Builder interface. Note that the default tab can also be changed.
Lightning Experience Opportunity Home Page – Adding a Custom Tab
Any component can be dragged into the new tab. The Custom Component showing the List of Quotes (I called it ‘playWithOpportunityId’) is available in the list on the left hand side.
Lightning Experience Opportunity Home Page – Adding a Custom Lightning Component – ‘playWithOpportunityId’ – showing the list of Quotes for the Opportunity
The Lightning App Builder creates or updates a Lightning Page when saved. This Lightning Page must then be ‘Activated’ using the available button to override the current Lightning Experience Record home page.
It is interesting that I did not have to add Salesforce Lightning Design System to the Lightning Component – presumably it was applied to the custom component by Lightning Experience.
After being added to the Lightning Page, any changes to the Lightning Component is not immediately applied to the Lightning Page. The Page must be re-opened and saved in the App Builder to apply new changes. This is a little inconvenient and takes a little getting used to.
I believe that this capability will be a real eye-opener for customers, managers, administrators and users to the capability of Lightning Components. And provide a new world of possibilities for developers.