I'm looking for a way to "send" or "register" some configuration from several modules to one single module.
The idea is that different modules will have theirs own route configuration. And one single module controller will build the menu based on those configurations.
I want to "send" or "register" configuration instead of querying, because the menu controller cannot knows which modules are available.
Here is a solution to my need:
Each module defines its own routes using $routeProvider with a custom parameter defining if the route should be displayed in the menu and another custom parameter with the name to display. (See above Module A example).
Then the menu controller will loop the $route.routes variable (which contains all defined routes) to build de menu. (See above Module B example).
Module A:
angular
.module('module-a', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/path-a', {
controller: 'ModuleAController',
templateUrl: 'path/to/template.html',
menuDisplay: true, // Custom parameter
displayName: 'Solution Manager' // Custom parameter
});
});
Module B:
angular
.module('module-b', [])
.controller('MenuController', ['$scope', '$route', function ($scope, $route) {
var entries = [];
angular.forEach($route.routes, function (route, path) {
if ( route.menuDisplay == true ) {
this.push({
displayName: route.displayName,
route: '#' + path
});
}
}, entries);
$scope.menu = entries;
}]);
You will probably add some more checks to ensure displayName is provided, etc ...
Related
I have an existing page into which I need to drop an angular app with controllers that can be loaded dynamically.
Here's a snippet which implements my best guess as to how it should be done based on the API and some related questions I've found:
// 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. Note that this is a simplification of the actual chain of events, there are various async calls and user inputs between the lines above.
When I try to run the above code, the linker which is returned by $compile throws: Argument 'Ctrl' is not a function, got undefined. If I understood bootstrap correctly, the injector it returns should know about the Foo module, right?
If instead I make a new injector using angular.injector(['ng', 'Foo']), it seems to work but it creates a new $rootScope which is no longer the same scope as the element where the Foo module was bootstrapped.
Am I using the right functionality to do this or is there something I've missed? I know this isn't doing it the Angular way, but I need to add new components that use Angular to old pages that don't, and I don't know all the components that might be needed when I bootstrap the module.
UPDATE:
I've updated the fiddle to show that I need to be able to add multiple controllers to the page at undetermined points in time.
I've found a possible solution where I don't need to know about the controller before 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
$('<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();
});
Fiddle. Only problem is that you need to store the $controllerProvider and use it in a place where it really shouldn't be used (after the bootstrap). Also there doesn't seem to be an easy way to get at a function used to define a controller until it is registered, so I need to loop through the module's _invokeQueue, which is undocumented.
UPDATE: To register directives and services, instead of $controllerProvider.register simply use $compileProvider.directive and $provide.factory respectively. Again, you'll need to save references to these in your initial module config.
UDPATE 2: Here's a fiddle which automatically registers all controllers/directives/services loaded without having to specify them individually.
bootstrap() will call the AngularJS compiler for you, just like 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.
I would suggest to take a look at ocLazyLoad library, which registers modules (or controllers, services etc on existing module) at run time and also loads them using requireJs or other such library.
I also needed to add multiple views and bind them to controllers at runtime from a javascript function outside the angularJs context, so here's what I came up with :
<div id="mController" ng-controller="mainController">
</div>
<div id="ee">
2nd controller's view should be rendred here
</div>
now calling setCnt() function will inject and compile the html, and it will be linked to the 2nd controller:
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";
});
here's an example to test this : https://refork.codicode.com/x4bc
hope this helps.
I have just improved the function written by Jussi-Kosunen so that all stuff can be done with one single call.
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();
});
}
This way you could load your template from anywhere and instanciate controllers programmatically, even nested.
Here is a working example loading a controller inside another one:
http://plnkr.co/edit/x3G38bi7iqtXKSDE09pN
why not use config and ui-router?
it is loaded at runtime and you have no need to show your controllers in html code
for example something like the following
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'
},
}
})
});
},
};
This is what I did, 2 parts really, using ng-controller with its scope defined function and then $controller service to create the dynamic controller :-
First, the HTML - we need a Static Controller which will instantiate a dynamic controller ..
<div ng-controller='staticCtrl'>
<div ng-controller='dynamicCtrl'>
{{ dynamicStuff }}
</div>
</div>
The static controller 'staticCtrl' defines a scope member called 'dynamicCtrl' which is called to create the dynamic controller. ng-controller will take either a predefined controller by name or looks at current scope for function of same name ..
.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;
}
}])
We use eval() to take a string (our dynamic code which can come from anywhere) and then the $controller service which will take either a predefined controller name (normal case) or a function constructor followed by constructor parameters (we pass in a new scope) - Angular will inject (like any controller) into the function, we are requesting just $scope and $rootScope above.
'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(),
}
};
})();
The above code is only an example.
This way you don't need to put ng-controller="someController" anywhere on a page — you only declare <body ng-app="mainApp">
Same structure can be used for each module or modules inside modules
I recently converted my project from bootstrapping angular through the ng-app default method to using RequireJs and doing so manually. Everything works fine most of the time, but some percentage of the time I get errors saying my controllers and services of the login page are undefined, so the controller doesn't properly load and values aren't bound.
I believe the problem stems from the order of execution of dependencies. First, here are the errors:
Error: [ng:areq] http://errors.angularjs.org/1.3.15/ng/areq?p0=NavController&p1=not%20aNaNunction%2C%20got%20undefined
Error: [$injector:unpr] http://errors.angularjs.org/1.3.15/$injector/unpr?p0=UsersServiceProvider%20%3C-%20UsersService%20%3C-%20LoginController
So far I am only seeing this problem in Chrome, and when I have the javascript console open everything seems to work. I have inserted a few console.log() statements to get some visibility into the order of activity within these scripts. For three files, routeConfig.js, nav.js and usersService.js I have a console.log on the first line within the define function and another one on the first line of the angular definition function:
routeConfig.js:
define(['angularAMD',
'angular-route',
'uiBootstrap',
'lib/angularSlideables',
'lib/ng-infinite-scroll',
'lib/angular-modal-service'], function (angularAMD) {
console.log('top of routeConfig');
var app = angular.module('svlPlatform', ['ngRoute', 'ui.bootstrap', 'angularSlideables', 'infinite-scroll', 'angularModalService']).value('THROTTLE_MILLISECONDS', 750);
app.config(['$routeProvider', function ($routeProvider, $httpProvider) {
console.log('inside routeConfig');
// Because angularAMD is built on requireJS, the following paths to controllerUrl follow requireJs's conventions.
// They don't need a .js extention and they are relative to the baseUrl specified in require.config.
$routeProvider
.when('/login', angularAMD.route({
templateUrl: 'views/login.html',
controllerUrl: 'controllers/login'
}))
.when('/me', angularAMD.route({
templateUrl: 'views/me.html',
controllerUrl: 'controllers/me'
})) //this continues and returns app...
nav.js:
define(['config/routeConfig', 'services/authService', 'services/usersService', 'services/menuService'], function(app) {
console.log('top of nav controller');
app.controller('NavController', ['$log', '$anchorScroll', '$location', '$rootScope', 'AuthService', 'UsersService', 'MenuService',
function ($log, $anchorScroll, $location, $rootScope, auth, users, menu) {
console.log('inside nav controller');
var vm = this;
menuScope = this;
function loadMenu() {
menu.items().then(function(response){
vm.menuItems = response.data.menuItems;
$('body').css("background-color","#f2f2f2");
markMenuActive(vm.menuItems);
}, function(error) {
console.log('There was a problem getting the menu data.');
});
} //continues...
usersService.js
define(['config/routeConfig', 'utils/urlBuilder', 'services/menuService'], function (app, _urlBuilder) {
console.log('top of users service');
var urlBuilder = _urlBuilder;
app.factory('UsersService', ['$http', 'MenuService', function ($http, menu) {
console.log('inside users service');
var meCache;
var refreshMeCache = function () {
meCache = $http.get('/api/v1/users/me');
menu.refreshItemsCache();
}; //continues...
Here is my requireJs config:
require.config({
baseUrl: 'javascripts',
paths: {
'angular': '//ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min',
'angular-route': '//ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-route.min',
'bootstrapJs': '//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min',
'uiBootstrap': '//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.1/ui-bootstrap-tpls.min',
'angularAMD': '//cdn.jsdelivr.net/angular.amd/0.2.0/angularAMD.min'
},
shim: {
'angularAMD': ['angular'],
'bootstrapJs': [],
'uiBootstrap': ['angular'],
'lib/angularSlideables': ['angular'],
'lib/ng-infinite-scroll': ['angular'],
'angular-route': ['angular'],
'lib/angular-modal-service': ['angular']
},
deps: [
'bootstrapJs',
'config/routeConfig',
'services/authService',
'services/usersService',
'services/menuService',
'controllers/nav',
'controllers/main',
'lib/angular-modal-service',
'config/config',
'app'
]
});
When the login page (the first page the app loads) is loaded correctly, i see this after I open the console:
top of routeConfig
top of users service
top of nav controller
inside routeConfig
inside users service
inside nav controller
but when the page doesn't load properly, this is what i'm seeing in the console.
top of routeConfig
inside routeConfig
top of users service
top of nav controller
...then errors
I don't understand the bootstrapping process or requireJs' load process enough to figure out why this is happening and how to fix it. Thank you to anyone who has any ideas. Thanks.
Perhaps this is the best way to solve this problem, perhaps it is not. I gave up trying to use require to specify any kind of load order in the require config or using any of the module definitions. I forced it by having several of my controller and service modules return the app, and have them dependent in series rather than in parallel, this forces require to run the scripts in the order I specify.
So, rather than have nav.js and usersService.js dependent on routeConfig.js, which provides the app object for both of those components, I have the service dependent on routeConfig.js, and the nav.js module dependent on the usersService (which now returns its app object). This method could continue to include other controllers and services which need to be loaded before bootstrapping. I then finally call another file, app.js, which calls the angular.bootstrap() (angularAMD.bootstrap() in my case), and everything works now.
I'm a complete Angular noob and trying to do some fancy stuff quickly, so forgive me if this is a dumb question.
I've created a website that uses routing, and I'm using ui-router for the routing instead of the standard Angular router. The theory is still the same - I have an index.html page in the root of my website which is the "master" or "host" page, and loginView.htm, which is a partial, exists in a separate directory.
The mainController for the project is loaded in the index.html page. Referencing this controller does NOT cause an error or problem.
What I'd like to do, in order to keep code manageable and small, is have the custom controller for a partial page lazy load when I load the partial, and then associate that partial page with the newly loaded controller. Makes sense, right? I don't want to load all the controllers by default, because that's a waste of time and space.
So my structure looks like this (if it matters to anyone):
Root
--app/
----admin/
------login/
--------loginView.html
--------loginController.js
--mainController.js
index.html
This is my loginController code. For testing purposes, I have made the mainController code match this exactly.
var loginController = function ($scope, $translate) {
$scope.changeLanguage = function (key) {$translate.use(key); };
};
angular.module('app').controller('loginController', loginController);
Finally, here is my routing code:
function config($stateProvider, $urlRouterProvider, $ocLazyLoadProvider) {
$urlRouterProvider.otherwise("/admin/login");
$stateProvider
.state('login', {
url: "/admin/login",
templateUrl: "app/admin/login/loginView.html",
controller: loginController,
resolve: {
loadPlugin: function ($ocLazyLoad) {
return $ocLazyLoad.load([
{
name: 'loginController',
files: ['app/admin/login/loginController.js']
}
]);
}
}
})
;
}
angular
.module('app')
.config(config)
.run(function ($rootScope, $state) {
$rootScope.$state = $state;
});
Now - if I remove the whole "resolve" section, and change the controller to "mainController", everything works. The page loads, the buttons work, they call the "changeLanguage" function and everything is wonderful.
But I want the "changeLanguage" feature to reside in the loginController because that's the only page that uses it. So when the code looks like it does above, an error fires("Uncaught Error: [$injector:modulerr]") and the partial page fails to load.
I don't understand what I'm doing wrong, and I'm not finding what I need via Google (maybe I just don't know the right question to ask).
Help?
Looking through the docs I cannot find the name property for ocLazyLoad#load.
Try the following:
resolve: {
loadPlugin: function ($ocLazyLoad) {
return $ocLazyLoad.load(['app/admin/login/loginController.js']);
}
}
Or, pre configure it in a config block:
app.config(function ($ocLazyLoadProvider) {
$ocLazyLoadProvider.config({
modules: [{
name: 'loginController',
files: ['app/admin/login/loginController.js']
}]
});
});
// then load it as:
$ocLazyLoad.load('loginController');
I'm building an application using Angular-UI-Router. I have a top-level Angular module that depends on sub-modules and those sub-modules have controller with the same name as follows:
var moduleA = angular.module('moduleA', []);
moduleA.controller('SameNameCtrl', function () {
// ModuleA SameNameCtrl implementation
});
var moduleB = angular.module('moduleB', []);
moduleB.controller('SameNameCtrl', function () {
// ModuleB SameNameCtrl implementation
});
var app = angular.module('app', ['ui.route', 'moduleA', 'moduleB']);
How do I specify controller in different modules when constructing state with Angular-UI-Router?
$stateProvider
.state('app.stateA', {
url: '/stateA',
templateUrl: 'template-A.html',
controller: ???? // how to specify moduleA SameNameCtrl
})
.state('app.stateB', {
url: '/stateB',
templateUrl: 'template-B.html',
controller: ???? // how to specify moduleB SameNameCtrl
})
Sure, I can assign different controller name for controllers in different module, but I'd like to know if it's possible with controllers having the same name.
Thanks!
Unfortunatelly modules are not namespace mechanism in angularjs. You should come up / follow a naming convention to differentiate the controllers
Trying to setup some helpers value to the module. Tried with service and value and it didn't help:
var finance = angular.module('finance', ['finance.services'])
.value("helpers", {
templatePath: function (name) {
return '/areas/scripts/finance/templates/' + name + '/index.html';
}
})
.config(['$routeProvider', 'helpers', function ($routeProvider, helpers) {
$routeProvider.
when('/', {
templateUrl: helpers.getTemplatePath('dashboard'),
controller: DashboardController
})
.when('/people', {
templateUrl: '/areas/scripts/app/people/index.html',
controller: PeopleController
})
.otherwise({
redirectTo: '/dashboard'
});
}]);
What I am doing wrong?
The problem is that you are trying to inject a value object helpers in the config block of a AngularJS module and this is not allowed. You can only inject constants and providers in the config block.
The AngularJS documentation (section: "Module Loading & Dependencies") gives the insight into this:
A module is a collection of configuration and run blocks which get
applied to the application during the bootstrap process. In its
simplest form the module consist of collection of two kinds of blocks:
Configuration blocks - get executed during the provider registrations
and configuration phase. Only providers and constants can be injected
into configuration blocks. This is to prevent accidental instantiation
of services before they have been fully configured.
Run blocks - get
executed after the injector is created and are used to kickstart the
application. Only instances and constants can be injected into run
blocks. This is to prevent further system configuration during
application run time.
Instead of .value you can use .constant. Then you can use your service in .config part.
Your helper method is called templatePath and you are calling it inside .config as getTemplatePath. Shouldn't it be:
when('/', {
templateUrl: helpers.templatePath('dashboard'),
controller: DashboardController
})