64 votes

AngularJS : Chargement dynamique d'un contrôleur

J'ai une page existante dans laquelle je dois déposer une application angulaire avec des contrôleurs qui peuvent être chargés dynamiquement.

Voici un extrait qui met en œuvre ma meilleure idée de la façon dont cela devrait être fait en fonction de l'API et de certaines questions connexes que j'ai trouvées :

// 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 . Notez qu'il s'agit d'une simplification de la chaîne d'événements réelle, il y a plusieurs appels asynchrones et entrées utilisateur entre les lignes ci-dessus.

Lorsque j'essaie d'exécuter le code ci-dessus, le linker qui est retourné par $compile jette : Argument 'Ctrl' is not a function, got undefined . Si j'ai bien compris le fonctionnement de bootstrap, l'injecteur qu'il renvoie doit connaître l'existence de l'objet de l'utilisateur. Foo module, n'est-ce pas ?

Si à la place, je crée un nouvel injecteur en utilisant angular.injector(['ng', 'Foo']) cela semble fonctionner, mais cela crée une nouvelle $rootScope qui n'a plus le même champ d'application que l'élément où se trouve la fonction Foo a été amorcé.

Est-ce que j'utilise la bonne fonctionnalité pour faire cela ou y a-t-il quelque chose que j'ai manqué ? Je sais que ce n'est pas la façon de faire d'Angular, mais j'ai besoin d'ajouter de nouveaux composants qui utilisent Angular à d'anciennes pages qui ne le font pas, et je ne connais pas tous les composants qui pourraient être nécessaires lorsque je démarre le module.

UPDATE :

J'ai mis à jour le violon pour montrer que je dois être capable d'ajouter plusieurs contrôleurs à la page à des moments indéterminés.

71voto

Jussi Kosunen Points 3253

J'ai trouvé une solution possible où je n'ai pas besoin de connaître le contrôleur avant de démarrer :

// 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();
});

Violon . Le seul problème est que vous devez stocker le $controllerProvider et l'utiliser à un endroit où il ne devrait vraiment pas être utilisé (après le bootstrap). De plus, il ne semble pas y avoir de moyen facile d'accéder à une fonction utilisée pour définir un contrôleur jusqu'à ce qu'il soit enregistré. _invokeQueue qui n'est pas documenté.

UPDATE : Pour enregistrer les directives et les services, au lieu de $controllerProvider.register utilisez simplement $compileProvider.directive y $provide.factory respectivement. Encore une fois, vous devrez enregistrer les références à ces éléments dans la configuration initiale du module.

UDPATE 2 : Voici un violon qui enregistre automatiquement tous les contrôleurs/directives/services chargés sans avoir à les spécifier individuellement.

17voto

Mark Rajcok Points 85912

Bootstrap() appellera le compilateur AngularJS pour vous, tout comme 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']);

Violon .

7voto

barius Points 1484

Je vous suggère de jeter un coup d'œil à Bibliothèque ocLazyLoad qui enregistre les modules (ou les contrôleurs, les services, etc. sur un module existant) au moment de l'exécution et les charge également en utilisant requireJs ou une autre bibliothèque de ce type.

1voto

Je viens d'améliorer la fonction écrite par Jussi-Kosunen afin que tout puisse être fait en un seul appel.

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();
        });
}

De cette façon, vous pouvez charger votre modèle à partir de n'importe où et instancier les contrôleurs de façon programmatique, même imbriqués.

Voici un exemple fonctionnel de chargement d'un contrôleur à l'intérieur d'un autre : http://plnkr.co/edit/x3G38bi7iqtXKSDE09pN

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X