Carregando um controlador AngularJS dinamicamente

Eu tenho uma página existente na qual eu preciso soltar um aplicativo angular com controladores que podem ser carregados dinamicamente.

Aqui está um snippet que implementa meu melhor palpite sobre como isso deve ser feito com base na API e em algumas perguntas relacionadas que encontrei:

// 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 = $('
').appendTo('body'); // compile the new element injector.invoke(function($compile, $rootScope) { // the linker here throws the exception $compile(ctrl)($rootScope); });

JSFiddle . Observe que isso é uma simplificação da cadeia de events real, existem várias chamadas assíncronas e inputs do usuário entre as linhas acima.

Quando eu tento executar o código acima, o linkador que é retornado pelo $ compile lança: O Argument 'Ctrl' is not a function, got undefined . Se eu entendi o bootstrap corretamente, o injetor que ele retorna deve saber sobre o módulo Foo , certo?

Se ao invés disso eu fizer um novo injetor usando angular.injector(['ng', 'Foo']) , ele parece funcionar, mas cria um novo $rootScope que não é mais o mesmo escopo do elemento onde o módulo Foo foi bootstrapped .

Estou usando a funcionalidade certa para fazer isso ou há algo que perdi? Eu sei que isso não está fazendo da maneira Angular, mas eu preciso adicionar novos componentes que usam Angular para páginas antigas que não, e eu não sei todos os componentes que podem ser necessários quando eu inicializo o módulo.

ATUALIZAR:

Eu atualizei o violino para mostrar que eu preciso ser capaz de adicionar vários controladores para a página em pontos indeterminados no tempo.

   

Eu encontrei uma solução possível, onde eu não preciso saber sobre o controlador antes do 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 $('
').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

Violino . O único problema é que você precisa armazenar o $controllerProvider e usá-lo em um local onde ele realmente não deveria ser usado (após o bootstrap). Também não parece haver uma maneira fácil de obter uma function usada para definir um controlador até que ele seja registrado, portanto, preciso percorrer o _invokeQueue do módulo, que não está documentado.

ATUALIZAÇÃO: Para registrar diretivas e serviços, em vez de $controllerProvider.register simplesmente use $compileProvider.directive e $provide.factory respectivamente. Novamente, você precisará salvar referências a elas na sua configuração inicial do módulo.

UDPATE 2: Aqui está um violino que registra automaticamente todos os controladores / diretivas / serviços carregados sem precisar especificá-los individualmente.

O bootstrap () irá chamar o compilador AngularJS para você, assim como o 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 $('
{{name}}
').appendTo('body'); // Bootstrap with Foo angular.bootstrap($('body'), ['Foo']);

Violino .

Eu sugeriria dar uma olhada na biblioteca ocLazyLoad , que registra módulos (ou controladores, serviços, etc., no módulo existente) em tempo de execução e também os carrega usando requireJs ou outra biblioteca desse tipo.

Eu também precisei adicionar várias visualizações e vinculá-las aos controladores em tempo de execução de uma function javascript fora do contexto angularJs, então aqui está o que eu criei:

 
2nd controller's view should be rendred here

agora chamar a function setCnt () irá injetar e compilar o html, e ele será vinculado ao segundo controlador:

 var app = angular.module('app', []); function setCnt() { // Injecting the view's html var e1 = angular.element(document.getElementById("ee")); e1.html('
my name: {{name}}
'); // 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"; });

aqui está um exemplo para testar isso: http://refork.com/x4bc

espero que isto ajude.

Acabei de melhorar a function escrita por Jussi-Kosunen para que todas as coisas possam ser feitas com uma única chamada.

 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 

Dessa forma, você pode carregar seu modelo de qualquer lugar e instanciar controladores programaticamente, até mesmo nesteds.

Aqui está um exemplo de trabalho carregando um controlador dentro de outro: http://plnkr.co/edit/x3G38bi7iqtXKSDE09pN

por que não usar config e roteador-ui?

ele é carregado em tempo de execução e você não precisa mostrar seus controladores em código html

por exemplo, algo parecido com o seguinte

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

O código acima é apenas um exemplo.

Desta forma você não precisa colocar ng-controller="someController" em qualquer lugar de uma página – você apenas declara

Mesma estrutura pode ser usada para cada módulo ou módulos dentro dos módulos

Isso é o que eu fiz, duas partes realmente, usando o ng-controller com sua function definida de escopo e, em seguida, o $ controller para criar o controlador dynamic:

Primeiro, o HTML – precisamos de um controlador estático que instanciará um controlador dynamic.

 
{{ dynamicStuff }}

O controlador estático ‘staticCtrl’ define um membro de escopo chamado ‘dynamicCtrl’ que é chamado para criar o controlador dynamic. O ng-controller terá um controlador predefinido pelo nome ou examinará o escopo atual para a function do mesmo nome.

 .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; } }]) 

Usamos eval () para pegar uma string (nosso código dynamic que pode vir de qualquer lugar) e então o $ controller que terá um nome de controller pré-definido (caso normal) ou um construtor de function seguido por parâmetros de construtor (passamos em um novo escopo) – Angular irá injetar (como qualquer controlador) na function, estamos solicitando apenas $ escopo e $ rootScope acima.