Last Updated: August 22, 2023
·
196K
· jdobry

Building large apps with AngularJS

New to AngularJS? Confused by directive mumbo-jumbo? Giddy over Angular's magic and your new productivity? Jaded by gotchas you didn't see coming? Enjoying the zen of Angular?

Check out my presentation on building large apps with AngularJS

Whatever your experience with Angular and whatever size of project you're working on, there will come a day when you encounter Angular in the wild being used on a large project. Does that sound like a dream come true or are you still wary? Either way, you will inevitably run into a problem that Angular does not solve for you. Will you survive?

If you have needed to:

  • Organize your files
  • Name your files
  • Lazy-load your code
  • Serialize application state to the URL
  • Derive application state from the URL
  • Manage a Model layer
  • Do smart caching
  • Work with a RESTful interface (for real)
  • Use a 3rd-party lib with Angular

Then you've already met the beast.

Angular is not the framework to end all frameworks, however, never before have I derived so much pleasure from web development. Not with NodeJs, not with NoSQL, not with BackboneJS. To misquote @ryanflorence: "I'm Sick of the Web: [Angular]'s Healing Balm".

My first few weeks with AngularJS I was like a kid on Christmas morning, unwrapping presents as fast as I could and running around showing everybody. Then my new race car broke after 10 loops around the track, I found that none of my previous toys played nice with my new ones, and I realized I would need a Ph.D. to understand my new book "Directives for Dummies". After a few months of hate/love, things started to click. Directives became my best friends, Angular best practices got me up and running, and I began derive real satisfaction from my work.

Now before you think it's all fun and games, here are some points to consider as you embark on your journey building large apps with Angular:

Convention vs Configuration

When a framework embraces conventions, developers familiar with the framework can move from project to project with minimal "ramp up" time in order to become familiar with each project. It basically comes down to familiarizing oneself with the project's business domain. The purpose of conventions is to improve:

"Conventionless" frameworks result in fragmentation, meaning that one developer's use of a framework can differ wildly from another's. Example: BackboneJS. An excellent tool in and of itself—but decidedly lacking in enforced conventions. Have you ever seen two Backbone developers do something the exact same way? They might look at each others' project and have no idea what is going on. Freedom has its downsides.

In regard to convention, AngularJS is a little on the light side. AngularJS puts itself forth as a "MVW" framework loosely based on a traditional MVC framework. Angular Controllers setup and add behavior to $scope objects, and Angular's templates and two-binding make for a nifty View layer, but Angular has virtually no opinion when it comes to your Model layer. You can do whatever you want.

The path to successfully building a large, fast, and stable application with AngularJS lies in finding and following conventions for you and your dev team. The AngularJS documentation doesn't help much with that, but many developers are becoming experienced with Angular and have shared the conventions that have helped them find success. You can't commit to writing your large application with AngularJS without paying attention to the mailing list, the GitHub project, and the opinions of Angular's core contributors.

Model Layer

Large applications often have a lot of data to deal with, so designing a solid Model layer is essential to building and maintaining a stable Angular application. Angular Services provide a useful tool for separating your Model layer or business logic from your View and Controller layers. $rootScope and $scope are useful as the Model layer for small applications, but can quickly become a mass of messy prototypal inheritance chains that may or may not work the way you think they will. For example:

$scope.fruit = 'banana';
var $newScope = $scope.$new();

$scope.fruit; // 'banana'
$newScope.fruit; // 'banana'

$newScope.fruit = 'apple';
$newScope.fruit; // 'apple'

$scope.fruit; // 'banana' // Did you think it would be 'apple'?

In a large AngularJS application it is important to have a source of truth for your data. Keep your data in your Angular Services and use your Controllers to expose your data to your views.

Your services own your data:

