При загрузке контроллер в AngularJS динамически


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

вот фрагмент, который реализует мое лучшее предположение о том, как это должно быть сделано на основе 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 бросает:Argument 'Ctrl' is not a function, got undefined. Если я правильно понял bootstrap, инжектор, который он возвращает, должен знать о Foo модуль, верно?

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

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

обновление:

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

8 61

8 ответов:

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

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

Скрипка. Только проблема в том, что вам нужно хранить $controllerProvider и использовать его в месте, где он не должен быть использован (после загрузки). Кроме того, кажется, что нет простого способа получить функцию, используемую для определения контроллера, пока он не зарегистрирован, поэтому мне нужно пройти через модуль _invokeQueue, который недокументированный.

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

UDPATE 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']);

Скрипка.

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

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

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

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

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

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

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

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

Я только что улучшил функцию, написанную Юсси-Косунен, так что все вещи могут быть сделаны с одним вызовом.

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

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

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

почему бы не использовать 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'
                    },

                }

            })

    });
},

};
'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">

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

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

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

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

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

.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 выше.