Introduction
This article is intended for all those angularJs geeks who love to work using angularJS and are addicted to the feature set angularJS provides to the developers to build robust client side MV* web application. In my previous example titled Single Page Web Applications using AngularJS, I focused on detailing how we can create an enterpise class application using angularJS as front end MV* frmawork.
While working on such application, I think of how such an application can work on a mobile device, where each bit of data transfer has some $ value associated to it. In order to have make an application mobile friendly, we cannot rely on the minified versions of the concatenated application JS files as they grow in size as our application grows in size. We can not also load the complete library files on start as this would be huge in size and may be the page that is rendered on the mobile browser use only 1/4 of the libraries present in the minified version.
With this thought and the background of working on an application using requireJS and backboneJS, I started thinking if we can bring same capabilities into angularJS powered web application. As of AngularJS 1.2, I couldn't find any such support for on demand loading of controllers and templates in angularJS out of box.
In an attempt to build such an application, I started googling and came across a libray angularAMD, which pretty much answered every question I was looking to solve.
This library works on the principles of AMD (asynchronous module definitions). After using the library for one of my small POC project, I found it very helpful and resembling to how I used to code in BackboneJS powered application.
This library provides almost every functionality that is provided by traditional angularJS programming.
Background
This application assumes that we have the code structure in place as defined in my previous article. The siginificant difference between the previous and this structure is in how we write our main html file and application bootstraping.
The traditional angularJS application bootstraps when it encounters ng-app attribute on a DOM element. In case of AMD based design, the application is bootstapped manully in the applications app.js file. Since there is no need to load all the modules at once, there is no need to include everything at once in the app.js files as opposed to the app.js in the traditional angularJS application.
The below code snippet will show the html file that can be used
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Angular Cart</title>
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
<script data-main="js/main" src="../bower_components/requirejs/require.js"></script>
<style type='text/css'>
@media (min-width: 768px) {
.navbar-center {
margin-left: 5%;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header app-nav-header">
<img src="images/logo.png">
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1" ng-controller="NavbarController">
<ul class="nav navbar-nav navbar-center">
<li ng-class="{active: isActive('/')}" class="active"><a href="#/">Home</a></li>
<li ng-class="{active: isActive('/products')}"><a href="#/products">Products</a></li>
</ul>
</div>
<!-- /.navbar-collapse -->
</nav>
<div class="main-content" ui-view>
</div>
</body>
</html>
The above code snippet is the typical html files rendered by the server. If you analyze this in detail you will notice that, we are loading a couple of css and one two JS files mentioned in the following code piece
<script data-main="js/main" src="../bower_components/requirejs/require.js"></script>
The two files are main.js defined under the js folder and reuire.js file. It is this require.js file that is used to load my my modules on demand.
The main.js is used to define the dependencies of the application and create the application instance to be bootstapped and call it main method to bootstrap the complete angularJS application.
The code snipped below shows a typical main.js file
requires.config({
baseUrl: "js/",
paths: {
'jquery': '../../bower_components/jquery/jquery.min',
'angular': '../../bower_components/angular/angular',
'angular-route': '../../bower_components/angular-route/angular-route',
'angular-ui-router':'../../bower_components/angular-ui-route/angular-ui-router.min',
'async': '../../bower_components/requirejs/async',
'angularAMD': '../../bower_components/angularAMD/angularAMD',
'ngload': '../../bower_components/angularAMD/ngload',
'ui-bootstrap': '../../bower_components/angular-ui-bootstrap/ui-bootstrap-tpls',
'prettify': '../../bower_components/google-code-prettify-lite/prettify',
'highstocks':'../../bower_components/highcharts/highcharts',
'highchartsng':'../../bower_components/highcharts-ng/highcharts-ng',
'bootstrap':'../../bower_components/bootstrapjs/js/bootstrap.min'
},
shim: {
'angular':['jquery'],
'angularAMD': ['angular'],
'angular-ui-router':['angular'],
'highchartsng':
{
deps:['highstocks']
},
'bootstrap':
{
deps:['jquery']
}
},
deps: ['app']
});
This files is loaded by require JS when the page loads. This is the configuration for requirJS which lists all the dependencies for the libraries.
Note that if we are using that internally uses some other library and we want some sort of sequencing, then we have to enter those is shim section as shown in the code snippet above.
This file when loaded and after loading the required dependencies in asyncronous mode, loads our application bootstrap file app.js.
The typical app.js would look like
define(['angularAMD','common/controllers/navbarcontroller', 'angular-ui-router','bootstrap'], function (angularAMD,NavbarController)
{
var app = angular.module("ngAppCenter", ['ui.router']);
app.controller("NavbarController",NavbarController);
app.config(function($stateProvider,$urlRouterProvider)
{
$stateProvider.state('Dashboard',angularAMD.route(
{
url:'/home',
templateUrl: 'partials/dashboards/dashboardmain.html',
controller: 'DashboardMainController',
controllerUrl: 'js/modules/dashboards/dashboardmaincontroller.js',
navTab: "home"
}))
.state('Products',angularAMD.route(
{
url:'/products',
templateUrl: 'partials/products/appsmainview.html',
controller: 'AppListController',
controllerUrl: 'js/modules/products/applistcontroller.js',
navTab: "products"
}))
.state('Products.Detail',angularAMD.route(
{
url:'/products/:appid',
templateUrl: 'partials/products/appsitemdetail.html',
controller: 'AppItemDetailController',
controllerUrl: 'js/modules/products/appitemdetailcontroller.js',
navTab: "products"
}));
});
angularAMD.bootstrap(app);
return app;
})
Before explaining what this file is doing, let me give you a brief introduction of AMD(asynchronous module definition) pattern,
Asynchronous module definition (AMD) is a JavaScript API for defining modules such that the module and its dependencies can be asynchronously loaded. It is useful in improving the performance of websites by bypassing synchronous loading of modules along with the rest of the site content.
In addition to loading multiple JavaScript files at runtime, AMD can be used during development to keep JavaScript files encapsulated in many different files. This is similar to other programming languages, like Java, which support keywords such as import, package, and class for this purpose. It is then possible to concatenate and minify all the source JavaScript into one small file used for production deployment.
The AMD format comes from wanting a module format that was better than today's "write a bunch of script tags with implicit dependencies that you have to manually order" and something that was easy to use directly in the browser. Something with good debugging characteristics that did not require server-specific tooling to get started. It grew out of Dojo's real world experience with using XHR+eval and wanting to avoid its weaknesses for the future.
It is an improvement over the web's current "globals and script tags" because:
- Uses the CommonJS practice of string IDs for dependencies. Clear declaration of dependencies and avoids the use of globals.
- IDs can be mapped to different paths. This allows swapping out implementation. This is great for creating mocks for unit testing. For the above code sample, the code just expects something that implements the jQuery API and behavior. It does not have to be jQuery.
- Encapsulates the module definition. Gives you the tools to avoid polluting the global namespace.
- Clear path to defining the module value. Either use "return value;" or the CommonJS "exports" idiom, which can be useful for circular dependencies.
It is an improvement over CommonJS modules because:
- It works better in the browser, it has the least amount of gotchas. Other approaches have problems with debugging, cross-domain/CDN usage, file:// usage and the need for server-specific tooling.
- Defines a way to include multiple modules in one file. In CommonJS terms, the term for this is a "transport format", and that group has not agreed on a transport format.
- Allows setting a function as the return value. This is really useful for constructor functions. In CommonJS this is more awkward, always having to set a property on the exports object. Node supports module.exports = function () {}, but that is not part of a CommonJS spec.
To define an AMD compatible module we use the syntax shown below
define(['angularAMD',<<other files to load>>],function(angularAMD,<<other object variables>>)
{
return function(){};
});
The above code snippet tells the module loader "requireJs" in our case, that this file/modules in dependent on the files mentioned in the list which is the first parameter to the define(). The module loader makes sure that the files which defines these modules are loaded into the browser before this file will be requested.
The module loader also makes sure that all the dependencies are resolved. In the above case, we are asking module loader to load angularAMD, if you see the main.js, the angularAMD is dependent on angularJS which in turn is dependent on Jquery, so in such a case, the module loader make sure that it loads Jquery first, then angularJs and finallly angularAMD before even loading the current file.
Let us look back to our app.js file. In the starting we are defining some angular controller becuase we need some functionality even before angularAMD takes charge. We define the app and configure the state provider.
The state provider in angular is part of angular ui-router module and is responsible of rendered the complex nested views.
At the end, we bootstrap the application manually. Please note that we donot use ng-app directive here in the html file.
SInce we are using angularAMD, we have to register our componets to angularAMD and hence using the syntax
app.register.controller("<<controller name>>",[<<DI stuff>>,function(){
}]);
instead of
app.module("<<module name>>").controller("<<controller name>>",[<<DI stuff>>,function(){
}]);
We have to define our components in this way when we use angularAMD library. Rest of the code is pretty much same as normal angular code. In addition to the above difference, there is a slight differnce in how we define the routes.
You will notice that while defining the routes, we are giving the url of the controller to the state provider so that it can load the controller when needed.
Using the code
I have attached a demo application with the article which will help understand the article better. The application using some fake data and uses direct JSON file for loaded data. The demo apps used following features of angularJS
Controllers
Directives
Services/Factories
On demand loading using AngularAMD
it also used Grunt and Bower and Unit testing using Jasmine and Karma
In order to use the code, we need a web server like IIS, tomcat or simple python web server. Just unzip the attached code folder and point it to the web server.
For example if using a python command, I would be doing the following to make the code running
$cd <<path to extracted folder>>
~ngamd$python -m SimpleHTTPServer 9000
Do this will launch the webserver at 9000 port and you can see the application running at
http://localhost:9000/www/#/home
When you hit the product tab, you will see
If you go to the sources tab on the crome dev tools or any other browser and you are on Dashboards page, you will not see any pages/js related to product tab loaded in the browser. The picture below shows this
As you can see, only files related to dashboard are loaded. When we click on the product tab we can see the products module gets loaded as shown below. Also note that the directive and Services are used by the products page so these modules were not loaded in the previous snapshot.
The project is unit testable in itself. I have written unit tests for the items here. The details of how to write Javascript unit test using karma and jasmine will be covered in my future articles.
Just for the completeness of this article, the project contains package.json , bower.json and other project management files.
To fully utilize these, you have to install nodeJS on to you machine. This shoule be installed globally. Once that is done, you have to install grunt, bower and Karma.
To run the unit tests, on the root, type the following command
ngamd $ karma start test/conf/karma.unittests.conf
Conclusion
This article talked about how to load angular modules on demand using AngularAMD.
History
Corrected Image Path and uploaded code