days
0
9
hours
2
2
minutes
1
9
seconds
5
3
Modular building blocks

Building modular applications with AngularJS

Moritz Schulze
Bricks image via Shutterstock

If you’ve ever wanted to modularise a large application to give it more structure, here’s your chance. Software consultant Moritz Schulze takes us through the steps for setting up a modular AngularJS application.

In a previous article on JAXenter.com, I’ve shown how to build a secured REST API with Spring. It followed how we at techdev built our time tracking application trackr.

In this article I will describe a way to build a modular AngularJS application which is suited as a frontend for a large application like this one in the last article. The main focus is the modularization of the JavaScript code in a way that helps structuring large applications. I chose to not use any JavaScript superset language like TypeScript or the modern ECMAScript 6. This will make the code more readable for people used to plain old JavaScript, but some boilerplate might be avoided with those tools. As in the last example, code for this article is provided on our company GitHub account. The commits represent the chapters.

Modularization

I will use two concepts of modules. The first one is about file dependencies and the correct order of loading the JavaScript files, the other one is about application dependencies.

RequireJS

RequireJS is a JavaScript library for Asynchronous Module Definitions (AMD). You can define modules with dependencies on other modules and RequireJS will load and provide them. It also allows for different versions of the same library to be used in the same project which can be quite useful!

// src/loadModule.js
define(['jQuery'], function($) {
   function loadSomething() {
       $.get('/load/something');
   }
   return loadSomething;
});

Here we define a module (without a special name) in the file loadModule.js that depends on the module jQuery and exports a function.

Other modules now might load this one and use the function. If jQuery was already required and loaded by another module it won’t be loaded again – RequireJS keeps track of all loaded modules and provides them.

You might wonder whether a special version of jQuery has to be used since jQuery is not a RequireJS module. Luckily not. There is a way to configure RequireJS so it knows the jQuery file will export a $ function and will wrap it into a module. You can also define dependencies between non-RequireJS modules.

This config is also my preferred way to start a JavaScript application with RequireJS. With it, you only need one <script> tag in your index.html.

// bootstrap.js
 require.config({
     baseUrl: '/src',
     paths: {
         'jQuery': 'lib/jQuery/jquery-2.11.js',
         'bootstrap': 'lib/bootstrap/bootstrap.js'
     },
     shim: {
         'bootstrap': ['jQuery'],
         'jQuery': { exports: ['$'] }
     }
 });

 require(['app'], function(app) {
     // bootstrap the app
     app.init();
 });

 // app.js
 define(['jQuery'], function(jQuery) {
     function init() {
         $.get('init');
     }

     return {
         init: init
     };
 })

 // index.html
 ...</pre>
<script src="require.js" data-main="bootstrap.js"></script>

RequireJS will load the bootstrap.js file, read the config and execute the last require call at the end of the file – which will load the application and in return all its dependencies and call app.init() after everything is loaded.

Now we can define file dependencies and move on to AngularJS modules.

AngularJS Modules

Most developers using AngularJS have already written a module – because every app is one. Most likely they also have set up some dependencies to other modules like ngRoute. In this example we will make heavy use of modules for our application.

AngularJS modules aren’t very powerful in terms of encapsulation but at least provide facilities to separate code and package reusable modules. It may seem they provide proper namespacing but that’s not the case. If you define a UserService in some module it is injectable in all other modules – and would overwrite or be overwritten by another UserService. I will show a way how I solved this namespacing problem.

I like to structure my whole application according to the business functionality. That goes for files (JS and HTML) as well as modules. In my experience this really helps when navigating the code.

I will provide you a basic structure for an AngularJS app with RequireJS. As an example, let’s create an address book app with two roles, users and admins. Every user can see a list of addresses and an admin can edit and delete individual entries.

This already cries for a user and an admin module.