app.service('MyService', function ($http, $q, $angularCacheFactory) {
    var _dataCache = $angularCacheFactory('dataCache', { 
        maxAge: 3600000 // items expire after an hour
    });
    /**
     * @class MyService
     */
    return {
        manipulateData: function (input) {
            var output;
            // do something with the data
            return output;
        },

        getDataById: function (id) {
            var deferred = $q.defer();
            if (_dataCache.get(id)) {
                deferred.resolve(_dataCache.get(id));
            } else {
                // Get the data from the server and populate cache
            }
            return deferred.promise;
        }
    };
});

Your controllers and directives consume your data:

$scope.$watch(function () {
    return MyDataService.getMyData();
}, function () {
    $scope.myData = MyDataService.getMyData();
});

Ultimately, you'll want your Model layer to support the needs of a large client-side application. Many tools exist. The point is to not let Angular do your Model layer for you, because you'll be disappointed. Look into something like $resource, restangular, BreezeJS, or even using BackboneJS Models/Collections. Treat your data with respect.

File Organization

Found at the heart of many an argument is the issue of file organization and naming. Where do we put everything? Do we organize by type, form, function, or feature? PascalCase or camelCase? Some frameworks make this decision for you by enforcing a convention. Ruby on Rails, for example, expects to find your controllers in myApp/app/controller/ and your views in myApp/app/views/. This leaves little room for argument or ambiguity. Some frameworks also use naming conventions to find your files. Here is an example taken from the EmberJS website:

App.Router.map(function() {
    this.route("about", { path: "/about" });
    this.route("favorites", { path: "/favs" });
});

When the user visits /, Ember.js will render the index template. Visiting /about renders the about template, and /favs renders the favorites template.
Note that you can leave off the path if it is the same as the route name. In this case, the following is equivalent to the above example:

App.Router.map(function() {
    this.route("about");
    this.route("favorites", { path: "/favs" });
});

The AngularJS framework itself does not provide any such conventions. It's all up to you. Below are some possible solutions.

Monolithic files

The angular-seed project recommends something like the following for project file organization:

partials/
    home.html
    login.html
    users.html
    orders.html
js/
    controllers.js
    directives.js
    filters.js
    services.js
    app.js

This works great for small, focused applications, but quickly becomes untenable with large applications.

Monolithic folders

A step in the right direction, but can quickly become ridiculous. Imagine searching through the controllers/ folder when it contains 100+ files, trying to find the code for some obscure piece of functionality that your product manager wants to tweak.

js/
    controllers/
        homeController.js
        loginController.js
    directives/
        usersDirective.js
        ordersDirective.js
    filters/
    services/
        userService.js
        orderService.js
        loginService.js
partials/
    home.html
    login.html
    users.html
    orders.html
app.js

Organizate by feature

I personally like to be able to visually map what I see on the screen to the source files that make it happen. If I am looking at the "users" page then I expect to be able to find all of the code for it in the "users" package/folder. Modifying the "users" page now means making changes to files in one package/folder, instead of performing "shotgun surgery".

orders/
    directives/
        orders.html
        ordersDirective.js
    services/
        orderService.js
users/
    directives/
        users.html
        usersDirective.js
    services/
        userService.js
home/
    controllers/
        home.html
        homeController.js
        login.html
        loginController.js
    services/
        loginService.js
shared/
    services/
        i18nService.js
    filters/
        i18nFilter.js
app.js

Modules and lazy loading application components

Angular Modules are good for nothing...so far. Except for loading 3rd-party angular code into your app and mocking during testing. Other than that, there is no reason to use more than one module in your app. Misko said of multiple modules: "[you] should group by view since views will be lazy loaded in near future". I'd love for someone on the Angular team to enlighten us on what their plans are for that.

Angular does not officially support lazy loading of components. While different workarounds exist, they can result in undefined behavior because Angular expects everything to be loaded during the bootstrap phase. Kudos to Ifeanyi on a lazy-loading solution.

Here's the basic idea:

var app = angular.module('app', []);

