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?
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>