// bootstrap.js
 require.config({ /* ... */ });
 require(['angular', 'app'], function(angular) {
     angular.bootstrap(document, ['jax']);
 });

 // src/app.js
 define(['angular', 'modules/user/userModule', 'modules/admin/adminModule'], function(angular) {
     var jax = angular.module('jax', ['jax.user', 'jax.admin']);
     return jax;
 });

 // src/user/userModule.js
 define(['angular', './addressBookController', './addressBookService'], function(angular, addressBookController, addressBookService) {
     var user = angular.module('jax.user', []);
     user.controller('user.addressBookController', addressBookController);
     user.service('user.addressBookService', addressBookService);
     return user;
 });

 // src/user/addressBookService.js
 define([], function() {
     return ['$http', function($http) {
         function loadAddressBook() {
     		return $http.get(...);
         }
 		return {
 			loadAddressBook: loadAddressBook
 		}
 	}];
 });

 // src/user/addressBookController.js
 define([], function() {
     return ['$scope', 'user.addressBookService', function($scope, addressBookService) {
         addressBookService.loadAddressBook().then(function(addressBook) {
             $scope.addressBook = addressBook;
         });
     }];
 });

 // src/admin/adminModule.js
 define(['angular'], function(angular) {
     var admin = angular.module('jax.admin', []);
     return admin;
 });

We don’t have any routes yet – since I will be using the ui-router later on I left them out.

You can already see how I do my own namespacing – both the service and controller have a prefix in their name. Since the prefix is separated with dots I always have to use the array notation of angular injection. This together with RequireJS leads to a lot of boilerplate strings in your files. I see it this way – Java requires me to write imports. IDEs are doing this for a long time, so you never have to bother with it. Unfortunately there’s no IDE that can generate RequireJS imports so we still have to do it ourselves.

But when your application grows to a certain size you will come to appreciate the namespacing of AngularJS services and controllers. If you do it right you can easily figure out which module a service is coming from.

The ui-router

AngularJS has a module to allow simulation of different pages, called ngRoute. With it you can configure your app to react to changes in the hash part of the current location by replacing a marked div with a template and instantiating a controller in it. So index.html#/addressbook could load a template called addressbook.html and our user.addressBookController.

This system is not very flexible. Let’s say you have a navigation bar and want to replace some of it’s content when you’re visiting a specific URL.

<nav><a href="#/admin">Admin</a> We want to insert a view here, too!</nav>
 <div ng-view id="content"/>

But you already used your ng-view to display the content. Multiple views are not supported by ngRoute. This is where the ui-router comes into play. Rather than just telling AngularJS what template to load into one view and what controller to use you build deeply nested views and hierarchical so-called states that can fill each view in the tree with a template and instantiate a controller on it. A childstate can easily replace a view that a parent state has defined. So a URL does now correspond to one state that might have several parent states and several views.

It might take a moment to get into the notation the ui-router uses, I encourage you to read the documentation on their GitHub wiki.

The ui-router goes very well with my modularization approach. Each module can define its own states that can be child states of more general states that set up the base UI.

Let’s add the ui-router to the addressbook example.

// index.html
 <head>
     <script data-main="bootstrap.js" src="require.js"></script>
 </head>
 <body>
     <div ui-view="root"/>
 </body>

 // src/app.html
 <nav class="navbar navbar-default navbar-fixed-top">
     <div class="container">
         <div ui-view="navbar"></div>
     </div>
 </nav>

 <div class="container">
     <div ui-view="content"></div>
 </div>

 // src/moduleSelection.html
 <h1>Modules</h1>
 <a ui-sref="jax.user" href>User</a><br/>
 <a ui-sref="jax.admin" href>Admin</a>

 // src/app.js
 define(['angular', 'angular-ui-router', 'modules/user/userModule', 'modules/admin/adminModule'], function(angular) {
     var jax = angular.module('jax', ['ui.router', 'jax.user', 'jax.admin']);
     jax.config(['$stateProvider', function($stateProvider) {
         $stateProvider
             .state('jax', {
                 url: '',
                 abstract: true,
                 views: {
                     root: {
                         templateUrl: 'src/app.html'
                     }
                 }
             })
             .state('jax.index', {
                 url: '',
                 views: {
                     content: {
                         templateUrl: 'src/moduleSelection.html'
                     }
                 }
             });
     }]);
     return jax;
 });

 // src/modules/user/userModule.js
 define(['angular', './addressBookController', './addressBookService', 'angular-ui-router'], function(angular, addressBookController, addressBookService) {
     var user = angular.module('jax.user', ['ui.router']);
     user.controller('user.addressBookController', addressBookController);
     user.service('user.addressBookService', addressBookService);

     user.config(['$stateProvider', function($stateProvider) {
         $stateProvider
             .state('jax.user', {
                 url: '/user',
                 views: {
                     content: {
                         templateUrl: 'src/modules/user/addressBook.html',
                         controller: 'user.addressBookController'
                     },
                     navbar: {
                         templateUrl: 'src/modules/user/userNavbar.html'
                     }
                 }
             });
     }]);
     return user;
 });

 // src/modules/user/addressBook.html
 <h1>Addresses</h1>
 <table class="table table-bordered">
     <thead>
     <tr>
         <td>Name</td>
         <td>First name</td>
         <td>City</td>
     </tr>
     </thead>
     <tbody>
     <tr ng-repeat="address in addresses">
         <td>{{address.name}}</td>
         <td>{{address.firstName}}</td>
         <td>{{address.city}}</td>
     </tr>
     </tbody>
 </table>

 // src/modules/user/userNavbar.html
 <ul class="nav navbar-nav"><li><a>Custom User navbar</a></li></ul>

 // src/modules/admin/adminModule.js
 define(['angular', 'angular-ui-router'], function(angular) {
     var admin = angular.module('jax.admin', ['ui.router']);
     admin.config(['$stateProvider', function($stateProvider) {
         $stateProvider
             .state('jax.admin', {
                 url: '/admin',
                 views: {
                     content: {
                         templateUrl: 'src/modules/admin/admin.html'
                     }
                 }
             });
     }]);
     return admin;
 });

 // src/modules/admin/admin.html
 <h1>Welcome to the admin module!</h1>

