Dynamically Loading Controllers and Views with AngularJS and RequireJS


New to AngularJS? Check out the AngularJS in 60-ish Minutes video to get a jumpstart on using the framework to build Single Page Applications (SPAs).

image



Dynamically Loading Controllers and Views

AngularJS provides a simple way to associate a view with a controller and load everything at runtime using the $routeProvider object. Routing code is typically put in a module’s config() function and looks similar to the following:

$routeProvider
     .when('/customers',
        {
            controller: 'CustomersController',
            templateUrl: '/app/views/customers.html'
        })
    .when('/customerorders/:customerID',
        {
            controller: 'CustomerOrdersController',
            templateUrl: '/app/views/customerOrders.html'
        })
    .when('/orders',
        {
            controller: 'OrdersController',
            templateUrl: '/app/views/orders.html'
        })
    .otherwise({ redirectTo: '/customers' });


While this type of code works great for defining routes it requires controller scripts to be loaded upfront in the main shell page by default. That works fine in some scenarios but what if you have a lot of controller scripts and views in a given application and want to dynamically load them on-the-fly at runtime? One way of dealing with that scenario is to define a resolve property on each route and assign it a function that returns a promise. The function can handle dynamically loading the script containing the target controller and resolve the promise once the load is complete. An example of using the resolve property is shown next:

 

$routeProvider
    .when('/customers',
        {
            templateUrl: '/app/views/customers.html',
            resolve: resolveController('/app/controllers/customersController.js')
        });

 

This approach works well in cases where you don’t want all of your controller scripts loaded upfront, but it still doesn’t feel quite right – at least to me. I personally don’t like having to define two paths especially if you’ll be working with a lot of routes. If you’ve ever worked with a framework that uses convention over configuration then you’ll know that we can clean up this code by coming up with a standard convention for naming views and controllers. Coming up with a convention can help simplify routes and maintenance of the application over time. The approach that I’ll demonstrate in this post uses the following routing code to define the path, view and controller:

$routeProvider
    .when('/customers', route.resolve('Customers'))
    .when('/customerorders/:customerID', route.resolve('CustomerOrders'))
    .when('/orders', route.resolve('Orders'))
    .otherwise({ redirectTo: '/customers' });


Notice that a single value is passed into the route.resolve() function. Behind the scenes the function will automatically create the path to the view and the path to the controller based on some simple conventions and then load the appropriate files dynamically. You can access a sample (work in progress) project at https://github.com/DanWahlin/CustomerManager. Let’s take a look at how it works.

Dynamically Loading Controllers

The following diagram shows the different players involved in simplifying routes and dynamically loading controllers. RequireJS is used to dynamically load controller JavaScript files and make an application’s main module available to the controllers so that they’re registered properly after they’re loaded.

image

 

Here’s how it works:

  1. A file named main.js defines custom scripts that will be loaded using RequireJS. I originally defined 3rd party libraries such as AngularJS in main.js as well but decided there simply wasn’t enough benefit over loading them at the bottom of the initial page using a <script> tag and pulled them out as a result. Only custom scripts are added into main.js in the sample application.
  2. The routeResolver.js script creates an AngularJS provider. It’s loaded by RequireJS and used in app.js within the config() function to define routes and resolve them dynamically at runtime.
  3. The app.js file is loaded by RequireJS. It defines the main AngularJS module used in the application as well as the config for the module. The config section accesses compiler providers (such as $controllerProvider) needed to dynamically load controllers, directives, etc. and also uses the routeResolver to define views and controllers that need to be dynamically loaded (see diagram above). Views and routes are defined using a single name. For example, route.resolve(“Customers”) will cause customers.html to be loaded from the views folder and customersController.js from the controllers folder. 
  4. Each controller is passed the AngularJS module created in app.js by RequireJS. This allows the controllers to dynamically register themselves with AngularJS using a $controllerProvider behind the scenes.
  5. At runtime the routes defined in app.js are dynamically resolved using the routeResolver provider that is injected into the config() function located in app.js.

