Inject routes dynamically in angularjs - angularjs

I'm using angular to dynamically create another angular app. I was wondering if there is a way to inject routes dynamically. Something like
angular.module('app').controller('CreateCtrl', function ($scope,$routeProvider) {
var count = 0;
$scope.addPage = function(){
$routeProvider.when('/'+count, {
templateUrl: 'views/created.html',
controller: 'CreatedCtrl'
});
count++;
};
});
I know this code is silly but I hope it transmits what I'm trying to do. When user intercts with the application he/she will create new routes... is that possible?

There are two ways:
Option 1
modifying the routes object directly, as suggested in https://stackoverflow.com/a/13173667/17815
$route.routes['/dynamic'] = {templateUrl: 'dynamic.tpl.html'};
Be careful: you need to add both "/newurl" and "/newurl/" (with trailing slash).
see the angular code:
this.when = function(path, route) {
routes[path] = extend({reloadOnSearch: true}, route);
// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length-1] == '/')
? path.substr(0, path.length-1)
: path +'/';
routes[redirectPath] = {redirectTo: path};
}
return this;
};
Option 2
you can keep a reference to $routeProvider:
var $routeProviderReference;
var app = angular.module('myApp', ['myApp.controllers']);
app.config(['$routeProvider', function($routeProvider) {
$routeProviderReference = $routeProvider;
}]);
See http://blog.brunoscopelliti.com/how-to-defer-route-definition-in-an-angularjs-web-app
This is polluting the global namespace but you still use the interface to mutate the routes object. I guess this makes it easier to maintain.
Maybe you can try to put it in a closure.
In any case, keep in mind that you are expected to do it in a provider to make the application robust. "define before use".

Related

Can ng-include load controller defined within including html file in <script> tags (or any other way)? [duplicate]

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

Where should I place code to be used across components/controllers for an AngularJS app?

