Search code examples
javascriptangularjslazy-loadingangularjs-ng-include

Using controller defined in included template


In the code snippet I try to use a controller FooCtrl which is defined in the included template app/foo.html by using the directive common.script.

angular.module('common.script', []).directive('script', function() {
  return {
    restrict: 'E',
    scope: false,
    compile: function(element, attributes) {
      if (attributes.script === 'lazy') {
        var code = element.text()
        new Function(code)()
      }
    }
  }
})
angular.module('app.templates', ['app/foo.html'])
angular.module("app/foo.html", []).run(function($templateCache) {
  $templateCache.put("app/foo.html",
    "<script data-script=\"lazy\">\n" +
    "   console.log('Before FooCtrl')\n" +
    "	angular.module('app').controller('FooCtrl', function($scope) {\n" +
    "		console.log('FooCtrl')\n" +
    "	})\n" +
    "<\/script>\n" +
    "<div data-ng-controller=\"FooCtrl\">app\/foo.html\n" +
    "<\/div>"
  )
})
angular.module('app', ['common.script', 'app.templates']).controller('ApplicationCtrl', function($scope) {
  console.log('ApplicationCtrl')
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="ApplicationCtrl">
  <div data-ng-include="'app/foo.html'"></div>
</div>

But instead of the expected output FooCtrl in the console AngularJS throws:

Error: [ng:areq] Argument 'FooCtrl' is not a function [...]

I don't understand why! The code in the template is executed before the exception is thrown, thus the controller should be defined. How could I fix that?


Solution

  • The real problem here is lazy loading of resources! There are tons of material and related posts about this topic.

    The solution here could be an extended common.script directive:

    'use strict'
    
    angular.module('common.script', [])
    
    .config(function($animateProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
      angular.module('common.script').lazy = {
        $animateProvider: $animateProvider,
        $controllerProvider: $controllerProvider,
        $compileProvider: $compileProvider,
        $filterProvider: $filterProvider,
        $provide: $provide
      }
    })
    
    .directive('script', function() {
      return {
        restrict: 'E',
        scope: {
          modules: '=script'
        },
        link: function(scope, element) {
          var offsets = {}, code = element.text()
    
          function cache(module) {
            offsets[module] = angular.module(module)._invokeQueue.length
          }
    
          function run(offset, queue) {
            var i, n
            for (i = offset, n = queue.length; i < n; i++) {
              var args = queue[i], provider = angular.module('common.script').lazy[args[0]]
    
              provider[args[1]].apply(provider, args[2])
            }
          }
    
          if (angular.isString(scope.modules)) {
            cache(scope.modules)
          } else if (angular.isArray(scope.modules)) {
            scope.modules.forEach(function(module) {
              cache(module)
            })
          }
    
          /*jshint -W054 */
          new Function(code)()
    
          Object.keys(offsets).forEach(function(module) {
            if (angular.module(module)._invokeQueue.length > offsets[module]) {
              run(offsets[module], angular.module(module)._invokeQueue)
            }
          })
        }
      }
    })
    

    The only downside of this solution is that you have to specify the module(s) you want to extend in a script tag:

    <script data-script="'app'">
      angular.module('app').controller('FooCtrl', function($scope) {
        console.log('Works!')
      })
    </script>