A large application example with AngularJS ( >=v.1.3)

Ugly preface

Skip the bullshit below if you’re in a hurry…
There are loads of tutorials now for any version of AngularJS starting from version 1.0.x walking you step-by-step through the process of creating a Single Page Application. Even I got the idea… The guys behind Angular have the habit of the “breaking changes”, introducing some major changes in the syntax and mechanism of the things, so I joined the club with a small tutorial for AngularJS 1.3 as it took me about a day to compile a working application.

What I didn’t see anywhere was a simple, performance-driven tutorial for a back-end developer like me where a system was built from ground up, with no strings attached. I needed someone to show me:

  • Loading the core stuff with Require.JS – a simple start-up script that loads only the required stuff.
  • Lazy-loading of AngularJS components with Require.JS – A large-scale application would have hundreds of controllers and services and there is no browser so far that can handle all of that. So following the minimalist approach I want to load only the things that I need.
  • Dynamic loading of views and controllers – A big system would have literally thousands of actions, spread around about hundreds of views, each handled by a separate controller. Again – there is no browser that wouldn’t behave as an asshole if you try to load all of that upon page entry. Just forget it!

This was all I needed!
All around the internet, any tutorial/example that I met was actually as if it was the old corporate world – they give you an enterprise app example, but they’re going to omit exactly the key features “that you won’t understand” that would really save you months of reading, because if you know that you’ll steal their bread. Everything is like this is a top-secret stuff that someone lost in a bet and is forced to release some info, so he kept it blurry and short.

 The task

The task is to make a SPA with AngualarJS and RequireJS that would load all of its necessary components dynamically, including templates, controllers, services and etc. The bootstrap should be loaded in a matter of seconds, so that the user would have a response quickly, while the rest of the stuff is loaded. And yet only a small portion should be loaded initially, leaving the rest to the lazy-loader. That’s all.

My wife needed a small application for her smartphone to track her books, as she passes through some bookstore, so this was the perfect moment.

Disclaimer

I’m not the smartest guy on the planet, so a lot of the things here I stole from this or that tutorial or example. I will try to mention all of the useful articles I met in the appropriate place. A big thanks to all the authors. If you see something I missed, please add a comment below.

  • All of this is for Angular 1.3. (I’m annoyed to write this, but no one knows what would change in 1.4)
  • I assume you have an idea of how at least a basic Angular SPA works.

The code

Start your environment. I use Vagrant with Puphpet, some UI developer prefer Express and NodeJS. Even a simple WAMP/LAMP would work.

All the code can be found on my git repo. I’ll keep it updated, as this is a real-life application that I will use. Any fixes/suggestions are welcome.

The main html file is actually rather stupid. It contains only the basic markup. Place here any CSS you will use, initial layout markup or generally anything.

A nice idea is for example to place here a loading animation, that you will remove/delete after the SPA is fully loaded. Very nice for mobile sites or even mobile apps built with Apache Cordova.

So, once you have your startup page done, just include your require.js file in the HEAD tag. Not another line of JavaScript include should be put here, unles your initial screen would require it. What I didn’t know is that you can instruct RequireJS for the initial script to be executed with a simple data-main attribute. It will automatically load it upon initialization (put the file name without the extension).

The code below illustrates what I meant. Notice the “meta viewport” parameter, that disables zoom-in and out so that all looks nice on a mobile page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script data-main="js/main" src="js/require.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css"/>
<link rel="stylesheet" href="css/font-awesome.min.css"/>
<title></title>
</head>
<body>
<nav class="navbar navbar-default navbar-static-top" role="navigation" id="topNav">
...
<li><a href="#/" class="navbar-link"><i class="fa fa-home"></i> List</a></li>
<li><a href="#/add" class="navbar-link"><i class="fa fa-plus"></i> Add book</a></li>
...
</nav>
<div data-ng-view=""></div>
</body>
</html>

Do not put the ng-app directive anywhere. We will manually initialize Anguar when all needed files are available.