app.config(function ($controllerProvider, $compileProvider, $filterProvider, $provide, $animationProvider) {
    // save references to the providers
    app.lazy = {
        controller: $controllerProvider.register,
        directive: $compileProvider.directive,
        filter: $filterProvider.register,
        factory: $provide.factory,
        service: $provide.service,
        animation: $animationProvider.register
    };

    // define routes, etc.
});

Lazy-load dependencies on a per-route basis:

$routeProvider.when('/items', {
    templateUrl: 'partials/items.html',
    resolve: {
        load: ['$q', '$rootScope', function ($q, $rootScope) {
            var deferred = $q.defer();
            // At this point, use whatever mechanism you want 
            // in order to lazy load dependencies. e.g. require.js
            // In this case, "itemsController" won't be loaded
            // until the user hits the '/items' route
            require(['itemsController'], function () {
                $rootScope.$apply(function () {
                    deferred.resolve();
                });
            });
            return deferred.promise;
        }]
    }
});

itemsController.js

// reference the saved controller provider
app.lazy.controller('ItemsController', function ($scope) {
    // define ItemsController like normal
});

items.html

<section data-ng-controller="ItemsController">;
    <!-- a bunch of awesome html here -->;
</section>;

Understand the $scope life cycle

$scope is the lifeblood of AngularJS. You cannot build a large application with AngularJS and avoid the intricacies of the $scope life-cycle. The $rootScope of your application and all of its child $scopes contain $watch expressions. There is a $watch expression for everything you put onto $scope. This is the magic of Angular's two-way data-binding.

Various events trigger what's called a $digest loop that checks each $watch expression to see if the value being watched has changed. If it has changed, than Angular executes all of the listeners for that variable. This can happen many times in a second. It is important for your watch expressions to be fast and idempotent. Performance can become an issue if you attach functions as $watch expressions to your $scope. These functions will be executed many times and can slow down performance. If any of the listeners fired by changing $watch expressions create yet more changes, which kick off more $digest loops which end up firing yet more listeners, then performance can be adversely affected.

3rd-party integration with AngularJS

If you've spent any real amount of time with Angular than you've probably run into problems trying to make 3rd-party libraries work with Angular. If you're building a large application, chances are you need to integrate several such libraries with your app. Angular thrives on its $digest loop, giving you the two-way data-binding magic. 3rd-party libraries don't know what Angular does. If your 3rd-party library changes something in the DOM, or returns you a value via an AJAX call, Angular doesn't know about it and continues doing its thing, none the wiser. The keys to 3rd-party integration with Angular are $scope.$apply(), $scope.$evalAsync, $q.when(), and $timeout.

When something happens with a 3rd-party library, then you'll probably need to manually kick off a $digest loop via $scope.$apply(). This way Angular can react to whatever your 3rd-party library did. Angular's $q promise library is a very useful tool for asynchronously resolving the result of whatever your 3rd-party is going to do. Doing something like:

$scope.$apply(function () {
    // do something with 3rd-party lib
});

or

$timeout(function () {
    // do something with 3rd-party lib
}, 0);

