London World Tour 2017

I am delighted to be speaking at the London World Tour on Thursday May 18th. My session is on ‘Thinking and Delivering Lightning Components’ at 4pm in the Developer Theatre.

The slidedeck for the talk is available here and I have made the demos available as an unmanaged package.

The London event promises to be the biggest Salesforce Event outside of Dreamforce itself … and a lot of fun! If you see me – say hello!

See you at #LC2017!

See you at the London World Tour 2017!

I have also recorded a webinar where I go through the Lightning Design System Starter Kit to augment the session.

Advertisements

Scotland User Group Event

I am delighted to be getting an opportunity to speak to the Scotland User Group today Markc 2nd. It’ll be a hands-on session looking at the Salesforce UX teams Design System Starter Kit.

The tutorial is available here. The slide deck is available here.

I’ll also be touching on the material I covered at a recent talk at Londons Calling 2017. The video recording of that talk is below.

Looking forward to seeing everyone at the event!

Londons Calling 2017

I am honoured to presenting a session at London’s Calling 2017 – a European Salesforce.com event independently organised by the community. The event takes place in London on the 10th February 2017.

The day-long event is bigger than last year’s event – full of amazing sessions and keynotes and a chance to meet and chat with many MVPs from the Developer Community and Developer Evangelists from Salesforce. My congratulations to Simon Goodyear, Francis Pinar, Jodi Wagner and Kerry Townsend for their incredible efforts in organising the event.

London's Calling 2016

London’s Calling 2017

My own session is at 10.00am in the ‘Shift’ room and in it I discuss the planning and delivering of customer requirements with Lightning Components – and the lessons I have learned over the last year, most of which I have spent developing Lightning Components for Blue Wave’s customers.

The slide deck for the session is available here. The checkbox group component is available in my github account.

I have also recorded a webinar where I go through the Lightning Design System Starter Kit to augment the session.

See you at #LC2017!

See you at #LC2017!

Summer 16: Lightning Components for Record Home Pages

Salesforce Summer 16 Release Logo

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)

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

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

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

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.

Connecting Lightning Components and Visualforce

It’s a busy time. Since January I have been preparing to teach the new ‘Programming Lightning Components‘ course (DEV601) for Salesforce University. It is an excellent in-depth 5 day course. I have taught it twice over the last 5 weeks and am due to teach it again next week. Feedback from attendees has been very positive.

As you interact on these courses you always learn new things, and an interesting add on to Lightning out for Visualforce is worth sharing.

Lightning Out for Visualforce

Lightning Out for Visualforce allows for the embedding of Lightning Components via javascript in a Visualforce page. In order to use it, a visualforce page requires

  • a platform provided javascript library ‘/lightning/lightning.out.js’. There is alternatively a new visualforce tag ‘<apex:includeLightning />’ that can be used.
  • the Lightning Components must be added as an aura dependency to a Lightning Application. Many Lightning Components can be used in this application.
  • The Lightning Application must extend an interface ‘ltng:outApp’.

Currently, the only way to use a Lightning Component in Salesforce Classic (Aloha) is through Lightning Out for Visualforce.

Visualforce Interacting with a Lightning Component

It is possible to call javascript functions in the Lightning Component from Visualforce. The example below uses a built-in callback function provided in the $Lightning.createComponent function to save a reference to the component in the context of the JavaScript in the page.

In the below example, the Visualforce page will display a Lightning Component called ‘lightSabre’ (via a Lightning Application called ‘LightSabreApp’) which as a button that plays a sound. Once the Component is created in the ‘lightSabreDiv’ DOM element, the callback function (which takes a parameter of the component being created) can be used to apply the component reference to a javascript variable in the Visualforce page.

<apex:page >    
    <script src="/lightning/lightning.out.js"></script>
    

<div id="lightSabreDiv"></div>


    <a href="#" onclick="pressButton()">PRESS (to call function within c:LightSabre Controller)</a>

    <script>
        var component;

    	function pressButton() {
            component.helper.playSound(component);
        }
    
        $Lightning.use("c:LightSabreApp", function() {
            $Lightning.createComponent(
                "c:LightSabre",
                {},
                "lightSabreDiv",
                function(cmp) {
                    component = cmp;
                }
            );            
        });
    
    </script>
    
</apex:page>

Lightning Application Markup:

<aura:application access="GLOBAL" extends="ltng:outApp">
    <aura:dependency resource="c:LightSabre" />        
</aura:application>

The page can then call functions within the component. In this case the ‘pressButton()’ javascript function is directly calling a function in the helper of the Lightning Component that plays the sound (in this case a static resource).

Component Markup:

<aura:component description="Linking Visualforce to Lightning">


<div class="centered">
      <ui:button label="Light Sabre!!" press="{!c.playbackBegin}" />
      <audio aura:id="audiofile" src="/resource/lightsaber"></audio>
</div>


</aura:component>

Component Controller:

({
    playbackBegin : function(component, event, helper) { 
        var audioEl = component.find('audiofile').getElement();
        audioEl.play();
    }
})

Component Helper:

({
    playSound : function(component) { 
        var audioEl = component.find('audiofile').getElement();
        audioEl.play();
    },
})

Lightning Out for Visualforce went GA in the Spring 16 release. Lightning Out will also be available for any external site (with authentication) and through the Mobile SDK – these features are in pilot at time of writing and will be available in a future release.

#LondonsCalling: Lightning Single Page Application

Until I put together a full blog post on it, here is the slidedeck for my LDNs Calling 2016 presentation.

The sessions at the fantastic event are being recorded and the videos and slidedecks will be posted to http://www.londonscalling.net/.

Updates:

  1. I have added all of the code to my github repo.
  2. The recording of all presentations given at Londons Calling is now available at the London’s Calling Youtube Channel. My own presentation is here:

London’s Calling – Feb 5th 2016

I am looking forward to presenting a session at London’s Calling – the first European Salesforce.com event independently organised by the community. The event takes place in London on the 5th February 2016.

The day-long event is a full of amazing sessions and keynotes from Salesforce.com’s Peter Coffee and Erica Kuhl – not to mention the chance to meet and chat with many MVPs from the Developer Community and Developer Evangelists from Salesforce.
London's Calling 2016

My own session is at 10.55am in the ‘Alt’ room (the rooms are called CMD, CTRL, ALT and TAB – love it). The session is a sequel to my session at Dreamforce 15 where I created a complex single page application (SPA) with a Visualforce page and no apex was used. The sequel asks the question – how can I create a similar spa in a Lightning Application with Lightning Components and the Lightning Design System – and what is the experience like?

Visualforce SPA to Lightning SPA

Visualforce SPA to a Lightning SPA

I will post the slide deck for the session here the day before the event – and you might even get free code here too!

Kudos to the team who are organising the event and my thanks for getting the opportunity to speak at it.

My photo - See you all there!

See you all there!