Lightning, Visualforce and the DOM
Introduction
Lightning Components entered beta in the Spring 15 release, were the subject of a Salesforce Developer Week and have been generating a huge amount of interest ever since. Developers now face a choice when building a custom Salesforce UI of whether to use Visualforce or Lightning. To my mind they each bring their own pros and cons, but one scenario where Lighting is an obvious choice (with a few caveats) an application where the business logic will reside on the client in JavaScript.
Demo Page
To demonstrate this, I’m building two versions of a simple page to output basic account details in Bootstrap panels:
Lightning Implementation
One distinguishing feature of the lightning implementation is the number of artefacts :
- Apex Controller to provide access to data
- Lightning Component Bundle to output the account panels, consisting of:
- Lightning Component
- JavaScript controller
- JavaScript helper
- Lightning Application to provide an URL-addressable "page" to view the component
The Apex controller has a single method to retrieve the accounts - note how I can re-use the same method across Lightning and Visualforce by applying more than one annotation:
public class AccountsListController { @AuraEnabled @RemoteAction public static List<Account> GetAccounts() { return [SELECT id, Name, Industry, CreatedDate FROM Account ORDER BY createdDate DESC]; } }
The lightning component is a little lengthy, due to the HTML markup to generate the Bootstrap panels, so I’ve made it available at this gist. The interesting aspects are:
<aura:attribute name="accounts" type="Account[]" /> <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
the first line defines the list of accounts that the component will operate on, while the second defines the JavaScript controller action that will be executed when the component is initialised, which will populate the accounts from the database into the attribute.
The markup to output the panels for the accounts utilises an aura:iteration component,which wraps the bootstrap panel HTML and allows merge fields to be used to access account fields:
<aura:iteration items="{!v.accounts}" var="acc"> <div class="row-fluid"> <div class="col-xs-12 fullwidth"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">{!acc.Name}</h3> </div> .... </aura:iteration>
for those from a Visualforce background this is a familiar paradigm - a custom tag that binds to the collection, followed by HTML and merge fields to format the collection elements.
The JavaScript controller is very simple, most of the work is done in the helper, in order to allow reuse elsewhere in the bundle in the future:
({ doInit: function(component, event, helper) { // Display accounts helper.getAccounts(component); } })
The JavaScript helper doesn’t have a huge amount of code in it either - it creates an action tied to a server side method, defines the callback to be executed when the action completes and queues the action:
({ getAccounts: function(component) { var action = component.get("c.GetAccounts"); var self = this; action.setCallback(this, function(a) { component.set("v.accounts", a.getReturnValue()); }); $A.enqueueAction(action); } })
the most interesting aspect of this code is the line that sets component’s accounts attribute to the result of the action:
component.set("v.accounts", a.getReturnValue());
that’s all I need to do to update the contents of the page - due to the data binding of the aura:iterator, updating the attribute rewrites the DOM with the new markup to display the accounts.
For the sake of completeness, here’s the markup for the Lightning application, which provides a way for the component to be accessed via a URL:
<aura:application > <c:AccountsList /> </aura:application>
Visualforce Implementation
To implement the page in Visualforce, I have the following artefacts:
- Apex Controller (the same one as in the Lightning section, above)
- Visualforce Page
- Visualforce Component containing the JavaScript business logic. This could reside in the page, but I find it easier to write JavaScript in a component without the potential distraction or confusion of the HTML markup.
The Visualforce page has a small amount of markup to pull in the bootstrap resources and provide the containing responsive grid:
<apex:page controller="AccountsListController" applyHtmlTag="false" sidebar="false" showHeader="false" standardStyleSheets="false"> <html> <head> <apex:stylesheet value="{!URLFOR($Resource.Bootstrap_3_3_2, 'bootstrap-3.3.2-dist/css/bootstrap.min.css')}"/> <apex:stylesheet value="{!URLFOR($Resource.Bootstrap_3_3_2, 'bootstrap-3.3.2-dist/css/bootstrap-theme.min.css')}"/> <apex:includescript value="{!$Resource.JQuery_2_1_3}" /> <apex:includeScript value="{!URLFOR($Resource.Bootstrap_3_3_2, 'bootstrap-3.3.2-dist/js/bootstrap.min.js')}"/> <c:AccountsListJS /> </head> <body> <div class="container-fluid"> <div class="row-fluid"> <div class="col-xs-12 col-md-9"> <div id="accountsPanel"> </div> </div> </div> </div> </body> </html> </apex:page>
while the heavy lifting is done by the JavaScript in the AccountsListJS component. Again this is somewhat lengthy so I’ve made it available as a gist. One aspect that stands out is the JavaScript to process the accounts received from the server and display the bootstrap panels:
renderAccs : function(accounts) { var markup=''; $.each(accounts, function(idx, acc) { markup+= ' <div class="row-fluid"> \n' + ' <div class="col-xs-12 fullwidth"> \n' + ' <div class="panel panel-primary"> \n' + ' <div class="panel-heading"> \n' + ' <h3 class="panel-title">' + acc.Name + '</h3> \n' + ' </div> \n' + ' <div class="panel-body"> \n' + ' <div class="top-buffer table-responsive"> ' + ' <label>Industry: </label>' + acc.Industry + ' </div> \n' + ' </div> \n' + ' <div class="panel-footer"> \n' + ' </div> \n' + ' </div> \n' + ' </div> \n' + ' </div> \n' + ' <div class="fluid-row"> \n' + ' <div class="col-xs-12 top-buffer"> \n ' + ' </div> \n' + ' </div> \n'; }); $('#accountsPanel').html(markup); }
in the Visualforce case, the DOM manipulation to render the Bootstrap panels is entirely down to me - I have to iterate the returned list of accounts, programmatically generate all of the HTML and then set this into the HTML element with the id of “accountsPanel”. This hardly satisfies the separation of concerns design principle, as it tightly couples the business logic with the presentation. If I decide to move to another presentation framework, I have to change the HTML markup and the JavaScript. Its also much more error prone - Douglas Crockford described the browser as a hostile environment to program in for good reason. I could alleviate this by introducing a framework such as Knockout, but that comes with its own learning curve.
Conclusion
So does this mean that we can forget about Visualforce when building apps that primarily execute client-side? Not entirely at present, at least in in my opinion. Bootstrap lends itself well to Lightning, as its only really concerned with the display of data - it doesn’t try to handle requests and routing, and doesn’t make much use of JavaScript to style elements. Integration with a framework that carries out progressive enhancement, such as jQuery Mobile, would require a fair bit of custom JavaScript to re-apply the progressive enhancement to the updated DOM elements.
Thanks for great writeup.
ReplyDeleteCan we navigate b/w components?. Ex: Next
It is not fair for KnockoutJS to pick up the learning curve. I see it is also to Lightning.
ReplyDeleteSo does Visualforce - two learning curves versus one for the same functionality..
DeleteThis comment has been removed by the author.
ReplyDeleteI understand what Lightning components are. But I don't understand how will it be replace visualforce in future? Currently you can build VF pages, tie it to tabs and create apps and bundle them. Can lightning components be tied to tabs instead of VF pages?
ReplyDeleteIn the future, with the Lightning Experience, there will not be tabs like there is today. There are icons on the left side, but I do see VF pages going away and being replaced by apps that are constructed using Lightning Components as they handle responsive design on all device types (desktop, mobile, tablet). It will take a while for SFDC to phase out VF pages though as that is what everyone has been using for quite some time, so they will still be supported, but you might start creating them using the Lightning Design System which uses dynamic CSS, built on SASS. This will provide a responsive design to VF pages so that they work on all devices. SFDC is making a big push to mobile.
DeleteIn the future, with the Lightning Experience, there will not be tabs like there is today. There are icons on the left side, but I do see VF pages going away and being replaced by apps that are constructed using Lightning Components as they handle responsive design on all device types (desktop, mobile, tablet). It will take a while for SFDC to phase out VF pages though as that is what everyone has been using for quite some time, so they will still be supported, but you might start creating them using the Lightning Design System which uses dynamic CSS, built on SASS. This will provide a responsive design to VF pages so that they work on all devices. SFDC is making a big push to mobile.
Deleteyou know, i wonder if you could use a javascript template engine like Mustache along with Lightning. That way you won't be having to hardcode all that markup. You'd just render the template and place the result in the DOM. It would be a start at least.
ReplyDeleteyou know, i wonder if you could use a javascript template engine like Mustache along with Lightning. That way you won't be having to hardcode all that markup. You'd just render the template and place the result in the DOM. It would be a start at least.
ReplyDelete