Next goes main.js. If you haven’t dealt with Requre before, the idea is very simple – you list all the scripts to be loaded and setup a function to be executed after all is done. Now if we just pour in the list of angular.min, angular-route.min and app scripts right in, you’ll get an ugly error. This is due to some dependencies, for example angular-route.min, relies on angular.min and app relies on both of the above to be loaded (I will ommit the “.js” extension when talking for a file name). We will use the shim property to define these initial relations and we will define further relations inline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
document.getElementById('topNav').style.display = 'none';
require.config({
baseUrl: './js/app',
urlArgs: 'v=1.0',
paths: {
angular: "angular.min",
angularrouter: "angular-route.min"
},
shim: {
angular: {
exports: "angular"
},
angularrouter: {
deps: ['angular'],
exports: "angularrouter"
}
}
});

require(
[
'angularrouter',
'controllers/booklistController',
'app',
'services/routeResolver'
],
function () {
angular.bootstrap(document, ['booksApp']);
document.getElementById('topNav').style.display = 'block';
});

AS you see above, we have a config section, there we first list the basic modules. Each has an ‘exports’ value, that we will use in other modules ‘deps’ array. The ‘deps’ array, as you can guess, defines what needs to be loaded before this module can function. As simple as that.

Later on in the code we have the require function that we load our controller, the angularrouter module and the main app module. I’ll explain the last routeResolver a bit later. The Angular magic hapens in the function() parameter with the first line where we initialize it by passing the main app module. (remember when I told you to skip the ng-app directive?)

Dependencies

Now comes the confusing part. We will have two dependency lines. On one hand we have the RequireJS definition, that defines file dependencies. This basically explains to RequireJS what should be loaded before the code from this file can be executed. The following line defines that:

