angular-ui-router with requirejs, lazy loading of controller

Solution 1:

I created working plunker here.

Let's have this index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>my lazy</title>    
  </head>

  <body ng-app="app">
    
      <a href="#/home">#/home</a>     // we have three states - 'home' is NOT lazy
      <a href="#/">#/</a>  - index    // 'index' is lazy, with two views
      <a href="#/other">#/other</a>   // 'other' is lazy with unnamed view
    
    <div data-ui-view="topMenu"></div>        
    <div data-ui-view=""></div>
    
    <script src="angular.js"></script>           // standard angular
    <script src="angular-ui-router.js"></script> // and ui-router scritps

    <script src="script.js"></script>            // our application

    <script data-main="main.js"                  // lazy dependencies
        src="require.js"></script>
     
  </body>    
</html>

Let's observe the main.js - the RequireJS config:

require.config({

    //baseUrl: "js/scripts",
    baseUrl: "",

    // alias libraries paths
    paths: { 
      
        // here we define path to NAMES
        // to make controllers and their lazy-file-names independent
        
        "TopMenuCtrl": "Controller_TopMenu",
        "ContentCtrl": "Controller_Content",
        "OtherCtrl"  : "Controller_Other",
    },

    deps: ['app']
});

In fact, we only create aliases (paths) for our ControllerNames - and their Controller_Scripts.js files. That's it. Also, we return to require the app, but we will in our case use different feature later - to register lazily loaded controllers.

what does the deps: ['app'] mean? Firstly, we need to provide file app.js (the 'app' means find app.js) :

define([], function() {

  var app = angular.module('app');
  return app; 
})

this returned value is the one we can ask for in every async loaded file

define(['app'], function (app) {
    // here we would have access to the module("app")
});

How will we load controllers lazily? As already proven here for ngRoute

angularAMD v0.2.1

angularAMD is an utility that facilitates the use of RequireJS in AngularJS applications supporting on-demand loading of 3rd party modules such as angular-ui.

We will ask angular for a reference to $controllerProvider - and use it later, to register controllers.

This is the first part of our script.js:

// I. the application
var app = angular.module('app', [
  "ui.router"
]);


// II. cached $controllerProvider
var app_cached_providers = {};

app.config(['$controllerProvider',
  function(controllerProvider) {
    app_cached_providers.$controllerProvider = controllerProvider;
  }
]);

As we can see, we just created the application 'app' and also, created holder app_cached_providers (following the angularAMD style). In the config phase, we ask angular for $controllerProvider and keep reference for it.

Now let's continue in script.js:

// III. inline dependency expression
app.config(['$stateProvider', '$urlRouterProvider',
  function($stateProvider, $urlRouterProvider) {

    $urlRouterProvider
      .otherwise("/home");

    $stateProvider
      .state("home", {
        url: "/home",
        template: "<div>this is home - not lazily loaded</div>"
      });

    $stateProvider
      .state("other", {
        url: "/other",
        template: "<div>The message from ctrl: {{message}}</div>",
        controller: "OtherCtrl",
        resolve: {
          loadOtherCtrl: ["$q", function($q) { 
            var deferred = $q.defer();
            require(["OtherCtrl"], function() { deferred.resolve(); });
            return deferred.promise;
          }],
        },
      });

  }
]);

This part above shows two states declaration. One of them - 'home' is standard none lazy one. It's controller is implicit, but standard could be used.

The second is state named "other" which does target unnamed view ui-view="". And here we can firstly see, the lazy load. Inside of the resolve (see:)

Resolve

You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.

If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.

With that in our suite, we know, that the controller (by its name) will be searched in angular repository once the resolve is finished:

// this controller name will be searched - only once the resolve is finished
controller: "OtherCtrl",
// let's ask RequireJS
resolve: {
  loadOtherCtrl: ["$q", function($q) { 
    // wee need $q to wait
    var deferred = $q.defer();
    // and make it resolved once require will load the file
    require(["OtherCtrl"], function() { deferred.resolve(); });
    return deferred.promise;
  }],
},

Good, now, as mentioned above, the main contains this alias def

// alias libraries paths
paths: {       
    ...
    "OtherCtrl"  : "Controller_Other",

And that means, that the file "Controller_Other.js" will be searched and loaded. This is its content which does the magic. The most important here is use of previously cached reference to $controllerProvider

// content of the "Controller_Other.js"

define(['app'], function (app) {
    // the Default Controller
    // is added into the 'app' module
    // lazily, and only once
    app_cached_providers
      .$controllerProvider
      .register('OtherCtrl', function ($scope) {
        $scope.message = "OtherCtrl";
    });        
});

the trick is not to use app.controller() but

$controllerProvider.Register

The $controller service is used by Angular to create new controllers. This provider allows controller registration via the register() method.

Finally there is another state definition, with more narrowed resolve... a try to make it more readable:

// IV ... build the object with helper functions
//        then assign to state provider    
var loadController = function(controllerName) {
  return ["$q", function($q) {
      var deferred = $q.defer();
      require([controllerName], function() {deferred.resolve(); });
      return deferred.promise;
  }];
}    

app.config(['$stateProvider', '$urlRouterProvider',
  function($stateProvider, $urlRouterProvider) {

    var index = {
        url: "/",
        views: {
          "topMenu": {
            template: "<div>The message from ctrl: {{message}}</div>",
            controller: "TopMenuCtrl",
          },
          "": {
            template: "<div>The message from ctrl: {{message}}</div>",
            controller: "ContentCtrl",
          },
        },
        resolve : { },
    };        
    index.resolve.loadTopMenuCtrl = loadController("TopMenuCtrl");
    index.resolve.loadContentCtrl = loadController("ContentCtrl");
    
    $stateProvider
      .state("index", index);          
}]);

Above we can see, that we resolve two controllers for both/all named views of that state

That's it. Each controller defined here

paths: { 
    "TopMenuCtrl": "Controller_TopMenu",
    "ContentCtrl": "Controller_Content",
    "OtherCtrl"  : "Controller_Other",
    ...
},

will be loaded via resolve and $controllerProvider - via RequireJS - lazily. Check that all here

Similar Q & A: AngularAMD + ui-router + dynamic controller name?