As you can see, this allows a very detailed separation of concerns. It also allows very fine and granular control for each module what to display. For example, the admin module does not want to display additional navigation information and just leaves the template empty. The ui-router has many options for a state definition and is very flexible.

JSHint

I think it helps a larger project if there are guidelines for code style that everyone adheres to. JavaScript is a very generous language in terms of syntax which can lead to very inconsistent style. A linting tool can help detect such errors with a tool and with IDE support. I use JSHint which is understood by IntelliJ and can be executed with Grunt (see next section) to automatically fail a build if something’s wrong. Since it’s very easy to integrate, I highly recommend it.

Here are some rules I enforce with JSHint:

  • Four space indentation (tabs is also possible if you prefer that)
  • Only camelcase identifiers
  • No trailing whitespace
  • Enforcement of curly braces around blocks
  • No unused variables in functions
  • Variables must be defined before used
  • Single quotation marks

Build Process with Grunt

Grunt is a great tool for AngularJS applications. It offers functionality to run certain tasks on the source code, CSS files and HTML files. It’s great to package certain tasks together into goals that can be executed, like linting or testing.

Building a Single File

While having a lot of small files containing the AngularJS code (modules, controllers, …) is great during development, you certainly don’t want that in production. You could accumulate over 50 requests only to load all parts of your application really fast and the user will notice the increased loading time from that.

Luckily there is a Grunt plugin to package RequireJS modules into single files and even minify the JavaScript along. You can even do this for single modules, so we could build app.js, jaxUser.js and jaxAdmin.js files from our previous example.

Another great use of this build phase is to replace non-minified vendor libraries with minified ones. While developing it is very helpful to have a non-minified AngularJS included since it provides better error messages. But when deploying to production you most certainly want to use a minified version. I have two additional requirements:

  1. The vendor libraries are delivered in separate files/requests because they’re probably easier to cache
  2. I want to use the provided minified versions and not minify AngularJS every time I build the project myself

Since listing the config for that here would be quite boring I refer you to my sample application. There are some tricks involved and the implementation of my requirements has some drawbacks.

CHECK OUT: Building a secure REST API with Spring Data REST and Java 8

First, in the Grunt task for RequireJS you can set a path to empty, so it won’t be included in the final file. I do this for all libraries, they should be loaded separately. Second, I keep an extra index.html and bootstrap.js just for production use. This helps referencing the minified libraries. The large drawback is, if you add a new library to your project you have to configure three places: The Gruntfile.js and both bootstrap.js files. I guess a more elaborate setup can be found to make this process easier.

I know this is probably confusing, so please take a look at the sample application to get a better understanding. Run grunt and look at the output – you get what I think is a nice deliverable.

Author
Moritz Schulze
Moritz Schulze is a Software Engineering consultant with techdev solutions.

Comments
comments powered by Disqus