Let’s break down each of the files one by one now that you’ve seen how they integrate with each other.

 

Defining Scripts in main.js

RequireJS is used to load all of the custom scripts used in the sample application. It’s useful to ensure dependencies are loaded in the proper order and also helps dynamically loaded controllers, directives and other AngularJS objects easily access the application’s module so that they can be properly registered. An example of the main.js file used to configure RequireJS is shown next. All third party scripts are loaded directly in the shell page (index.html) rather than using RequireJS. Services used across multiple controllers are loaded upfront as well as some controllers needed immediately. However, the services could certainly be loaded dynamically if desired with a little more work (that feature isn’t currently implemented).

 

require.config({
    baseUrl: '/app',
    urlArgs: 'v=1.0'
});

require(
    [
        'app',
        'services/routeResolver',
        'services/config',
        'services/customersBreezeService',
        'services/customersService',
        'services/dataService',
        'controllers/navbarController',
        'controllers/orderChildController'
    ],
    function () {
        angular.bootstrap(document, ['customersApp']);
    });


Since the app.js file is loaded dynamically (it defines the main application module) an ng-app=”customersApp” attribute isn’t hard-coded into the shell page that initially loads as with standard AngularJS applications. Instead, it’s added at runtime by calling angular.bootstrap().

 

The routeResolver.js File

The routeResolver.js file handles creating an AngularJS module and provider that can dynamically load views and controllers. AngularJS already comes with built-in support for loading views dynamically and with a little more work controllers can be loaded dynamically as well. Loading controller scripts can be done by assigning the resolve property mentioned earlier to a function that handles loading the controller. What’s unique about routeResolver is that it doesn’t accept hard-coded paths to the target view or controller. Instead, you define a base name such as “Customers” and the resolver will generate the path to the appropriate view and controller based on a standard convention.

Here’s the complete code for the routeResolver provider as well as a routeConfig object used to configure view and controller directories. The code starts by defining a new AngularJS module named routeResolverServices. A provider was chosen (as opposed to a service or factory) because the routeResolver needs to be injected into the config() function of a given application’s module (in app.js for example). The config() function uses the routeResolver to define the routes based on the conventions mentioned earlier.

 

'use strict';

define([], function () {

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

    //Must be a provider since it will be injected into module.config()    
    services.provider('routeResolver', function () {

        this.$get = function () {
            return this;
        };

        this.routeConfig = function () {
            var viewsDirectory = '/app/views/',
                controllersDirectory = '/app/controllers/',

            setBaseDirectories = function (viewsDir, controllersDir) {
                viewsDirectory = viewsDir;
                controllersDirectory = controllersDir;
            },

            getViewsDirectory = function () {
                return viewsDirectory;
            },

            getControllersDirectory = function () {
                return controllersDirectory;
            };

            return {
                setBaseDirectories: setBaseDirectories,
                getControllersDirectory: getControllersDirectory,
                getViewsDirectory: getViewsDirectory
            };
        }();

        this.route = function (routeConfig) {

            var resolve = function (baseName) {
                var routeDef = {};
                routeDef.templateUrl = routeConfig.getViewsDirectory() + baseName + '.html';
                routeDef.controller = baseName + 'Controller';
                routeDef.resolve = {
                    load: ['$q', '$rootScope', function ($q, $rootScope) {
                        var dependencies = [routeConfig.getControllersDirectory() + baseName + 'Controller.js'];
                        return resolveDependencies($q, $rootScope, dependencies);
                    }]
                };

                return routeDef;
            },

            resolveDependencies = function ($q, $rootScope, dependencies) {
                var defer = $q.defer();
                require(dependencies, function () {
                    defer.resolve();
                    $rootScope.$apply()
                });

                return defer.promise;
            };

            return {
                resolve: resolve
            }
        }(this.routeConfig);
    });

});


