AngularJS and Salesforce Lightning Design System – Part 2

In the previous post we went over how to build a simple visualforce page with AngularJS and Salesforce Lighting Design System (SLDS) and created a reusable template to get us started with future projects.

The last app we build was very simple but it demonstrated AngularJS dynamic data binding very well. In this post we will create an application that will retrieve 10 Accounts owned by the current user from our org and display them in a table. Again this is a fairly simple example but it will demonstrate one way of getting data from Salesforce and how easily we can wire up our page to display the retrieved data.

Lightning Design System Page

<!-- PAGE HEADER -->
 
<div class="slds-page-header" role="banner">
     
 
Accounts
 
     
<h1 class="slds-text-heading--medium">My Accounts</h1>
 
     
 
1 items
 
</div>
 
<!-- / PAGE HEADER -->
<!-- PRIMARY CONTENT WRAPPER -->
 
<table class="slds-table slds-table--bordered">
     
<thead>
         
<tr>
             
<th scope="col">Account Name</th>
 
             
<th scope="col">Account ID</th>
 
        </tr>
 
    </thead>
 
     
<tbody>
         
<tr>
             
<td>Burlington Textiles Corp of America</td>
 
             
<td>00137000003SadR</td>
 
        </tr>
 
    </tbody>
 
</table>
 
<!-- / PRIMARY CONTENT WRAPPER -->

In the snippet above we define the header and the primary content. The header just shows a label and an indicator of how many records are shown. The primary content is a table displaying Account Name and Id. We also have a few slds classes on the page. Primarily the slds-page-header for the header and slds-table for the table. There are also a few helper classes for text sizing and making the table bordered. With a little over a dozen lines of HTML and a few classes we get an awesome looking page.

Page header and primary content example
Example of a page header and primary content in a table.

Remote Objects Setup

Now that we have the basic outline of our page lets figure out how we will actually get the data from our org. There are a few different ways to get data out of salesforce. Some of the options include Remote Objects, JavaScript Remoting, REST API. In this example we will be using Remote Objects to keep everything contained to this page. When using Remote Objects there is no controller or controller extension necessary. All of the data access is handled by the Remote Objects component. To make Account object accessible on our page we simply use the Remote Objects component and specify which SObject and fields to make accessible.

<apex:remoteObjects >
 <apex:remoteObjectModel name="Account" fields="Id,Name,LastModifiedDate,OwnerId"/>
</apex:remoteObjects>

If we wanted to access data from other objects we would just add more apex:remoteObjectModel elements specifying the name and fields within the parent apex:remoteObjects element. The apex:remoteObjects element generates JavaScript model classes for every child apex:remoteObjectModel element which are used to make data access calls directly from JavaScript. The code above will generate a SObjectModel.Account class which will have access to the Id and Name on Account.

Data Retrieval

Typically we would create an instance of the model class and then call the retrieve function passing the query parameters and a callback functions to get the account data. The code would look something like this:

var accountModel = new SObjectModel.Account();
accountModel.retrieve({
  where: {OwnerId: {eq: '{!$User.Id}'}},
  orderby: [{LastModifiedDate: 'DESC'}],
  limit: 10},
  function(error, records) {
		if (error) {
			alert(error.message);
		} else {
			viewModel.accounts = records;
		}
  }
);

While the code above is valid, it does not play well with Angular. The problem is that when the retrieve function gets the data and calls this callback function which assigns the returned records to viewModel.accounts, Angular will not be aware of the change to viewModel.accounts. To fix the problem we can use $scope.$apply function to call a digest which updates the data bindings. In our simple example this would work but is not necessarily a good practice. The problem is that if you call $apply during a digest you will get errors like “Digest already in progress”. To overcome this issue we can simply write a small factory wrapper for remote objects.

angular.module('SLDSApp').factory('sf', SFRemoteObjects);
SFRemoteObjects.$inject = ['$q'];
function SFRemoteObjects($q){

	function sobject(objectTypeName){
		var SObject = {};
		SObject.model = new SObjectModel[objectTypeName]();
		SObject.retrieve = function(criteria){
			var deferred = $q.defer();
			SObject.model.retrieve(criteria, handleWithPromise(deferred));
			return deferred.promise;
		};
		return SObject;
	}

	function handleWithPromise(deferred){
		return function(error, result){
			if(error){
				deferred.reject(error);
			} else {
				deferred.resolve(result);
			}
		}
	}

	return {
		sobject: sobject,
	}
}

