Salesforce Developers Blog

Navigating to Reports & Records Using Lightning Component Events

Avatar for Daniel PeterDaniel Peter
Learn how to create a client-side autocomplete to navigate to specific reports, and add events to navigate to a report and drill down.
Navigating to Reports & Records Using Lightning Component Events
March 24, 2015
Listen to this article
0:00 / 0:00

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.

Daniel Peter - Lightning Components 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:

Auto complete with Lightning Components

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:

Salesforce1 Lightning Reports Navigation

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:

  Lightning Events - Displaying a Record   Lightning Events - Drilling down into a report

Fragment Identifiers: jQuery Mobile and Salesforce1 Lightning

The Lightning Component framework URLs make use of the “#” fragment identifier:

Lightning Events

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.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS

More Blog Posts

Our Detailed Plan to Improve Lightning Experience Performance

Our Detailed Plan to Improve Lightning Experience Performance

We’re inviting you to learn the current state of Lightning Experience performance, opportunities we have uncovered, and our next steps.September 07, 2023

Get Better User Insights with Custom Interactions for AppExchange App Analytics

Get Better User Insights with Custom Interactions for AppExchange App Analytics

ISV partners can now use custom interactions for AppExchange App Analytics to gain an understanding of how customers use managed packages.February 01, 2024

Invoke REST APIs with the Salesforce Integration User and OAuth Client Credentials

Invoke REST APIs with the Salesforce Integration User and OAuth Client Credentials

Learn to configure the Salesforce Integration User for both authentication using OAuth Client Credentials and authorization using permission sets.February 08, 2024