Should it be associated with the app module? Should it be a component or just a controller? Basically what I am trying to achieve is a common layout across all pages within which I can place or remove other components.
Here is what my app's structure roughly looks like:
-/bower_components
-/core
-/login
--login.component.js
--login.module.js
--login.template.html
-/register
--register.component.js
--register.module.js
--register.template.html
-app.css
-app.module.js
-index.html
This might be a bit subjective to answer but what I personally do in a components based Angular application is to create a component that will encapsulate all the other components.
I find it particularly useful to share login information without needing to call a service in every single component. (and without needing to store user data inside the rootScope, browser storage or cookies.
For example you make a component parent like so:
var master = {
bindings: {},
controller: masterController,
templateUrl: 'components/master/master.template.html'
};
function masterController(loginService) {
var vm = this;
this.loggedUser = {};
loginService.getUser().then(function(data) {
vm.loggedUser = data;
});
this.getUser = function() {
return this.loggedUser;
};
}
angular
.module('app')
.component('master', master)
.controller('masterController', masterController);
The master component will take care of the routing.
index.html:
<master></master>
master.template.html:
<your common header>
<data ui-view></data>
<your common footer>
This way, every component injected inside <ui-view> will be able to 'inherit' the master component like so:
var login = {
bindings: {},
require: {
master: '^^master'
},
controller: loginController,
templateUrl: 'components/login/login.template.html'
};
and in the component controller
var vm=this;
this.user = {};
this.$onInit = function() {
vm.user = vm.master.getUser();
};
You need to use the life cycle hook $onInit to make sure all the controllers and bindings have registered.
A last trick to navigate between components is to have a route like so (assuming you use ui-router stable version):
.state('home',
{
url : '/home',
template : '<home></home>'
})
which will effectively route and load your component inside <ui-view>
New versions of ui-router include component routing.

AngularJS inject $http into config for angular-translate

I'm using angular-translate module and am trying to inject all my translations that are on server with $http. I use a provider and I know that only i can inject dependencies through $get but I can't call that function from my provider. I need to know if i can do this and how i do it.
This is my provider.
.provider('languageServices', function (){
this.languages = {};
this.getExistLanguages = function() {
return ['en','es'];
};
this.getAllLanguages = function(){
return this.languages;
};
this.$get = function($http){
return {
getSpecificLanguage : function(lan) {
return this.languages = $http.post('fr3/i18n',lan);
}
}
};
});
this is my config app
.config(function ($stateProvider, $urlRouterProvider, USER_ROLES, $translateProvider, languageServicesProvider) {
$stateProvider.state('dashboard', {
url: '/dashboard',
views: {
'header': {template: ''},
'content': { templateUrl: 'views/dashboard.html' }
},
data: { authorizedRoles: [USER_ROLES.admin] }
});
$translateProvider.preferredLanguage('es');
// here is where i want inject all my translations with something like:
// var languages = languageServicesProvider.getAllLanguages();
//and languages pass it to translateProvider
});
I know this code has some errors but I only want you have a idea that I want to do.
Thanks
So angular-translate provides it's own $http process to do this. I happened to literally just implement this today, so you're in luck. You can see it here in their docs.
http://angular-translate.github.io/docs/#/api/pascalprecht.translate.$translateUrlLoader
Their docs are pretty bad, but the way you would implement this is in your app.config where you have your preferred language thing you would add...
$translateProvider.useUrlLoader(options)
Again this is where their documentation is bad. The options you need for this will just be your url, so...
$translateProvider.userUrlLoader({
url: '/yoururl'
});
This will create an http call that will try to get from '/yoururl?lang=en_US' or whatever language code is currently active.
You are also going to need to include the script for url loader which is this here
https://github.com/angular-translate/angular-translate/blob/master/src/service/loader-url.js
That also gives you some more info in the comments about using it.
Let me know if you have anymore questions. Hope this helps. Took me a while to figure out what is going on with this thing. Again, very bad documentation.
Edit:
I noticed you are also writing your own services to make a list of available languages. Angular-translate also has something for this...
$translateProvider.registerAvailableLanguageKeys(['en', 'ja']);

Using routes in AngularJS to gather data

I have an single page app built on AngularJS which is configured using HTML5 routing.
So I use:
http://www.example.com/products rather than http://www.example.com/#/products
I also have wildcard subdomains affiliates can use for example:
http://myaffiliate.example.com
And I gather data about myaffiliate from a firebase using this controller:
app.controller("ReplicatedController", function($scope, $firebaseObject) {
var parts = location.hostname.split('.');
var refSubdomain = parts.shift();
var ref = new Firebase("https://example-firebase.firebaseio.com/" + refSubdomain);
var syncObject = $firebaseObject(ref);
syncObject.$bindTo($scope, "coach");
});
This all works fine but in addition to using a wildcard subdomain I also need affiliates to be able to use urls. For example:
http://example.com/myaffiliate
Is it possible to do this, and how do I do that?
You're going to need the $routeProvider service so you can send route parameters. Then you're able to do something like this.
.config(function($routeProvider){
$routeProvider.when('/:my_affiliate', {
//other route details
});
})
.controller('Ctrl', function($scope, $routeParams) {
console.log($routeParams.my_affiliate); // This will print affiliate
});

Restrict access to route with routeprovider unless variable as been set

I'm in the process of learning AngularJS, working on a more in-depth ToDo app. I'm having an issue with trying to limit access to a url or "route" using angular.
When you hit my dev url on my machine (todo.ang) it brings you to todo.ang/#/home, on this view you see the categories which have todos associated to each. EG (category = cat, cat has a todo of "feed", and "play"), when you click a category I'm calling the $scope.goToCategory function (seen in my JS fiddle) which sets a variable for my firebase ref then redirects you too /#/todo. This is working correctly.
My problem is, I don't want the user to be able to access /#/todo if the todoRef variable is still undefined. But it seems like even after $scope.goToCategory is called and todoRef is set to a firebase URL, the routerprovider never gets recalled to know that todoRef has been set to a different value so it always forces you back to /#/home.
code:
var todoRef = undefined;
if (todoRef !== undefined) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl'
});
}
$scope.goToCategory = function(catId) {
test = catId;
todoRef = new Firebase("URL HERE");
$location.path('/todo');
}
I didn't include the entire file of code but if thats necessary, I can do that as well.
JSFiddle
All routes are only being set during the config phase.
what happens in your code is that 'todo' route is ignored during the initiation of ngRoute.
What you should do is to setup the route but have a resolve like so:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl',
resolve: {
todoRef: ['$q', function($q) {
return todoRef ? todoRef : $q.reject('no ref');
}]
}
});
}]);
If 'todoRef' is undefined the route is rejected.
Also you should consider moving 'todoRef' into a service and not on global scope.
You can also listen for route errors and for example redirect to home route:
app.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('$routeChangeError', function() {
$location.path('/home');
});
}]);

Resources