1
define(['angular', 'services/routeResolver'], function (angular, routeResolver) {

On the other hand we have the Angular dependancies that you know from the other tutorials, let’s say, the factories, custom modules and etc.

1
var app = angular.module('booksApp', ['ngRoute', 'routeResolverServices'])

In our main module we define the app variable, that will be used further in the code, as we will deviate from the standard way of setting controllers and services, that you know from the other tutorials. A chunk of code showing this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var app = angular.module('booksApp', ['ngRoute', 'routeResolverServices'])
.config(['$routeProvider', 'routeResolverProvider', '$controllerProvider',
'$compileProvider', '$filterProvider', "$provide", "$httpProvider",
function ($routeProvider, routeResolverProvider, $controllerProvider, $compileProvider,
$filterProvider, $provide, $httpProvider) {
app.register =
{
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};

//Define routes - controllers will be loaded dynamically
var route = routeResolverProvider.route;

$routeProvider
.when('/list', route.resolve('Booklist'))
.otherwise({
redirectTo: '/list'
});
}
]);

app.run(['$rootScope', '$location',// 'authService',
function ($rootScope, $location) {
//Client-side security may go here
}]);

return app;

Our main app relies on a service, that contains a routeResolverProvider. This is the part of the code where another magic happens. I shamelessly stole it from this blog post. For an in-depth explanation go and read it, as I will only quickly describe the idea.

Dynamic loading of controllers and views

Now this is a part that is covered in several posts. The general idea is that Angular is not planned for dynamic loading of resources. In fact the only dynamic part in AngularJS is the views, that are loaded upon route definition. So what we want to do here is to let ourselves load the controllers on-demand as well. That is when the route is defined. As I mentioned above, we see a brilliant example of that in the CustommersApp example from Dan Wahlin, where he explains the creation of a routeResolverProvider. The general idea is that Angular already has a way to dynamically load views, so with a bit of coding he managed to use it for loading controllers as well.

This provider has a single method, called resolve, and as you can see it can get only one parameter. Here is the brilliant part. Within the very routeResolverProvider we set the base path for the controller files, and from that point on the magic code automatically constructs a file name for the controller and for the view, based on the string you provided. This is the magic of “Convention over Configuration”.

So instead the standard params we pass in the route configuration, we only define the path and let the resolver do its job .when('/list', route.resolve('Booklist')). You can steal the code of that routeResolver from Dan’s Git repo containing a full-fledged example. (The file is inside “CustomerManager/app/customersApp/services”, to save you a bit of time).

At a later stage I will show you what I got from another place – dynamic adding of entire paths, so the initial route config would only contain what we need at the startup page.

The controller is different as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
'use strict';
define(['app', 'angular', 'services/booklistFactory', 'services/routeResolver'], function (app, angular) {
var booklistController = function ($scope, $route, booksFactory, routeResolverProvider) {
// load data from server or take the cached version
if (booksFactory.getBooks().length == 0) {
booksFactory.refreshBooks().then(function (response) {
$scope.booksList = response.data;
});
} else {
$scope.booksList = booksFactory.getBooks();
}

addRoute('/add', routeResolverProvider.resolve('Addbook'));

$scope.refreshList = function () {
booksFactory.refreshBooks().then(function (response) {
console.log(response.data);
$scope.booksList = response.data;
$scope.filter.title = '';
});
}

...

addRoute('/add', routeResolverProvider.resolve('Addbook'));

booklistController.$inject = ['$scope', '$route', 'booklistFactory', 'routeResolver'];
<span style="line-height: 1.5;">app.controller('BooklistController', booklistController);
</span>});

We have the define construction to notify RequireJS that we need several scripts to be loaded, before we can do anything. As you’ve noticed here, I added the booklistFactory, that I will use to manipulate the books data. That’s OK as Require will first fetch it before angular will be able to proceed.

A thing that you can see is that unlike other tutorials, we no longer create separate module for the controllers, but rather reuse the global app module. we define a function with its DI(Dependency Injection) needs, then we use the $inject array, to actually inject those, and once we call the app.controller line, we ask the main app to create this controller with these properties.

Just bare in mind to list in the Require definition all the needed external files, that you need for your code. This way Require will fetch them at your disposal.

Dynamic definition of routes within controllers

As you noticed there is a line of addRoute() call that, logically intends to add another route to the main routes config, but here we do it on-the-fly. This is another last piece of my all-dynamic goal. We needed to be able to add routes on demant (a given controller may link to other views and etc. that we don’t want loaded in the beginning).

I looked a lot for a way to make routes dynamic as well. I finally found a hack in a StackOverflow thread, that showed clearly, that we can find all the routes in a simple $route property. So with a little code we can add more routes on-demand, even if it is a dynamic one, as ours are.
Here is the code for the addRoute function. It might be better to put it may be in the app object so that it’s available all around the code. (The original is on this JSFiddle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function addRoute(path, route) {
$route.routes[path] = angular.extend({
reloadOnSearch: true
},
route,
path && pathRegExp(path, route));

// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length - 1] == '/') ? path.substr(0, path.length - 1) : path + '/';

$route.routes[redirectPath] = angular.extend({
redirectTo: path
},
pathRegExp(redirectPath, route));
}

return this;
};

function pathRegExp(path, opts) {
var insensitive = opts.caseInsensitiveMatch,
ret = {
originalPath: path,
regexp: path
},
keys = ret.keys = [];

path = path.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)([\?\*])?/g, function (_, slash, key, option) {
var optional = option === '?' ? option : null;
var star = option === '*' ? option : null;
keys.push({
name: key,
optional: !! optional
});
slash = slash || '';
return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (star && '(.+?)' || '([^/]+)') + (optional || '') + ')' + (optional || '');
})
.replace(/([\/$\*])/g, '\\$1');

ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
return ret;
}

In the fiddle code you can see that they call this addRoute with regular parameters, too.

For completeness of the whole application I will give you the definition of the factory I use above to access the data. You know, just in case…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use strict';
define(['app'], function (app) {

var booklistFactory = function ($http) {
var factory = {};

factory.data = {books: []};

factory.getBooks = function () {
return factory.data.books;
};
factory.refreshBooks = function () {
return $http.get("/bookspy/load.php").success(function (data) {
factory.data.books = data;
});
};
factory.addBook = function (data) {
factory.data.books.push(data);
console.log(factory.data.books);
};
return factory;
};
booklistFactory.$inject = ['$http'];
app.factory('booklistFactory', booklistFactory);
});

 

Final words

Everything I showed you above is figured out by someone else. I just spent a day to assemble it all into a working app that I put on my Git here. I needed it for my work and I did it for my wife’s application so this was a perfect occasion to give some hints to the others out there, trying to go the same path.
I didn’t want to reinvent the wheel, so the whole credits go to the guys I link to, as they did the big coding.

Any comments?

One Response to A large application example with AngularJS ( >=v.1.3)

  1.  

    Thank you Very much. This is what I am looking for. Will check and comment back.

leave your comment

Please type the characters of this captcha image in the input box

Please type the characters of this captcha image in the input box