$timeout causes Angular to automatically do a $scope.$apply() after the anonymous function is executed (which isn't executed until the next browser event loop).

Learning AngularJS is a roller coaster ride, but ultimately results in increased productivity and flexibility in building large applications.

25 Responses
Add your response

well written stuff ...

over 1 year ago ·

Thanks!

over 1 year ago ·

Flipped through your presentation too, well put together. I read somewhere that modules actually can be pretty useful just for breaking things apart. Could you clarify why you feel they are "good for nothing" currently?

over 1 year ago ·

"Breaking things apart" is not new, it's a part of fundamental programming design (decomposition). You should do that no matter what framework you're using. Angular Modules all configure the same injector, meaning that all Controllers, Directives, Services, etc. all get thrown into one bucket with zero name-spacing capability. Misko talks about the current state of Angular Modules and their (near) uselessness here: https://www.youtube.com/watch?v=ZhfUv0spHCY&t=34m19s.

Rather than using Angular Modules to decompose your app, divide things up by feature (or view) and implement that division in your file/folder structure. In the future when Angular Modules are more useful, you'll be able to lazy-load modules, at which point it will be very useful to put the code for different features (or views) into their own modules.

over 1 year ago ·

Modules should just be called modules if the can be loaded lazy. Use lazy loadable modules today with rappidjs. http://www.rappidjs.com/#/wiki/ModuleLoader

over 1 year ago ·

Some good stuff, I'd definitely recommend organising code by feature early on, partly for your own sanity but partly because on-boarding anyone else on the project will be easier and they'll get an overview of the whole project structure just by looking at files/folders rather than rooting through files/folders.

over 1 year ago ·

The kind of article I was searching for. Seems like I'm not the only only running into theses issues. Thanks for sharing.

over 1 year ago ·

If your building a large application, chances are you need to integrate several such libraries with your app.

You're

over 1 year ago ·

Good catch!

over 1 year ago ·

Angular continuously runs what's called a $digest loop that checks each $watch expression to see if the value being watched has changed.

It doesn't keep running the digest loop continuously. It is triggered by various events. Running continuously would have been a real performance hit.

over 1 year ago ·

You're absolutely right. That's what I meant, but it ended up being a poorly worded sentence.

over 1 year ago ·

Great article - currently implementing the idea a bit differently, instead of creating a lazy object on the module I 'override' the existing ones and for loading on routing I kindof assume file structure and load everything for that route based on a json file that is generated with node (based on the file structure) 'dependencies.json':

"Home" : ["controllers/home/HomeCtrl.js", "directives/home/CustomDtrv.js"],
"About" : ...

I don't believe the overriding will be a problem and will look a lot better to other developers working on my project so they don't need to remember another thing about setting up controllers, directives, etc.As for the separate 'dependencies' json file this was more of a concept not sure of the advantages/disadvantages except that the developer only needs to put the file in the right folder to load it for a route...

Any thoughts?

over 1 year ago ·

It seems like your idea would help with reducing boilerplate. It was tiring listing the Require.js file dependencies AND listing the angular DI dependencies in every file.

over 1 year ago ·

The only problem I found with doing it this way was minification... Unfortunately I had to generate the json then load it then require it.... require optimization had no idea what I was doing.

As a result I just made a grunt task that could load multiple require modules. npm require-wild Eh it works for right now.

Thanks for the article!

over 1 year ago ·

This is a great article, very helpful, I'm working on a large angular project and have come to similar conclusions on folder structure etc. but the real gem for me was the section on 'Lazy-load dependencies on a per-route basis:'. I was not aware of the 'load' feature in the resolve config. This will definitely improve performance on loading needed modules, etc.
Thanks!

over 1 year ago ·

Hi,
Great article. I am using a third party directive (angular google map) which is used inside a lazy loaded controller. How can I make it work together ?

over 1 year ago ·

Thanks! Concise and considerate writeup

over 1 year ago ·

Nice writeup ! What's the best way to serialize application state in the URL when using AngularJS ?

over 1 year ago ·

Having a technical blog where neither the articles nor the comments have any dates on them is an awful practice.

over 1 year ago ·

$location.search(state)

over 1 year ago ·

You save my life, thanks. Keep calm and code!

over 1 year ago ·

Writing unit test cases for controller gives exception "Cannot read property 'controller' of undefined" with this syntax. Can you please suggest how to fix this ?

over 1 year ago ·

good gork... interesting

over 1 year ago ·

Great article. Lots of good points.

One thing you don't mention is breaking the Angular app into multiple smaller apps. I'd be interested in your thoughts on that (i.e., pros and cons).

over 1 year ago ·

Nice write up for someone new to building Angular apps.

over 1 year ago ·