In the snippet above we define a factory named sf and inject $q. $q is the Promise implementation available in AngularJS. The $q service provides a way to execute functions asynchronously and use their return values or exceptions when they are done processing. Since $q is integrated into the AngularJS digest we will not have to worry about our bindings not updating. Inside the factory we define a sobject function which with the handleWithPromise helper function will wrap the retrieve call with a promise. You can see that our sobject functions returns a wrapper SObject variable containing a model of the specified sobjectTypeName and a retrieve function wrapped in a promise. The handleWithPromise helper function simply checks if the data was retrieved and either resolves or rejects the promise. We can easily wrap other functions like create, update available on the Remote Object with a promise by following the same format.

Controller

Now that we have our page design and a reliable way to retrieve data from our org we can finish our Account page by writing a simple AccountController, scoping a part of the page to that controller and setting up the data bindings.

angular.module('SLDSApp').controller('AccountController', AccountController);
AccountController.$inject = ['sf'];
function AccountController(sf){
	var viewModel = this;
	sf.sobject('Account').retrieve({
		where: {OwnerId: {eq: '{!$User.Id}'}},
        orderby: [{LastModifiedDate: 'DESC'}],
        limit: 10
	}).then(function(result){
		viewModel.accounts = result;
	});
}

When defining our controller we will inject the sf factory that we created earlier. As stated before the factory will help us get data from our org by resolving the request as a promise which automatically tells AngularJS to update the data bindings. Inside our AccountController we use a capture variable for this which captures the context within our controller functions. To retrieve Accounts from our org we use the sf service calling the sobject function with ‘Account’ as a parameter. This creates a SObjectModel instance that has a retrieve function wrapped in a promise. Lastly we call the retrieve function passing the query parameters which returns a promise and executes the then function once the promise is resolved. In the then function we make the retrieved account accessible by assigning them to the viewModel.account variable.

Data Binding

With the controller completed we can update our page to setup the data bindings. We start by scoping a part of the page to our AccountController. This is done by adding the ng-contoller property on the given element. In our case we will bind the controller to the <body/> tag as shown below.

<body ng-controller="AccountController as viewModel">

Now that the controller is bound we can access any of its properties within the <body/> tag. In the header we will replace “1 items” with an expression “{{viewModel.accounts.length}} items” which will dynamically bind the number of elements in the account array to the page.

Next we modify the <tr/> tag inside the <tbody> tag by adding ng-repeat directive which will repeat the <tr> element for every account retrieved from our org. Lastly we setup the account data bindings using expressing within the child <td/> element which will display the Name and Id of a given account.

<tr ng-repeat="account in viewModel.accounts">
   
<td>{{account.get('Name')}}</td>
 
   
<td>{{account.get('Id')}}</td>
 
</tr>

Result

Screenshot of data for the Account page.
Retrieved org data for the Account page.

Code

<apex:page showHeader="false" standardStylesheets="false" sidebar="false" applyHtmlTag="false" applyBodyTag="false" docType="html-5.0">
    <html ng-app="SLDSApp" ('Account').retrieve({
                    where: {OwnerId: {eq: '{!$User.Id}'}},
                    orderby: [{LastModifiedDate: 'DESC'}],
                    limit: 10
                }).then(function(result){
                    viewModel.accounts = result;
                });
            }
 
            angular.module('SLDSApp').factory('sf', SFRemoteObjects);
            SFRemoteObjects.$inject = ['$q'];
            function SFRemoteObjects($q){
 
                function sobject(objectTypeName){
                    var SObject = {};
                    SObject.model = new SObjectModel[objectTypeName]();
                    SObject.retrieve = function(criteria){
                        var deferred = $q.defer();
                        SObject.model.retrieve(criteria, handleWithPromise(deferred));
                        return deferred.promise;
                    };
                    return SObject;
                }
 
                function handleWithPromise(deferred){
                    return function(error, result){
                        if(error){
                            deferred.reject(error);
                        } else {
                            deferred.resolve(result);
                        }
                    }
                }
 
                return {
                    sobject: sobject,
                }
            }
        </script>
        <!-- / JAVASCRIPT -->
    </html>
</apex:page>

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top