64

I have an existing page into which I need to drop an angular app with controllers that can be loaded dynamically.

Here's a snippet which implements my best guess as to how it should be done based on the API and some related questions I've found:

// Make module Foo
angular.module('Foo', []);
// Bootstrap Foo
var injector = angular.bootstrap($('body'), ['Foo']);
// Make controller Ctrl in module Foo
angular.module('Foo').controller('Ctrl', function() { });
// Load an element that uses controller Ctrl
var ctrl = $('<div ng-controller="Ctrl">').appendTo('body');
// compile the new element
injector.invoke(function($compile, $rootScope) {
    // the linker here throws the exception
    $compile(ctrl)($rootScope);
});

JSFiddle. Note that this is a simplification of the actual chain of events, there are various async calls and user inputs between the lines above.

When I try to run the above code, the linker which is returned by $compile throws: Argument 'Ctrl' is not a function, got undefined. If I understood bootstrap correctly, the injector it returns should know about the Foo module, right?

If instead I make a new injector using angular.injector(['ng', 'Foo']), it seems to work but it creates a new $rootScope which is no longer the same scope as the element where the Foo module was bootstrapped.

Am I using the right functionality to do this or is there something I've missed? I know this isn't doing it the Angular way, but I need to add new components that use Angular to old pages that don't, and I don't know all the components that might be needed when I bootstrap the module.

UPDATE:

I've updated the fiddle to show that I need to be able to add multiple controllers to the page at undetermined points in time.

isherwood
  • 58,414
  • 16
  • 114
  • 157
