Динамическая загрузка контроллера 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 throws: Argument 'Ctrl' is not a function, got undefined
, Если я правильно понял загрузчик, то возвращаемый им инжектор должен знать о Foo
модуль, верно?
Если вместо этого я сделаю новый инжектор, используя angular.injector(['ng', 'Foo'])
, кажется, работает, но это создает новый $rootScope
который больше не является той же областью, что и элемент, где Foo
модуль был загружен.
Использую ли я нужную функциональность для этого или я что-то упустил? Я знаю, что это не делает это с помощью Angular, но мне нужно добавить новые компоненты, которые используют Angular, на старые страницы, которые этого не делают, и я не знаю всех компонентов, которые могут понадобиться при загрузке модуля.
ОБНОВИТЬ:
Я обновил скрипку, чтобы показать, что мне нужно иметь возможность добавлять несколько контроллеров на страницу в неопределенные моменты времени.
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, которая регистрирует модули (или контроллеры, службы и т. Д. В существующем модуле) во время выполнения, а также загружает их с помощью requireJs или другой такой библиотеки.
Мне также нужно было добавить несколько представлений и связать их с контроллерами во время выполнения из функции javascript вне контекста angularJs, так что вот что я придумал:
<div id="mController" ng-controller="mainController">
</div>
<div id="ee">
2nd controller's view should be rendred here
</div>
теперь вызов функции setCnt() внедрит и скомпилирует html, и он будет связан со вторым контроллером:
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
надеюсь это поможет.
Почему бы не использовать 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'
},
}
})
});
},
};
Это то, что я сделал, на самом деле, из двух частей, используя ng-controller с его функцией, определенной областью действия, а затем службу $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 выше.
Я только что улучшил функцию, написанную Юсси-Косуненом, так что все вещи могут быть выполнены одним вызовом.
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
'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">
Та же структура может использоваться для каждого модуля или модулей внутри модулей