Scaffolding of a multipage Angular app in Salesforce

Introduction

This blog post assumes that you are familiar with how to setup a simple Angular App (with services, views, ...) and you understand the basics of Salesforce JS Remoting. You should understand also UI Router, a state based router written for Angular.

You got it, this post is less an introduction to all these frameworks/libs, than an example how to setup an application for a massive Angular app under Salesforce.

Architecture

Whoever knows Salesforce knows that the usual way to build applications is with Visualforce Pages. Some Visualforce Pages application are built with a lot of rendered outputPanel, while others may be built with a different visualforce page (or visualforce component) for each view.

An angular application will work slightly differently. You may built an application with some ng-if or ng-show but that's good for a 2 or 3 pages application, then it becomes really ugly, like in Visualforce with rendered components. A more scalable solution is by using a router, which assigns for each URL in the browser a different view. Angular has already its own native Router but it's rather limited since it doesn't support states (i.e. it's based rather on the URL of the browser). For this project, I have done my development with UI Router, which supports much more options.

First, I had started to write for each view a new Visualforce page but it became quickly a mess, since Visualforce Pages cannot be sorted in folder (like in a normal Server Application). My idea was then to use Static Resources in order to store the views. It worked well and was actually, from a performance point of view, much faster since Static Resources are unlike VF Pages not interpreted by Salesforce when they are sent (a simple VF Page took for me 250ms around to be sent while a static resource was sent in 5ms...). The only limitation is that by using static resources, you won't benefit from the Visualforce Markup, Labels, translations, ... Of course, it's not hard to rebuild that with JS Remoting, but it's not the matter of this blog post.

The second challenge was for loading JS Scripts (i.e. especially the Angular Controllers of the views). While the easy approach can consist of loading all the scripts directly on the page, this may result in a slower starting time. It's rather much smarter to load the scripts on-demand. There is no easy solution for that but someone has developed a module called Angular Couch Potato, based on require.js which, allows not only to easily integrate require.js to angular but also to load scripts on demand.

.state('header', {
    abstract: true,
    resolve: {
        a: $couchPotatoProvider.resolveDependencies(['controllers/main'])
    },
    views: {
        'main': {
            templateUrl: require.toUrl('views/header.html'),
            controller: 'main'
        }
    },
    onEnter: function(){
        console.log("enter header");
    }
})

As you can see above, it's just about writing $couchPotatoProvider.resolveDependencies in the resolve property of UI-Router. Then we can call the controller main in the corresponding view. What you may see is also require.toUrl for loading remotely the partial stored in the Static Resource. require.toUrl corresponds to the absolute path of the Resource, defined on the only one VF Page of the whole application (see the baseUrl):

require.config({
    baseUrl: "{!$resource.appResources}",
    paths: {
        'angular'               : 'libraries/angular.min',
        'angular-ui-router'     : 'libraries/angular.ui-router',
        'angular-animate'       : 'libraries/angular.animate.min',
        'angular-couch-potato'  : 'libraries/angular-couch-potato',
        'views'                 : 'partials'
    },
    shim: {
        'angular': {
            exports: 'angular'
        },
        'angular-ui-router': {
            deps: ['angular']
        },
        'angular-animate': {
            deps: ['angular']
        }
    }
});

In order to keep the states between the views, I have divided the application into 3 panels: a header, a sidebar and the main view. When you navigate between contacts and accounts, you may notice that the controller of the sidebar is not refreshed, so that the state itself is not reset. However, if you navigate to the third page, this will destroy the sidebar controller. Angular cache also the resources (partials and scripts) downloaded on-demand. You may see this behaviour in your network developer tool.

Further tweaks

I have added some CSS3 transition between the views. Also, instead of making a $scope.$apply() for resolving the asynchronous JS Remoting call outside the Angular Scope, I have used the internal $q promises, which provides a more cleaner way to perform Remote Calls:

define(['app'], function(app) {
    app.service('http', function($q){
        return {
            getAccounts: function() {
                var defer = $q.defer();
                ngMainController.getAccounts(
                    function(result, event) {
                        if(event.status) {
                            defer.resolve(result);
                        }
                        else defer.reject(event.message);
                    },
                    {escape:false, buffer: false}
                );
                return defer.promise;
            },
            ...
        }
    });
});

Having that, it's really straight forward to use it in an Angular Controller:

http.getAccounts().then(function(res){
    $scope.table = res;
}, function(err){
    console.error('err', err);
});

Conclusion

While with such approach, we are going quite far away from the Salesforce standards, we can easily imagine how flexible and scalable this application may be. It also provides a UX a usual Visualforce Application is far to have. This application is however really intended for complex multi pages application. A single (Angular or not) application doesn't require such complicate architecture.

You can find a demo here.

As for the source code, it's available on GitHub.

comments powered by Disqus