Looking through the code you can see that a routeConfig object is defined that allows the views and controllers directory to be set using code similar to the following (this code goes in the module.config() function that’ll be shown later):

 

//Change default views and controllers directory using the following:
routeResolverProvider.routeConfig.setBaseDirectories('/app/myViews', '/app/myControllers');


The routeConfig object defaults to the following directories:

Views:              /app/views

Controllers:    /app/controllers

In addition to routeConfig you’ll also find a route object with a resolve() function that handles dynamically loading controller scripts. It handles setting the templateUrl and controller name for a given route. It also takes advantage of AngularJS’s resolve property to dynamically load the target controller script using RequireJS. The resolve() function shown above delegates loading controller scripts to another function named resolveDependencies() which handles getting a deferral, returning a promise, and resolving the deferral once the controller script loads.

 

Defining Routes in app.js

The app.js file defines the application’s main module and also handles configuring routes. It’s wrapped with a define() call to RequireJS to ensure that the routeResolver is loaded and available. The customersApp module defined in app.js requires routeResolverServices.

The config() function has several different providers injected into it including the routeResolver provider (note that the actual name of the provider is routeResolver but that you must add “Provider” on the end of the name for it to work properly when injecting it). The AngularJS providers that are injected into config() are used to dynamically register controllers, directives, filters and more after a given script is dynamically loaded.

Notice that within the config() function an object literal is assigned to app.register (app represents the application’s module). The object literal contains properties that can be used to dynamically register a controller and other AngularJS objects that are downloaded on-the-fly (thanks to Mateusz Bilski for the initial code that started me thinking about this more). You’ll learn more about that process in a moment. Application routes are defined using the routeResolver provider’s route.resolve() function which accepts the base name to use to lookup views and controllers as mentioned earlier based on conventions.

 

'use strict';

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

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

    app.config(['$routeProvider', 'routeResolverProvider', '$controllerProvider', '$compileProvider', '$filterProvider', '$provide',
        function ($routeProvider, routeResolverProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {

            //Change default views and controllers directory using the following:
            //routeResolverProvider.routeConfig.setBaseDirectories('/app/views', '/app/controllers');

            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('/customers', route.resolve('Customers'))
                .when('/customerorders/:customerID', route.resolve('CustomerOrders'))
                .when('/orders', route.resolve('Orders'))
                .otherwise({ redirectTo: '/customers' });

                }]);

            return app;
});


Defining Controllers

Controllers in the application rely on RequireJS to access the object representing the application’s module and then access the register property shown earlier to register a controller script with AngularJS. This type of registration is required since using the standard angular.module(“ModuleName”).controller() code won’t work properly with dynamically loaded controller scripts (at least at the current time). An example of a controller named customersController.js is shown next. Notice that it uses RequireJS’s define() function to get to the app object and then uses it to register the controller. The app.register.controller() function points to AngularJS’s $controllerProvider.register() function behind the scenes as shown earlier with app.js. All of the controllers in the application follow this pattern.

 

'use strict';

define(['app'], function (app) {

    //This controller retrieves data from the customersService and associates it with the $scope
    //The $scope is ultimately bound to the customers view
    app.register.controller('CustomersController', ['$scope', 'config', 'dataService', function ($scope, config, dataService) {

        //Controller code goes here
    }]);
});

 

What about Directives, Filters and other Types?

The current routeResolver implementation only supports dynamically loading controllers. However, all of the necessary plumbing is in place so I may add that functionality at some point in the future.

 


Summary


If you’re new to AngularJS this may seem like a lot of code to perform a simple task like dynamically downloading a controller script. However, once you get the main.js and app.js scripts in place the rest takes care of itself if you follow the pattern outlined above for defining controllers.

The word on the street is that AngularJS will eventually provide a built-in way to dynamically load controllers and other scripts. I’ll certainly welcome that feature when it’s available but until then the routeResolver and RequireJS functionality shown here gets the job done. Although I’ve been through several iterations and variations of this code I expect it’ll change as I use it more and get feedback. Access the sample (work in progress) project that shows all of this in action at https://github.com/DanWahlin/CustomerManager.

