Динамическая загрузка контроллера AngularJS

У меня есть существующая страница, в которую мне нужно отказаться от приложения angular с контроллерами, которые могут быть загружены динамически.

Вот фрагмент, в котором я догадываюсь, как это сделать на основе API и некоторых связанных с ним вопросов, которые я нашел:

// 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. Обратите внимание, что это упрощение фактической цепочки событий, существуют различные асинхронные вызовы и пользовательские входы между строками выше.

Когда я пытаюсь запустить вышеуказанный код, компоновщик, который возвращается $compile throws: Argument 'Ctrl' is not a function, got undefined. Если я правильно понял загрузчик, инжектор, который он возвращает, должен знать о модуле Foo, правильно?

Если вместо этого я создаю новый инжектор с помощью angular.injector(['ng', 'Foo']), он, похоже, работает, но он создает новый $rootScope, который больше не является тем же областью, что и элемент, в котором был загружен модуль Foo.

Я использую правильные функции для этого или есть что-то, что я пропустил? Я знаю, что это не делает способ angular, но мне нужно добавить новые компоненты, которые используют angular, на старые страницы, которых нет, и я не знаю всех компонентов, которые могут понадобиться, когда я загружаю модуль.

UPDATE:

Я обновил fiddle, чтобы показать, что мне нужно добавить несколько контроллеров на страницу в неопределенные моменты времени.

Ответ 1

Я нашел возможное решение, где мне не нужно знать о контроллере перед загрузкой:

// 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 _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. Единственная проблема заключается в том, что вам нужно сохранить $controllerProvider и использовать его в том месте, где оно действительно не должно использоваться (после загрузки). Также нет простого способа получить функцию, используемую для определения контроллера, пока он не будет зарегистрирован, поэтому мне нужно пройти через модуль _invokeQueue, который недокументирован.

UPDATE: Чтобы зарегистрировать директивы и службы вместо $controllerProvider.register, просто используйте $compileProvider.directive и $provide.factory соответственно. Опять же, вам нужно сохранить ссылки на них в первоначальной конфигурации модуля.

UDPATE 2: Здесь сценарий, который автоматически регистрирует все контроллеры/директивы/службы, загруженные без необходимости их указывать по отдельности.

Ответ 2

bootstrap() вызовет для вас компилятор AngularJS, как и 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.

Ответ 3

Я бы предложил взглянуть на библиотеку ocLazyLoad, которая регистрирует модули (или контроллеры, службы и т.д. на существующем модуле) во время выполнения и также загружает их с помощью requireJs или другой такой библиотеки.

Ответ 4

Я только что улучшил функцию, написанную Jussi-Kosunen, так что все вещи можно сделать одним разом.

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

Таким образом, вы можете загружать свой шаблон из любого места, а управляющие контроллеры - программно, даже вложенные.

Вот рабочий пример загрузки контроллера внутри другого: http://plnkr.co/edit/x3G38bi7iqtXKSDE09pN

Ответ 5

Мне также нужно было добавить несколько представлений и связать их с контроллерами во время выполнения из javascript-функции вне контекста angularJs, поэтому вот что я придумал:

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

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

теперь вызов функции setCnt() будет вводить и компилировать html, и он будет связан со вторым контроллером:

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

function setCnt() {
  // Injecting the view 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";
});

вот пример, чтобы проверить это: http://refork.com/x4bc

надеюсь, что это поможет.

Ответ 6

почему бы не использовать config и ui-router?

он загружается во время выполнения, и вам не нужно показывать свои контроллеры в html-коде

например, что-то вроде следующего

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'
                    },

                }

            })

    });
},

};

Ответ 7

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

Приведенный выше код является лишь примером.

Таким образом, вам не нужно помещать ng-controller="someController" в любом месте страницы - вы объявляете только <body ng-app="mainApp">

Та же структура может использоваться для каждого модуля или модулей внутри модулей

Ответ 8

Это то, что я сделал, 2 части действительно, используя ng-контроллер с его определенной функцией, а затем $controller для создания динамического контроллера: -

Во-первых, HTML - нам нужен статический контроллер, который будет создавать динамический контроллер.

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

Статический контроллер 'staticCtrl' определяет член области с названием dynamicCtrl, который вызывается для создания динамического контроллера. ng-controller будет принимать либо предопределенный контроллер по имени, либо просматривает текущую область действия для функции с тем же именем.

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

Мы используем eval(), чтобы взять строку (наш динамический код, который может прибыть откуда угодно), а затем службу $controller, которая примет либо предопределенное имя контроллера (обычный случай), либо конструктор функции, за которым следуют параметры конструктора (мы перейти в новую область) - Angular будет вводить (как любой контроллер) в функцию, мы запрашиваем только $scope и $rootScope выше.