Guest Post: Daniel Peter is a Lead Applications Engineer at Kenandy, Inc., building the next generation of ERP on the Salesforce Cloud. You can reach him on Twitter @danieljpeter or www.linkedin.com/in/danieljpeter.
So far in this series, the examples have had Salesforce IDs hardcoded in them. For displaying a report, being able to navigate to it by browsing or searching would be nice! Also, when displaying a record with a Salesforce ID, being able to navigate to its detail page by tapping it would be nice too. That’s where Lightning Component events come in.
Salesforce report examples were previously built using a jQuery Mobile remote autocomplete paired with a Salesforce JavaScript remote action. For this Lightning project, this can be accomplished in various ways, such as what Peter Knolle has done here. Why not go for a purely client-side autocomplete? In most cases, orgs won’t have thousands of reports, so it won’t take long to download them to the client on page load. Searching will be a snap. There are always tradeoffs between client- and server-side autocomplete implementations, and there is no one right answer.
Autocomplete List of Available Reports
First, add another method to the Apex controller to query for the report names and IDs:
<aura:component controller="LIGHTNINGREPORT.LightningReportsController" implements="force:appHostable">
<!-- Handle component initialization in a client-side controller -->
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<!-- Handle loading events by displaying a spinner -->
<aura:handler event="aura:waiting" action="{!c.showSpinner}"/>
<aura:handler event="aura:doneWaiting" action="{!c.hideSpinner}"/>
<!-- load all the reports for search -->
<aura:attribute name="reportList" type="Report[]"/>
<!-- Dynamically load the report rows -->
<aura:attribute name="reportResponse" type="Object"/>
<div><center><ui:spinner aura:id="spinner"/></center></div>
<div data-role="page" data-theme="d" id="reportPage">
<div role="main">
<ul data-role="listview" data-filter="true" data-filter-reveal="true" data-filter-placeholder="Search reports..." data-inset="true">
<aura:iteration var="report" items="{!v.reportList}">
<LIGHTNINGREPORT:reportSearchComponent report="{!report}" />
</aura:iteration>
</ul>
<!-- Iterate over the list of report rows and display them -->
<!-- special case for the header row -->
<table data-role="table" data-mode="columntoggle" id="report-table">
<thead>
<LIGHTNINGREPORT:reportRowComponent row="{!v.reportResponse.reportFields}" isHeader="true"/>
</thead>
<tbody>
<aura:iteration var="row" items="{!v.reportResponse.fieldDataList}">
<LIGHTNINGREPORT:reportRowComponent row="{!row}" isHeader="false"/>
</aura:iteration>
</tbody>
</table>
</div>
</div>
</aura:component>
The reportList attribute is new. Also, there is code that iterates over reportList and passes each report to another subcomponent. This builds up the unordered list that jQuery mobile automagically converts to an autocomplete.
Here is the new reportSearchComponent.cmp subcomponent that renders each <li>. It might seem like overkill to create a component just for this, but it comes in handy for handling events:
<aura:component >
<aura:attribute name="report" type="Report"/>
<li>
<a href="javascript:void(0);">{!v.report.Name}</a>
</li>
</aura:component>
Next, add code to the helper and controller to wire the Apex controller method to the reportComponent:
Helper
getReportsForSearch : function(component) {
var action = component.get("c.getReportsForSearch");
var self = this;
action.setCallback(this, function(a){
var reportList = a.getReturnValue();
component.set("v.reportList", reportList);
// Display toast message to indicate load status
var toastEvent = $A.get("e.force:showToast");
if(action.getState() ==='SUCCESS'){
toastEvent.setParams({
"title": "Ready",
"duration": 500
});
}else{
toastEvent.setParams({
"title": "Error!"
});
}
toastEvent.fire();
});
$A.enqueueAction(action);
},
Controller
doInit : function(component, event, helper) {
// Retrieve reports for search autocomplete during component initialization
helper.getReportsForSearch(component);
helper.loadResources();
},
Now when the page loads, there’s an autocomplete list of reports to choose from instead of a loaded, hard-coded report:
Creating a Lightning Event
Now create an event that loads the report when clicked. There are two types of Lightning events: Component Events and Application Events. This example uses an Application Event. Events are central to developing with Lightning, and a major difference from Visualforce development. They take some getting used to.
Go to File -> New Lightning Event in the dev console and create reportLoadEvent.evt:
<aura:event type="APPLICATION" description="report is clicked">
<aura:attribute name="report" type="Report"/>
</aura:event>
The report attribute will be set to the single Report sObject, which is clicked/tapped.
reportSearchComponent.cmp needs code added to register the event, and a call method in the JavaScript controller for when the report is clicked:
<aura:component >
<aura:registerEvent name="rLoadEvent" type="LIGHTNINGREPORT:reportLoadEvent"/>
<aura:attribute name="report" type="Report"/>
<li>
<a href="javascript:void(0);" onclick="{!c.showReport}">{!v.report.Name}</a>
</li>
</aura:component>
Here is the code for reportSearchComponentController.js:
({
showReport : function(component, event, helper) {
var report = component.get("v.report");
var rLoadEvent = $A.get("e.LIGHTNINGREPORT:reportLoadEvent");
rLoadEvent.setParams({"report": report});
rLoadEvent.fire();
}
})
When clicked, the link gets the individual report that was loaded for the clicked subcomponent. It then gets the created event, and sets the report parameter on that event to the clicked report. And then my favorite part… fire()!
Loading a Report
For the main component to load a report when the subcomponent fires an event, add a handler to it:
<!-- Handle loading a report which was clicked on -->
<aura:handler event="LIGHTNINGREPORT:reportLoadEvent" action="{!c.loadReport}"/>
The handler calls this function in the JavaScript controller:
loadReport : function(component, event, helper) {
helper.getReportResponse(component, event, helper);
}
To display the report, the controller calls a slightly modified version of the original code in the helper method. It’s no longer called when the page is initialized with a hard-coded reportId. It’s doing it on the click event with a dynamically selected ID:
getReportResponse : function(component, event, helper) {
var action = component.get("c.getReportResponse");
var report = event.getParam("report");
action.setParams({"reportId": report.Id});
action.setCallback(this, function(a){
var reportResponseObj = JSON.parse(a.getReturnValue());
component.set("v.reportResponse", reportResponseObj);
// Display toast message to indicate load status
var toastEvent = $A.get("e.force:showToast");
if(action.getState() ==='SUCCESS'){
toastEvent.setParams({
"title": "Report Loaded.",
"duration": 500
});
}else{
toastEvent.setParams({
"title": "Error!",
"message": " Something has gone wrong."
});
}
toastEvent.fire();
});
$A.enqueueAction(action);
},
Notice how it uses event.getParam from the event passed into it to get the selected report, sets the reportId param as the Report sObject’s ID, and passes that to the Apex controller’s getReportResponse method so it can load the correct report ID.
Clicking the Phone Numbers report loads it:
Navigating to Records in the Report
To navigate to a record within the report, leverage the new data structure created to build hyperlinks in the reportRowComponent.cmp. In many cases, the reporting API gives a Salesforce ID as the value in the response. A quick hack in the Apex controller shows if it is a valid ID and if so, set the isHyperlink to true:
public static Boolean isHyperlink(String sVal) {
Boolean isHyperLink = true;
Id theId;
try {theId = (Id)sVal;}
catch (Exception e) {isHyperLink = false;}
return isHyperLink;
}
Next, create a new Lightning Component to create and navigate to the hyperlinks. This component will be more generic than the report app. Use it for any app to have a hyperlink for navigating to an sObject detail page:
sobjectHyperlink.cmp
<aura:component>
<aura:attribute name="sObjectId" type="Id" />
<aura:attribute name="hyperlinkLabel" type="String" />
<a href="javascript:void(0);" onclick="{!c.navigateToSObject}">{!v.hyperlinkLabel}</a>
</aura:component>
sobjectHyperlinkController.js
({
navigateToSObject : function(component, event, helper) {
var sObjId = component.get("v.sObjectId");
var navToSObjEvt = $A.get("e.force:navigateToSObject");
navToSObjEvt.setParams({
recordId: sObjId,
slideDevName: "detail"
});
navToSObjEvt.fire();
}
})
Finally, add conditional logic to reportRowComponent.cmp to use the sobjectHyperlink.cmp component if there is a valid hyperlink, or standard rendering if not:
<aura:component>
<aura:attribute name="row" type="Object[]"/>
<aura:attribute name="isHeader" type="Boolean"/>
<tr>
<aura:iteration var="cell" items="{!v.row}">
<aura:renderIf isTrue="{!v.isHeader}">
<th>{!cell.fieldLabel}</th>
<aura:set attribute="else">
<td>
<aura:renderIf isTrue="{!cell.isHyperLink}">
<LIGHTNINGREPORT:sobjectHyperlink sObjectId="{!cell.fieldValue}" hyperlinkLabel="{!cell.fieldLabel}"/>
<aura:set attribute="else">{!cell.fieldLabel}</aura:set>
</aura:renderIf>
</td>
</aura:set>
</aura:renderIf>
</aura:iteration>
</tr>
</aura:component>
Loading a report with hyperlinks now allows for drilling into the object details:
![]() |
![]() |
Fragment Identifiers: jQuery Mobile and Salesforce1 Lightning
The Lightning Component framework URLs make use of the “#” fragment identifier:
Read more about URL-Centric Navigation here.
jQuery Mobile also uses the fragment identifier to navigate within a page, such as the column toggle popup:
<a href="#report-table-popup" data-rel="popup">Columns...</a>
This creates some navigation challenges. Since that fragment is already being used in the URL, you can’t hyperlink to a fragment within a page in Lightning the same way as in a traditional page. This is a big clue as to how the Lightning framework works behind the scenes. Navigation is significantly different from Visualforce. As Lightning matures and more developers start working with it, a good solution can hopefully be found for this issue. More about this in a future article.