comments powered by Disqus

15 Comments

  • Dan, great stuff. Can I get your opinion on the reason you would want to do this, beyond just "I have a lot of controllers"? From a UX perspective, wouldn't the user have a smoother experience if all the scripts are loaded up front rather than waiting fractions of a second or more for each view to display?

  • I have watched your video tutorial on AngularJS fundamentals.
    My question is: Because AngularJS is a client-side framework, will it reduce the workload on the server, in comparison with ASP.NET MVC or Yii-framework?

  • Kevin,

    Thanks! As far as your question, you definitely don't need this unless you have a lot of controllers. Even in the sample application it's overkill because there are only a handful of controllers. Plus, you could argue that controllers could be bundled, minified, etc. and all downloaded upfront.

    But, there are some situations where they may need to be dynamically loaded especially when there are a lot of them in a given application. I'd recommend loading them upfront if you don't think there's any performance issues with doing that (keeping mobile in mind too). Plus, I like the simplified route syntax....better for maintenance. :-)

    Dan

  • rn75:

    Yep - AngularJS should definitely reduce the workload of the server because aside from the initial page it'll simply serve up JSON data and HTML templates. It of course depends on your application, how many Ajax calls are made, etc. though. But in general I'm comfortable saying that the web server's workload should be minimized in many situations since a lot of the burden is pushed to the client.

    Dan

  • What about unloading js dynamically?
    I think loading more and more js in SPA cause more memory usage for browser, it would be better to unload those views and controller if not required in current view.
    What do you think?

  • Gaurav:

    I had someone ask about that earlier and think it's an interesting idea. Makes a lot of sense especially on mobile devices where memory may be more of an issue. Having said that, I'm not exactly sure how well it would work even if a script is removed. Something to look into though.

    Dan

  • Hi Gokhan,

    I've never seen that error unfortunately so I'm not sure what would cause it. You shouldn't need to change any paths at all - something may be wrong with the setup I suspect if you have to change /Script to Scripts since /Scripts will force to look for a Scripts folder right off the root. I'd recommend grabbing the latest build from the URL above. If your path doesn't have the Scripts directory right off the root then /app probably won't be right off the root either (which is should be) which will definitely cause issues. The sample app does require the following (simply double-click the included .sln file to open the project in the editor):

    Visual Studio 2012 Web Express (free version) or higher
    ASP.NET MVC is used for the back end services along with Entity Framework for DB access (included if you have VS 2012 express installed)

    If you're not using the items mentioned above then the back end piece definitely won't work and your paths could be off as well. I'll update the readme file to provide more details there.

    Dan

  • Thanks Dan for the great tutorial. How do you handle html5mode (removing the # from the url) in a web api situation?

  • Dan, excellent article, you've covered the AngularJS's missing piece. For super-large projects this is necessary, I always felt the pinch when moving form Backbone-RequireJS to AngularJS for not being able to load controllers/scripts/templates on demand. Absolutely necessary when building something like a trading platform.

  • Hi Dan,

    good tutorial, but for me it doesn't work. in app.js app.config is called but it is an asynchronous call returning the app immediately. Then require.js loads my controller before app.register has been set, so in my controller app.register is still undefined. Any idea why this is happening for me and not for you?

    Herman

  • Never mind, figured it out :-)

  • The resolve property just saved me. I'm working on an app that has over 20 controller files and was getting some really strange bugs on our development server (but not on my local server). The use of resolve solved the problem (and saved my butt).

  • Thanks Dan for the great tutorial. Very detailed and useful. I'm issuing same problem as Herman above, but can't see where I'm wrong. Herman could you please write how you solved the problem. Thanks in advance.

  • Incredibly helpful and detailed article.

  • Thanks for this. I appreciate the effort and it was what my mind needed to read. Saved me a lot of testing and solved many of the bad documentation puzzles of angular.

Comments have been disabled for this content.