Guest post David is co-founder of Aerobatic, a cloud hosting platform optimized for single page HTML5 applications.
The sample app is hosted on the Aerobatic cloud platform which is highly specialized for just this sort of HTML5 / REST API application. While nearly all of the code will work without modification as static files on their own, Aerobatic provides an enhanced platform that further streamlines the delivery of single page apps including: a smart API proxy, simulator mode that eliminates test environments, traffic routing, automatic CDN hosting, and more.
This post builds upon a recent post I wrote over on the Aerobatic blog entitled: Securing an HTML5 Salesforce Connected App. That article describes some different approaches to handling the two basic aspects of securing a Salesforce connected app: authenticating via the Salesforce OAuth service, and utilizing the access token to make authenticated API calls. I suggest you read that article first, then come back here to learn how the rest of the sample application is built and tested.
Live Demo and Source Code
You can try the sample app out for yourself at https://sfcontacts.aerobaticapp.com. All the source code is available on GitHub at https://github.com/aerobatic/sfcontacts. Check out the README for instructions on how to fork the app to play around with and extend on your own.
The Case for Single Page Applications
Before diving into the technical details, it’s worth talking briefly about why the single page approach is emerging as the preferred way to build modern web applications. In my mind it really boils down to two things. Most importantly is the enhanced level of responsiveness and smoothness to the user interactions. A well implemented single page app feels more akin to a native mobile application than the traditional collection of hyperlinked web pages.
Alright, enough with the background, let’s get to the meat.
Our application is structured using common Angular conventions. Every individual module including services, controllers, and directives are housed in a dedicated .js file. The application uses Grunt for running the tasks that power the developer workflow (including the grunt-aerobatic tasks) and bower for client dependency management. Our Gruntfile is configured to combine and minify all our scripts into one so there is no penalty for structuring the code for optimal maintainability.
Third Party Dependencies
- Angular – Goes without saying.
- Lodash – Lots of nice code-saving utility functions.
- Bootstrap – Let’s us non-designers build reasonably attractive UIs. Also provides a solid foundation for responsive layouts that adjust according to the screen size.
- Bootswatch – A nice collection of custom Bootstrap themes. I’m using the Flatly theme.
- Bower – Install client library dependencies.
- Grunt – Powers the developer build and deploy workflow.
The Gruntfile is configured to handle common build tasks like combining and minifying scripts and stylesheets. It’s also using the ngAnnotate task which converts shorthand Angular dependency injection declarations to the long form that can be safely minified. This allows authoring simpler, more readable Angular code.
The grunt-aerobatic module includes the sim and deploy tasks. Running
grunt sim will start the development simulator for running the app locally. It’s actually a hybrid mode where the single page is running in the cloud but assets are downloaded from localhost. Running
grunt deploy deploys a new version of the app to the cloud.
The development simulator works in conjunction with the grunt watch task to enable livereload behavior where the browser reloads itself whenever a file changes.
Rather than calling the Salesforce REST API directly from the controller, we can achieve better separation of concerns by encapsulating all API interactions within an Angular service module. The Salesforce service exposes three basic CRUD operations:
deleteContact is left as an exercise for the reader). The $http service, like all things asynchronous in Angular, is built on promises provided by the $q service. It pays to get familiarized with how Angular handles promises. I found them a little tricky to initially get the hang of, but they are a powerful way to manage complex asynchronous code flows without descending into callback hell.
The relevant Salesforce for the API calls can be found here:
buildHttpConfig is a helper function that builds up the config object that is passed to the
$http service. The Salesforce REST API is not CORS enabled, so you can’t call it directly from the browser on a different domain. Aerobatic provides an API proxy that will relay the call on behalf of the browser. If you are using the Salesforce user-agent OAuth flow, the access token would be passed in the X-Authorization header. However Aerobatic provides a built-in OAuth option that stores the access token on the server in session state. Rather than pass the token to the browser, a token placeholder
@@user.accessToken@@ is specified which the proxy will substitute with the actual token that was granted by Salesforce – eliminating the need for the browser to store sensitive information. The security blog post covers this in more detail.
loadContacts function runs an SOQL query to fetch a subset of attributes for each of the contacts. I could have returned the result of the $http service directly but I prefer wrapping it in a deferred so that the Salesforce service has an opportunity to act on the response data or do specialized error handling before passing control back to the caller.
updateContact function makes a PATCH request where the Id of the contact to be updated is specified in the path. I’m omitting the Id attribute from the contact in the body of the request as only the attributes with modified values should be specified, and the Id never changes. All remaining attributes are being passed, but if the app knew that only certain attributes like say the FirstName and LastName were modified, just those two could be passed saving some network bandwidth.
Handling Unauthorized API Responses
If the OAuth access token is no longer valid, Salesforce will return a 401 (Unauthorized) status code. Whenever this happens I want to log the user out and require them to re-authenticate so a new access token is granted. I could trap for the 401 status in each API call, but Angular provides a better mechanism for dealing with cross-cutting boilerplate logic for $http calls in the form of $httpInterceptors. Interceptors allow global filtering of http requests as they leave the application and inspecting responses as they return. In this case I just implement the
responseError interceptor and redirect to the Aerobatic logout URL if the status is equal to 401. Interceptors are registered in the app config event using the $httpProvider.
The app has three different controllers:
LoginCtrl simply exposes the URL for the login button to the view and determines if a session timeout error message should be displayed by detecting the string “error=expired” in the query string.
MainCtrl handles the main page of the application that renders the list of contacts. The Salesforce service is dependency injected into the controller. The
loadContacts function is invoked immediately upon instantiation.
divideIntoRows function splits the array of contacts into sets of 3 to support binding to the responsive Bootstrap grid. Creating a new contact or editing an existing contact launches a modal form and transfers control to the
ContactModalCtrl. The $modal service from AngularUI Bootstrap provides a nice mechanism for passing data back and forth. The
ContactModalCtrl takes a dependency named
contact which resolves to the contact passed into the
openContactModal function. In the case of a brand new contact the value is null.
The saved contact is returned back in the
modalInstance.result as a promise that, upon resolving, is inserted at the start of the list. This is consistent with how the Salesforce API orders contacts where the most recently interacted one comes first. Angular will automatically rebind the grid as part of the digest cycle.
The contactModal.html template binds textboxes to the various attributes of the contact model.
When the form is submitted the saveContact function is invoked which calls either updateContact or createContact on the Salesforce service depending on whether the contact model has an Id attribute. If the operation succeeds the contact model is passed back to the
MainCtrl in the
Normally I would do some error handling on the client, but I intentionally left that out in order to demonstrate how to handle errors returned by the REST API. Obviously the API can’t trust that the data it receives is valid. If the API receives invalid data it will return a 400 (Bad Request) response status along with a JSON array of error objects in the response body:
If the promise is rejected, the error callback extracts the message attribute from each error and binds to a list at the top of the form:
The angular-mocks library is important to include as it provides necessary helper functions for injecting mock service dependencies to isolate tests from external resources and to allow testing of one module at a time.
For testing the
MainCtrl I want to use a mock Salesforce service. This turned out to be a bit trickier than anticipated but this blog post ultimately provided the breakthrough.
The trick here is to force a digest cycle to occur using the
scope.$root.$digest(); line so that code that executes as part of the promise callback will have completed before making assertions.
Unit testing the API calls in the Salesforce service entails using the $httpBackend to fake out the actual network calls. The $httpBackend includes several
expectXXX style functions that verify specific network calls were made matching on the url, verb, headers, and request body. If a match is made, a canned response is returned.
While this sample app doesn’t do anything you can’t already do within Salesforce itself, the main point was to demonstrate how to consume the Salesforce REST API within an HTML5 single page application and how Angular is a great framework with which to build it. Be sure to check out my previous blog post that goes into greater depth on the different security considerations, including how to setup OAuth and proxy API calls. Hopefully this provides a foundation on which you can build more interesting Salesforce apps that tie together data from multiple services.
Finally I’d encourage you to check out Aerobatic to see how a hosting platform that is purpose built for single page applications can augment your app delivery with enhanced security, performance, and workflow optimizations. Please feel free to reach out to me at firstname.lastname@example.org with questions, feedback, or to discuss a particular use case