Loading an AngularJS controller dynamically
Asked Answered
A

8

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.

Ashti answered 6/3, 2013 at 14:57 Comment(8)
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?Fault
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.Ashti
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.Fault
It is a large system, 10k+ files at the moment for a single version.Ashti
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.Ashti
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.Fault
Unfortunately most of them will be. It's a healthcare/hospital information system if you're curious about the size.Ashti
@Fault And throw modularity out of the window while you are at it.Hypomania
A
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.

Ashti answered 8/3, 2013 at 11:2 Comment(6)
The fiddle in UPDATE 2 was perfect, just what i needed.Gustavo
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?Applecart
it gives me error too ! [Error: [ng:areq] errors.angularjs.org/1.2.26/ng/…Vauban
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.Ashti
and what about filters?Farinaceous
UPDATE about filters: Just adding also the $filterProvider jsfiddle.net/MzseV/72Farinaceous
D
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.

Ditheism answered 6/3, 2013 at 16:16 Comment(4)
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).Ashti
@DeathCarrot, I don't know how to dynamically add additional controllers. This might help: github.com/matys84pl/angularjs-requirejs-lazy-controllersDitheism
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.Ashti
@Mark Rajcok how to pass DeathCarrot into controller instead of hard-coded value inside the controller?Latonya
E
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.

Epaulet answered 14/10, 2014 at 19:9 Comment(2)
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.Queenqueena
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.Architectonic
C
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.

Conover answered 25/3, 2015 at 22:49 Comment(3)
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 #36598385Conni
yes you can use RequireJS to load your javascript and get your controller scope like i've done in my example mController.scope()Conover
can u please explain me in detail using my question. I don't know much about RequireJs.Conni
B
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

Byway answered 3/6, 2014 at 1:11 Comment(1)
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.Byway
C
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'
                    },

                }

            })

    });
},

};
Clavus answered 11/4, 2015 at 14:50 Comment(1)
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.Prognathous
D
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.

Deflate answered 1/2, 2017 at 9:23 Comment(0)
C
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

Clavus answered 6/4, 2015 at 20:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.