Jussi Kosunen
  • 8,277
  • 3
  • 26
  • 34
  • Why don't you just declare all your controllers the normal Angular way up front and then do your insert-compile on page load based on what page you're on? – boxed Mar 06 '13 at 15:57
  • 1
    There are a lot of possible controllers, and I'm only going to use one or two on that specific page, however I don't know which one or two until they're selected by the user. – Jussi Kosunen Mar 07 '13 at 06:51
  • Put all those controllers into one big (preferably minified) .js file and be sure that file is properly cached by the browser. You can't possibly have so many controllers that this is a problem. – boxed Mar 07 '13 at 08:20
  • 3
    It is a _large_ system, 10k+ files at the moment for a single version. – Jussi Kosunen Mar 07 '13 at 09:47
  • To elaborate on the previous comment, in practice the number of controllers will eventually be in the high tens to low hundreds, depending on when the next framework du jour shows up. – Jussi Kosunen Mar 07 '13 at 10:55
  • Unless those controllers are hundreds of lines (which I really hope they're not!), just put them in one file. Minified that'll probably be insignificant compared to stuff like images etc anyway. – boxed Mar 08 '13 at 11:11
  • Unfortunately most of them will be. It's a [healthcare/hospital information system](http://en.wikipedia.org/wiki/Hospital_information_system) if you're curious about the size. – Jussi Kosunen Mar 08 '13 at 12:30
  • 1
    @boxed And throw modularity out of the window while you are at it. – Domi Jun 08 '14 at 08:32

8 Answers8

71

I've found a possible solution where I don't need to know about the controller before bootstrapping:

// Make module Foo and store $controllerProvider in a global
var controllerProvider = null;
angular.module('Foo', [], function($controllerProvider) {
    controllerProvider = $controllerProvider;
});
// Bootstrap Foo
angular.bootstrap($('body'), ['Foo']);

// .. time passes ..

// Load javascript file with Ctrl controller
angular.module('Foo').controller('Ctrl', function($scope, $rootScope) {
    $scope.msg = "It works! rootScope is " + $rootScope.$id +
        ", should be " + $('body').scope().$id;
});
// Load html file with content that uses Ctrl controller
$('<div id="ctrl" ng-controller="Ctrl" ng-bind="msg">').appendTo('body');

// Register Ctrl controller manually
// If you can reference the controller function directly, just run:
// $controllerProvider.register(controllerName, controllerFunction);
// Note: I haven't found a way to get $controllerProvider at this stage
//    so I keep a reference from when I ran my module config
function registerController(moduleName, controllerName) {
    // Here I cannot get the controller function directly so I
    // need to loop through the module's _invokeQueue to get it
    var queue = angular.module(moduleName)._invokeQueue;
    for(var i=0;i<queue.length;i++) {
        var call = queue[i];
        if(call[0] == "$controllerProvider" &&
           call[1] == "register" &&
           call[2][0] == controllerName) {
            controllerProvider.register(controllerName, call[2][1]);
        }
    }
}
registerController("Foo", "Ctrl");
// compile the new element
$('body').injector().invoke(function($compile, $rootScope) {
    $compile($('#ctrl'))($rootScope);
    $rootScope.$apply();
});

Fiddle. Only problem is that you need to store the $controllerProvider and use it in a place where it really shouldn't be used (after the bootstrap). Also there doesn't seem to be an easy way to get at a function used to define a controller until it is registered, so I need to loop through the module's _invokeQueue, which is undocumented.

UPDATE: To register directives and services, instead of $controllerProvider.register simply use $compileProvider.directive and $provide.factory respectively. Again, you'll need to save references to these in your initial module config.

UDPATE 2: Here's a fiddle which automatically registers all controllers/directives/services loaded without having to specify them individually.

Jussi Kosunen
  • 8,277
  • 3
  • 26
  • 34
  • 1
    The fiddle in UPDATE 2 was perfect, just what i needed. – Tyson Sep 17 '13 at 07:04
  • Hmm...it gives me an error when trying to call the `.injector()` function, calling it undefined. I'm using angular1.2.23. Has it been deprecated in the newer versions? – Aarmora Jan 08 '15 at 02:02
  • it gives me error too ! [Error: [ng:areq] http://errors.angularjs.org/1.2.26/ng/areq?p0=CTRL&p1=not%20a%20function%2C%20got%20undefined] – Ali Adlavaran Apr 21 '15 at 10:21
  • Works for me in 1.3.15. If you can reproduce it in jsfiddle/plunker then I could have a look at that, otherwise it's a bit difficult to diagnose. – Jussi Kosunen Apr 21 '15 at 12:51
  • and what about filters? – devmao Apr 28 '15 at 15:00
  • UPDATE about filters: Just adding also the $filterProvider https://jsfiddle.net/MzseV/72/ – devmao Apr 28 '15 at 15:24
17

bootstrap() will call the AngularJS compiler for you, just like ng-app.

// Make module Foo
angular.module('Foo', []);
// Make controller Ctrl in module Foo
angular.module('Foo').controller('Ctrl', function($scope) { 
    $scope.name = 'DeathCarrot' });
// Load an element that uses controller Ctrl
$('<div ng-controller="Ctrl">{{name}}</div>').appendTo('body');
// Bootstrap with Foo
angular.bootstrap($('body'), ['Foo']);

Fiddle.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • The problem is I need to be able to add controllers as they're needed, it wouldn't be feasible to add them all at the start since there's a lot of them. I've tried calling bootstrap multiple times but that also breaks existing scopes (among other things). – Jussi Kosunen Mar 07 '13 at 06:49
  • 1
    @DeathCarrot, I don't know how to dynamically add additional controllers. This might help: https://github.com/matys84pl/angularjs-requirejs-lazy-controllers – Mark Rajcok Mar 07 '13 at 21:09
  • Looks like you still need to know about the available controllers before you bootstrap (routes.js). Still, listing them out is better than loading them. Thanks, I'll probably end up doing it something like that unless I can find a way to do it completely dynamically. – Jussi Kosunen Mar 08 '13 at 07:22
  • @Mark Rajcok how to pass `DeathCarrot` into controller instead of hard-coded value inside the controller? – Dis Shishkov Dec 11 '13 at 13:15
7

I would suggest to take a look at ocLazyLoad library, which registers modules (or controllers, services etc on existing module) at run time and also loads them using requireJs or other such library.

Marius Balčytis
  • 2,601
  • 20
  • 22
  • Oh my Bajezus... this is exactly what everyone in this question is looking for... +1 for attention here... ocLazyLoad is all that and a bag of microchips. – Nick Steele Dec 03 '15 at 18:32
  • I use this one too :D sometimes you want some libraries to be lazy loaded in order. As they depend on the other libraries (that need to be loaded first). So I created a synchronous loader to do this type of thing. let me know if someone want me to write this loader. as it may not be relevant to this thread. – r4ccoon May 20 '16 at 20:11
2

I also needed to add multiple views and bind them to controllers at runtime from a javascript function outside the angularJs context, so here's what I came up with :

<div id="mController" ng-controller="mainController">
</div>

<div id="ee">
  2nd controller's view should be rendred here
</div>

now calling setCnt() function will inject and compile the html, and it will be linked to the 2nd controller:

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

function setCnt() {
  // Injecting the view's html
  var e1 = angular.element(document.getElementById("ee"));
  e1.html('<div ng-controller="ctl2">my name: {{name}}</div>');

  // Compile controller 2 html
  var mController = angular.element(document.getElementById("mController"));
  mController.scope().activateView(e1);
}

app.controller("mainController", function($scope, $compile) {
  $scope.name = "this is name 1";

  $scope.activateView = function(ele) {
    $compile(ele.contents())($scope);
    $scope.$apply();
  };
});

app.controller("ctl2", function($scope) {
  $scope.name = "this is name 2";
});

here's an example to test this : https://refork.codicode.com/x4bc

hope this helps.

Chtioui Malek
  • 11,197
  • 1
  • 72
  • 69
  • i used your approach to add controller dynamically. I want to inject javascript object to this controller. Can you please help me. I have asked question on stackoverflow http://stackoverflow.com/questions/36597885/how-to-inject-javascript-object-into-dynamically-added-angular-controller – Kaustubh Khare Apr 14 '16 at 06:20
  • yes you can use RequireJS to load your javascript and get your controller scope like i've done in my example `mController.scope()` – Chtioui Malek Apr 14 '16 at 10:26
  • can u please explain me in detail using my question. I don't know much about RequireJs. – Kaustubh Khare Apr 14 '16 at 10:41
1

I have just improved the function written by Jussi-Kosunen so that all stuff can be done with one single call.

function registerController(moduleName, controllerName, template, container) {
    // Load html file with content that uses Ctrl controller
    $(template).appendTo(container);
    // Here I cannot get the controller function directly so I
    // need to loop through the module's _invokeQueue to get it
    var queue = angular.module(moduleName)._invokeQueue;
    for(var i=0;i<queue.length;i++) {
        var call = queue[i];
        if(call[0] == "$controllerProvider" &&
            call[1] == "register" &&
            call[2][0] == controllerName) {
                controllerProvider.register(controllerName, call[2][1]);
            }
        }

        angular.injector(['ng', 'Foo']).invoke(function($compile, $rootScope) {
            $compile($('#ctrl'+controllerName))($rootScope);
            $rootScope.$apply();
        });
}

This way you could load your template from anywhere and instanciate controllers programmatically, even nested.

Here is a working example loading a controller inside another one: http://plnkr.co/edit/x3G38bi7iqtXKSDE09pN

David Vartanian
  • 460
  • 4
  • 12
  • Actually after learning more Angular, I think all this is conceptually (or technically ) wrong. Using directives you can do the same but so much easier. – David Vartanian Mar 05 '15 at 01:20
1

why not use config and ui-router?

it is loaded at runtime and you have no need to show your controllers in html code

for example something like the following

var config = {

   config: function(){
        mainApp.config(function ($stateProvider, $urlRouterProvider){
            $urlRouterProvider.otherwise("/");
            $stateProvider

            .state('index',{
                views:{
                    'main':{
                        controller: 'PublicController',
                        templateUrl: 'templates/public-index.html'
                    }
                }
            })
            .state('public',{
                url: '/',
                parent: 'index',
                views: {
                    'logo' : {templateUrl:'modules/header/views/logo.html'},
                    'title':{
                        controller: 'HeaderController',
                        templateUrl: 'modules/header/views/title.html'
                    },
                    'topmenu': {
                        controller: 'TopMenuController',
                        templateUrl: 'modules/header/views/topmenu.html'
                    },
                    'apartments': {
                        controller: 'FreeAptController',
                        templateUrl:'modules/free_apt/views/apartments.html'
                    },
                    'appointments': {
                        controller: 'AppointmentsController',
              templateUrl:'modules/appointments/views/frm_appointments.html'
                    },
                }
            })
            .state('inside',{
                views:{
                    'main':{
                        controller: 'InsideController',
                        templateUrl: 'templates/inside-index.html'
                    },
                },
                resolve: {
                    factory:checkRouting
                }
            })
            .state('logged', {
                url:'/inside',
                parent: 'inside',
                views:{        
                    'logo': {templateUrl: 'modules/inside/views/logo.html'},
                    'title':{templateUrl:'modules/inside/views/title.html'},
                    'topmenu': {
                       // controller: 'InsideTopMenuController',
                        templateUrl: 'modules/inside/views/topmenu.html'
                    },
                    'messages': {
                        controller: 'MessagesController',
                        templateUrl: 'modules/inside/modules/messages/views/initial-view-messages.html'
                    },
                    'requests': {
                        //controller: 'RequestsController',
                        //templateUrl: 'modules/inside/modules/requests/views/initial-view-requests.html'
                    },

                }

            })

    });
},

};
  • Because it doesn't answer the question. He is asking how to **dynamically** (lazy-load) the controllers. Not if they are visible from HTML or elsewhere. – Jeach Mar 04 '18 at 20:04
1

This is what I did, 2 parts really, using ng-controller with its scope defined function and then $controller service to create the dynamic controller :-

First, the HTML - we need a Static Controller which will instantiate a dynamic controller ..

<div ng-controller='staticCtrl'>
  <div ng-controller='dynamicCtrl'>
    {{ dynamicStuff }}
  </div>
</div>

The static controller 'staticCtrl' defines a scope member called 'dynamicCtrl' which is called to create the dynamic controller. ng-controller will take either a predefined controller by name or looks at current scope for function of same name ..

.controller('staticCtrl', ['$scope', '$controller', function($scope, $controller) {
  $scope.dynamicCtrl = function() {
    var fn = eval('(function ($scope, $rootScope) { alert("I am dynamic, my $scope.$id = " + $scope.$id + ", $rootScope.$id = " + $rootScope.$id); })');
    return $controller(fn, { $scope: $scope.$new() }).constructor;
  }
}])

We use eval() to take a string (our dynamic code which can come from anywhere) and then the $controller service which will take either a predefined controller name (normal case) or a function constructor followed by constructor parameters (we pass in a new scope) - Angular will inject (like any controller) into the function, we are requesting just $scope and $rootScope above.

steve
  • 3,230
  • 1
  • 19
  • 14
0
'use strict';

var mainApp = angular.module('mainApp', [
    'ui.router', 
    'ui.bootstrap', 
    'ui.grid',
    'ui.grid.edit',
    'ngAnimate',
    'headerModule', 
    'galleryModule', 
    'appointmentsModule', 
 ]);


(function(){

    var App = {
        setControllers:   mainApp.controller(controllers),
        config:   config.config(),
        factories: {
            authFactory: factories.auth(),
            signupFactory: factories.signup(),
            someRequestFactory: factories.saveSomeRequest(),
        },
        controllers: {
            LoginController: controllers.userLogin(),
            SignupController: controllers.signup(),
            WhateverController: controllers.doWhatever(),
        },
        directives: {
            signup: directives.signup(), // add new user
            openLogin: directives.openLogin(), // opens login window
            closeModal: directives.modalClose(), // close modal window
            ngFileSelect: directives.fileSelect(),
            ngFileDropAvailable: directives.fileDropAvailable(),
            ngFileDrop: directives.fileDrop()
        },
        services: {
           $upload: services.uploadFiles(),
        }
    };
})();

The above code is only an example.

This way you don't need to put ng-controller="someController" anywhere on a page — you only declare <body ng-app="mainApp">

Same structure can be used for each module or modules inside modules

Community
  • 1
  • 1