Sure, let’s rephrase that:
The previous application we developed was quite straightforward but effectively showcased AngularJS’s dynamic data binding. In this post, we aim to craft an application that fetches and showcases 10 user-owned Accounts from our Salesforce org in a tabular format. While this example remains relatively uncomplicated, it serves as a demonstration of retrieving data from Salesforce and seamlessly presenting it on our page.
Page Using the Lightning Design System
<!-- 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 excerpt provided, we set up both the header and main content. The header features a label and a display indicating the number of records. Meanwhile, the primary content showcases a table presenting Account Name and ID details. Additionally, we apply several slds classes throughout the page, notably using slds-page-header for the header and slds-table for the table. We include some helper classes to adjust text sizes and add borders to the table. Despite comprising just a dozen lines of HTML and a handful of classes, these elements come together to create an impressive page layout.
Example of a page header and primary content in a table.
Configuring Remote Objects
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>
To access data from additional objects, we simply include more apex:remoteObjectModel elements within the parent apex:remoteObjects element, specifying the object name and its fields. Each apex:remoteObjectModel generates JavaScript model classes, allowing direct data access calls from JavaScript. This code snippet, for instance, creates an SObjectModel.Account class enabling access to Account’s Id and Name fields.
Fetching Data
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 code snippet provided, we create a factory named ‘sf’ and inject ‘$q’, which is AngularJS’s Promise implementation. ‘$q’ enables asynchronous execution of functions and allows the utilization of their returned values or exceptions after processing. Its integration into the AngularJS digest ensures seamless updating of bindings. Within the factory, the ‘sobject’ function, in conjunction with the ‘handleWithPromise’ helper function, encapsulates the retrieve call within a promise. Notably, the ‘sobject’ function returns a wrapped ‘SObject’ variable containing a model corresponding to the specified ‘sobjectTypeName’ and a ‘retrieve’ function enveloped in a promise. The ‘handleWithPromise’ helper function simply examines the retrieved data and either fulfills or rejects the promise accordingly. This structure allows easy wrapping of other Remote Object functions such as ‘create’ or ‘update’ into promises by following the same pattern.
Script 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 setting up our controller, we’ll include the previously created ‘sf’ factory through injection. As mentioned earlier, this factory aids in fetching data from our organization by resolving the request as a promise, prompting AngularJS to automatically update the data bindings. Within our AccountController, we utilize a ‘capture’ variable to retain the context within our controller functions. To fetch Accounts from our organization, we employ the ‘sf’ service, invoking the ‘sobject’ function with ‘Account’ as an argument. This generates an instance of ‘SObjectModel’ containing a ‘retrieve’ function wrapped in a promise. Finally, we invoke the ‘retrieve’ function with the query parameters, which returns a promise, executing the ‘then’ function once the promise is resolved. Inside the ‘then’ function, we ensure accessibility to the retrieved accounts by assigning them to the ‘viewModel.account’ variable.
Binding Data
Now that the controller is finalized, we proceed to establish the data bindings on our page. We begin by scoping a section of the page to our AccountController. This is achieved by incorporating the ‘ng-controller’ attribute onto the designated element. In our scenario, we’ll bind the controller to the <body/>
tag, as illustrated below.
<body ng-controller="AccountController as viewModel">
With the controller linked, all its properties become accessible within the <body/>
tag. Within the header, we’ll substitute “1 items” with the expression “{{viewModel.accounts.length}} items.” This dynamic binding will continuously update the displayed count based on the number of elements in the ‘accounts’ array.
Following this, we enhance the <tr/>
tag inside the <tbody>
by introducing the ‘ng-repeat’ directive. This directive iterates through every account retrieved from our organization, repeating the <tr>
element accordingly. Lastly, we establish the account data bindings within the child <td/>
element, utilizing expressions to showcase the Name and ID of each retrieved account.
<tr ng-repeat="account in viewModel.accounts"> <td>{{account.get('Name')}}</td> <td>{{account.get('Id')}}</td> </tr>
Outcome
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>