Dynamic partial arguments in AngularJS routing - javascript

I'm working with an angularjs site and have a background with working with routes in Rails and also Laravel in php. With routes in Laravel we could dynamically create a set of routes similar to:
foreach($cities as $city):
Route::get($city.'/hotels');
Route::get($city.'/{slug}');
endforeach;
Here we defined series of seperate routes in Laravel which technically do look the same except for the value of city and slug.
I'm finding angularJS a bit limited in defining routes in this case. Frankly am a bit lost here.
UPDATE
I've made some modifications here - basically I set up a service which retrieves assets from my database such as in this case a list of cities and categories. I'm trying to do this:
If {slug} is in the array of categories retrieved from my API, then use my ListController and list view but if its not then instead use my SingleVenueController and single view. Here's my code at the moment but its not working :(
appRouteProvider.when('/:city/:slug', {
templateUrl : function(sharedParams, $routeParams){
t = sharedParams.getCurrentPageType($routeParams);
if(t=='list'){
return '../../app/templates/list.html';
}
if(t=='single'){
return '../../app/templates/single.html';
}
},
controller : function(sharedParams, $routeParams){
t = sharedParams.getCurrentPageType($routeParams);
if(t=='list'){
return 'ListsController';
}
if(t=='single'){
return 'SingleController';
}
},
resolve:{
sharedParamsData:function(sharedParams){
return sharedParams.promise;
},
}
})
In the above sharedParams is a service and the getCurrentPageType just checks the url slug to decide what controller to send back - but its not really working at all :(

How about defining a single route with a paramater ?
In angularjs v1.x you can defined as many routes you want with as many params xor query
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/city/:slug', {
templateUrl: 'book.html',
controller: 'BookController',
resolve: {
// you can also retrieve some data as a resolved promise inside your route for better performance.
}
})
ref: https://docs.angularjs.org/api/ngRoute/service/$route

appRouteProvider.when('/:city/:slug', {
templateUrl : 'dafault.html',
controller : 'DefaultController',
resolve:{
factory: function($routeParams, $http, $location, sharedParams){
var city = $routeParams.city;
var slug = $routeParams.slug;
var deferred = $q.defer();
sharedParams.getCurrentPageType($routeParams).then(function(t) {
if(t=='list'){
$location.path('/' + city + '/' + slug + '/list');
deferred.resolve();
}
else if(t=='single'){
$location.path('/' + city + '/' + slug + '/single');
deferred.resolve();
} else {
deferred.reject();
}
});
return deferred.promise;
},
}
});
appRouteProvider.when('/:city/:slug/list', {
templateUrl: '../../app/templates/list.html',
controller: 'ListsController',
});
appRouteProvider.when('/:city/:slug/single', {
templateUrl: '../../app/templates/single.html',
controller: 'SingleController',
});
You can do it with separate routes. The idea is when user hits the main route it resolves first with the data from the backend. If the condition is met, resolve function will redirect to specific route if not it wont pass

Services in Angular cannot be injected in the configuration phase since they become available only in the run phase of an Angular application.
There is however a trick to load $http service in the config phase which you can use to load your cities/categories and set up your routes. Meanwhile, since controllers aren't registered up until the run phase, you may use the $controllerProvider to register your controllers beforehand in the configuration phase:
app.config(function ($routeProvider, $controllerProvider) {
$controllerProvider.register('ListController', ListController);
$controllerProvider.register('SingleController', SingleController);
// wire the $http service
var initInjector = angular.injector(['ng']);
var $http = initInjector.get('$http');
...
});
You can now call your API to get the cities (or whatever else) and iterate while registering each route:
...
// fetch the cities from the server
$http.get('/cities')
.then(function (response) {
var cities = response.data;
for(var i = 0; i < cities.length; i++){
$routeProvider
// assuming each city object has a `name` property
.when('/' + cities[i]['name'] + '/:slug', {
templateUrl: getTemplate(cities[i]['name']),
controller: getController(cities[i]['name'])
})
}
});
...
Note that I'm using the getTemplate and the getController methods which return the templateUrl and the relevant controller name strings respectively using an ordinary switch expression. You can choose your own approach.
Plunkr Demo
Note:
While a function with the templateUrl route options property does work with setting up a custom template, but when you use a function alongside the controller property, Angular will consider it as the constructor for the controller. Therefore, returning the name of the controller in that function won't work.

As Ahmad has already pointed out in his answer, if you pass a function to controller it is considered as a constructor for the controller.
Also you can't get a service injected dynamically in config block of your app.
So what you can do is, move your sharedData service in separate app (in my code below I've used appShared as a separate app where this service is defined) and then access it using angular.injector. This way you don't have to define it as a parameter to templateUrl / controller functions.
Btw, you can't pass custom parameters to templateUrl function (ref: https://docs.angularjs.org/api/ngRoute/provider/$routeProvider)
If templateUrl is a function, it will be called with the following
parameters:
{Array.<Object>} - route parameters extracted from the current $location.path() by applying the current route
Now for the controller, use $controller to dynamically load either ListsController or SingleController based on your condition.
Once that is loaded, extend your current controller (defined by your controller function) using angular.extend so that it inherits all the properties and methods of the dynamically loaded controller.
Check the complete code here: http://plnkr.co/edit/ORB4iXwmxgGGJW6wQDy9
app.config(function ($routeProvider) {
var initInjector = angular.injector(['appShared']);
var sharedParams = initInjector.get('sharedParams');
$routeProvider
.when('/:city/:slug', {
templateUrl: function ($routeParams) {
console.log("template url - ");
console.log($routeParams);
var t = sharedParams.getCurrentPageType($routeParams);
console.log(t);
if (t == 'list') {
return 'list.html';
}
if (t == 'single') {
return 'single.html';
}
},
controller: function ($routeParams, $controller, $scope) {
//getController(cities[i]['name'])
console.log("controller - ");
console.log($routeParams);
var t = sharedParams.getCurrentPageType($routeParams);
console.log(t);
if (t == 'list') {
angular.extend(this, $controller('ListsController', { $scope: $scope }));
}
if (t == 'single') {
angular.extend(this, $controller('SingleController', { $scope: $scope }));
}
}
});
});

Related

Loading html and Controller from server and creating dynamic states UI - router

I am looking for a Solution to load my App Content dynamically from the Server.
My Scenario:
Lets say we have 2 Users (A and B), my App consists of different Modules like lets say a shoppingList and a calculator, now my goal would be the User logs into my App from the Database I get the User rights and depending what rights he has, i would load the html for the views and the controller files for the logic part from the Server, while doing that I would create the states needed for the html and ctrl. So basically my App is very small consistent of the Login and everything else is getting pulled from the Server depending on the Userrights.
What I use:
Cordova
AngularJs
Ionic Framework
Why I need it to be all dynamic:
1)The possiblity to have an App that contains just the login logic, so when fixing bugs or adding Modules I only have to add the files to the server give the User the right for it and it is there without needing to update the app.
2)The User only has the functionality he needs, he doesnt need to have everything when he only has the right for 1 module.
3)The App grows very big at the moment, meaning every Module has like 5-10 states, with their own html and Controllers. currently there are 50 different Modules planned so you can do the math.
I looked at this to get some inspiration:
AngularJS, ocLazyLoad & loading dynamic States
What I tried so far:
I created 1 Html file which contains the whole module so I only have 1 http request:
Lets say this is my response from the server after the User logged in
HTML Part:
var rights= [A,B,C,D]
angular.forEach(rights, function (value, key) {
$http.get('http://myServer.com/templates/' + value + '.html').then(function (response) {
//HTML file for the whole module
splits = response.data.split('#');
//Array off HTMl strings
for (var l = 1; l <= splits.length; l++) {
//Putting all Html strings into templateCache
$templateCache.put('templates/' + value +'.html', splits[l - 1]);
}
}
});
Controller Part:
var rights= [A,B,C,D]
angular.forEach(rights, function (value, key) {
$http.get('http://myServer.com/controller/' + value + '.js').then(function (response) {
// 1 file for the whole module with all controllers
splits = response.data.split('#');
//Array off controller strings
for (var l = 1; l <= splits.length; l++) {
//Putting all Controller strings into templateCache
$templateCache.put('controllers/' + value +'.js', splits[l - 1]);
}
}
});
After loading the Controllers I try to register them:
$controllerProvider.register('SomeName', $templateCache.get('controllers/someController));
Which is not working since this is only a string...
Defining the Providers:
.config(function ($stateProvider, $urlRouterProvider, $ionicConfigProvider, $controllerProvider) {
// turns of the page transition globally
$ionicConfigProvider.views.transition('none');
$stateProviderRef = $stateProvider;
$urlRouterProviderRef = $urlRouterProvider;
$controllerProviderRef = $controllerProvider;
$stateProvider
//the login state is static for every user
.state('login', {
url: "/login",
templateUrl: "templates/login.html",
controller: "LoginCtrl"
});
//all the other states are missing and should be created depending on rights
$urlRouterProvider.otherwise('/login');
});
Ui-Router Part:
//Lets assume here the Rights Array contains more information like name, url...
angular.forEach(rights, function (value, key) {
//Checks if the state was already added
var getExistingState = $state.get(value.name)
if (getExistingState !== null) {
return;
}
var state = {
'lang': value.lang,
'params': value.param,
'url': value.url,
'templateProvider': function ($timeout, $templateCache, Ls) {
return $timeout(function () {
return $templateCache.get("templates" + value.url + ".html")
}, 100);
},
'ControllerProvider': function ($timeout, $templateCache, Ls) {
return $timeout(function () {
return $templateCache.get("controllers" + value.url + ".js")
}, 100);
}
$stateProviderRef.state(value.name, state);
});
$urlRouter.sync();
$urlRouter.listen();
Situation so far:
I have managed to load the html files and store them in the templateCache, even load them but only if the states were predefined.What I noticed here was that sometimes lets say when I remove an item from a List and come back to the View the item was there again maybe this has something to do with cache I am not really sure...
I have managed to load the controller files and save the controllers in the templateCache but I dont really know how to use the $ControllerPrioviderRef.register with my stored strings...
Creating the states did work but the Controller didnt fit so i could not open any views...
PS: I also looked at require.js and OCLazyLoad as well as this example dynamic controller example
Update:
Okay so I managed to load the Html , create the State with the Controller everything seems to work fine, except that the Controller does not seem to work at all, there are no errors, but it seems nothing of the Controller logic is executed. Currently the only solution to register the controller from the previous downloaded file was to use eval(), which is more a hack then a proper solution.
Here the code:
.factory('ModularService', ['$http', ....., function ( $http, ...., ) {
return {
LoadModularContent: function () {
//var $state = $rootScope.$state;
var json = [
{
module: 'Calc',
name: 'ca10',
lang: [],
params: 9,
url: '/ca10',
templateUrl: "templates/ca/ca10.html",
controller: ["Ca10"]
},
{
module: 'SL',
name: 'sl10',
lang: [],
params: 9,
url: '/sl10',
templateUrl: "templates/sl/sl10.html",
controller: ['Sl10', 'Sl20', 'Sl25', 'Sl30', 'Sl40', 'Sl50', 'Sl60', 'Sl70']
}
];
//Load the html
angular.forEach(json, function (value, key) {
$http.get('http://myserver.com/' + value.module + '.html')
.then(function (response) {
var splits = response.data.split('#');
for (var l = 1; l <= value.controller.length; l++) {
$templateCache.put('templates/' + value.controller[l - 1] + '.html', splits[l - 1]);
if (l == value.controller.length) {
$http.get('http://myserver.com//'+value.module+'.js')
.then(function (response2) {
var ctrls = response2.data.split('##');
var fullctrl;
for (var m = 1; m <= value.controller.length; m++){
var ctrlName = value.controller[m - 1] + 'Ctrl';
$controllerProviderRef
.register(ctrlName, ['$scope',...., function ($scope, ...,) {
eval(ctrls[m - 1]);
}])
if (m == value.controller.length) {
for (var o = 1; o <= value.controller.length; o++) {
var html = $templateCache
.get("templates/" + value.controller[o - 1] + ".html");
var getExistingState = $state.get(value.controller[o - 1].toLowerCase());
if (getExistingState !== null) {
return;
}
var state = {
'lang': value.lang,
'params': value.param,
'url': '/' + value.controller[o - 1].toLowerCase(),
'template': html,
'controller': value.controller[o - 1] + 'Ctrl'
};
$stateProviderRef.state(value.controller[o - 1].toLowerCase(), state);
}
}
}
});
}
}
});
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
$urlRouter.listen();
}
}
}])
Any help appreciated
Ok, so let's start from the beginning.
All the application logic should be contained on the server and served via API-calls through REST, SOAP or similar. By doing so, you reduce the amount of logic built into the UI, which reduces the stress on the client. This basically makes your client app a rendering agent, containing only models and views for the data and logic served by the backend API.
As foreyez stated in his/her comment, this isn't an issue for any modern (or half-modern) device.
If you insist on not loading all of the layouts at once, you could of course separate them into partials, which you load after the login based on the user privileges. By doing so, you reduce the amount of in-memory data, even though the improvement would be doubtable, at best.
Can I suggest you to do some changes to the way you load the states?
Write a script that give you back a json with the states the user can access.
Ex.
resources/routing-config.yourLangage?user=user-id-12345
this will return a json file that depends on the user logged in. The structure can be something like this:
[
{
"name": "home",
"url": "/home",
"templateUrl": "views/home.html",
"controller": "HomeController",
"dependencies": ["scripts/home/controllers.js", "scripts/home/services.js", "scripts/home/directives.js"]
},
{
"name": "user",
"url": "/user",
"templateUrl": "views/user.html",
"controller": "UserController",
"dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
}
]
Then let's write a service that will read the states the user is allowed to access:
app.factory('routingConfig', ['$resource',
function ($resource) {
return $resource('resources/routing-config.yourLangage', {}, {
query: {method: 'GET',
params: {},
isArray: true,
transformResponse: function (data) {
// before that we give the states data to the app, let's load all the dependencies
var states = [];
angular.forEach(angular.fromJson(data), function(value, key) {
value.resolve = {
deps: ['$q', '$rootScope', function($q, $rootScope){
// this will be resolved only when the user will go to the relative state defined in the var value
var deferred = $q.defer();
/*
now we need to load the dependencies. I use the script.js javascript loader to load the dependencies for each page.
It is very small and easy to be used
http://www.dustindiaz.com/scriptjs
*/
$script(value.dependencies, function(){ //here we will load what is defined in the dependencies field. ex: "dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
// all dependencies have now been loaded by so resolve the promise
$rootScope.$apply(function(){
deferred.resolve();
});
});
return deferred.promise;
}]
};
states.push(value);
});
return states;
}
}
});
}]);
Then let's configure the app:
app.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$filterProvider', '$provide', '$compileProvider',
function ($stateProvider, $urlRouterProvider, $locationProvider, $filterProvider, $provide, $compileProvider) {
// this will be the default state where to go as far as the states aren't loaded
var loading = {
name: 'loading',
url: '/loading',
templateUrl: '/views/loading.html',
controller: 'LoadingController'
};
// if the user ask for a page that he cannot access
var _404 = {
name: '_404',
url: '/404',
templateUrl: 'views/404.html',
controller: '404Controller'
};
$stateProvider
.state(loading)
.state(_404);
// save a reference to all of the providers to register everything lazily
$stateProviderRef = $stateProvider;
$urlRouterProviderRef = $urlRouterProvider;
$controllerProviderRef = $controllerProvider;
$filterProviderRef = $filterProvider;
$provideRef = $provide;
$compileProviderRef = $compileProvider;
//redirect the not found urls
$urlRouterProvider.otherwise('/404');
}]);
Now let's use this service in the app.run:
app.run(function ($location, $rootScope, $state, $q, routingConfig) {
// We need to attach a promise to the rootScope. This will tell us when all of the states are loaded.
var myDeferredObj = $q.defer();
$rootScope.promiseRoutingConfigEnd = myDeferredObj.promise;
// Query the config file
var remoteStates = routingConfig.query(function() {
angular.forEach(remoteStates, function(value, key) {
// the state becomes the value
$stateProviderRef.state(value);
});
// resolve the promise.
myDeferredObj.resolve();
});
//redirect to the loading page until all of the states are completely loaded and store the original path requested
$rootScope.myPath = $location.path();
$location.path('/loading'); //and then (in the loading controller) we will redirect to the right state
//check for routing errors
$rootScope.$on('$stateChangeError',
function(event, toState, toParams, fromState, fromParams, error){
console.log.bind(console);
});
$rootScope.$on('$stateNotFound',
function(event, unfoundState, fromState, fromParams){
console.error(unfoundState.to); // "lazy.state"
console.error(unfoundState.toParams); // {a:1, b:2}
console.error(unfoundState.options); // {inherit:false} + default options
});
});
Eventually, the LoadingController:
app.controller('LoadingController', ['$scope', '$location', '$rootScope',
function($scope, $location, $rootScope) {
//when all of the states are loaded, redirect to the requested state
$rootScope.promiseRoutingConfigEnd.then(function(){
//if the user requested the page /loading then redirect him to the home page
if($rootScope.myPath === '/loading'){
$rootScope.myPath = '/home';
}
$location.path($rootScope.myPath);
});
}]);
In this way everything is super flexible and lazy loaded.
I wrote 3 different user portals already and I can easily scale to all of the user portal I want.
I have developed an application with keeping those things in mind. Here is my architecture.
Folder Structure:
WebApp
|---CommonModule
|---common-module.js //Angular Module Defination
|---Controllers //Generally Nothing, but if you have a plan to
//extend from one CommonController logic to several
//module then it is usefull
|---Services //Common Service Call Like BaseService for all $http
//call, So no Module Specific Service will not use
//$http directly. Then you can do several common
//things in this BaseService.
//Like Error Handling,
//CSRF token Implementation,
//Encryption/Decryption of AJAX req/res etc.
|---Directives //Common Directives which you are going to use
//in different Modules
|---Filters //Common Filters
|---Templates //Templates for those common directives
|---index.jsp //Nothing, Basically Redirect to
//Login or Default Module
|---scripts.jsp //JQuery, AngularJS and Other Framworks scripts tag.
//Along with those, common controlers, services,
//directives and filtes.
|---templates.jsp //Include all common templates.
|---ng-include.jsp //will be used in templates.jsp to create angular
//template script tag.
|---ModuleA
|---moduleA-module.js //Angular Module Definition,
//Use Common Module as Sub Module
|---Controllers
|---Services
|---Directives
|---Filters
|---Templates
|---index.jsp
|---scripts.jsp
|---templates.jsp
|---ModuleB
|--- Same as above ...
Note: Capital Case denotes folder. Beside ModuleA there will a LoginModule for your case I think or You could Use CommonModule for it.
Mehu will be as follows.
Module A <!--Note: index.jsp are indexed file
//for a directive -->
Module B
Each of those JSP page are actually a independent angular application. Using those following code.
ModuleA/index.jsp
<!-- Check User Permission Here also for Security
If permission does not have show Module Unavailable Kind of JSP.
Also do not send any JS files for this module.
If permission is there then use this following JSP
-->
<!DOCTYPE HTML>
<html lang="en" data-ng-app="ModuleA">
<head>
<title>Dynamic Rule Engine</title>
<%# include file="scripts.jsp" %>
<%# include file="templates.jsp" %> <!-- Can be cached it in
different way -->
</head>
<body>
<%# include file="../common.jsp" %>
<div id="ngView" data-ng-view></div>
<%# include file="../footer.jsp" %>
</body>
</html>
ModuleA/scripts.jsp
<%# include file="../CommonModule/scripts.jsp" %> <!-- Include Common Things
Like Jquery Angular etc -->
<scripts src="Controlers/ModlueAController1.js"></script>
.....
ModuleA/templates.jsp
<%# include file="../CommonModule/templates.jsp" %>
<!-- Include Common Templates for common directives -->
<jsp:include page="../CommonModule/ng-include.jsp"><jsp:param name="src" value="ModuleA/Templates/template1.jsp" /></jsp:include>
.....
CommonModule/ng-include.jsp
<script type="text/ng-template" id="${param.src}">
<jsp:include page="${param.src}" />
</script>
But main problem of this approach is When user will change Module, Page will get refreshed.
EDIT:
There is a ModuleA.module.js file which actually contain module deceleration as follows.
angular.module('ModuleA.controllers', []);
angular.module('ModuleA.services', []);
angular.module('ModuleA.directives', []);
angular.module('ModuleA.filters', []);
angular.module('ModuleA',
['Common',
'ModuleA.controllers' ,
'ModuleA.services' ,
'ModuleA.directives' ,
'ModuleA.filters'])
.config(['$routeProvider', function($routeProvider) {
//$routeProvider state setup
}])
.run (function () {
});
I think I'm doing what you're asking. I achieve this by using UI-Router, ocLazyLoad and ui-routers future states. Essentially our setup allows us to have 50+ modules, all in the same code base, but when a user opens the app. its starts by only loading the base files required by the app. Then, as the user moves around between states, the application will load up the files required for that part, as their needed. (apologies for the fragmented code, I've had to rip it out of the code base, but tried to only provide the stuff thats actually relevant to the solution).
Firstly, the folder structure
Core App
config.js
Module 1 (/module1)
module.js
controllers.js
Module 2 (/module2)
module.js
controllers.js
etc
Config.js:
The first thing we do is create the base state, this is an abstract state, so the user can never actually just hit it.
$stateProvider.state('index', {
abstract: true,
url: "/index",
views: {
'': {
templateUrl: "views/content.html" // a base template to have sections replaced via ui-view elements
}
},
...
});
Then we configure the modules in ocLazyLoad. This allows us to just tell ocLazyLoad to load the module, and it loads all the required files (although in this instance, its only a single file, but it allows each module to have varying paths).
$ocLazyLoadProvider.config({
loadedModules: ['futureStates'],
modules: [
{
name: 'module1',
files: ['module1/module.js']
},
{
name: 'module2',
files: ['module2/module.js']
}
]
});
Next we create a function to allow ui-router to load the modules when requested (through future states).
function ocLazyLoadStateFactory($q, $ocLazyLoad, futureState) {
var deferred = $q.defer();
// this loads the module set in the future state
$ocLazyLoad.load(futureState.module).then(function () {
deferred.resolve();
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
}
$futureStateProvider.stateFactory('ocLazyLoad', ['$q', '$ocLazyLoad', 'futureState', ocLazyLoadStateFactory]);
Then we configure the actual future states. These are states that may be loaded in the future, but we don't want to configure them right now.
$futureStateProvider.futureState({
'stateName': 'index.module1', // the state name
'urlPrefix': '/index/module1', // the url to the state
'module': 'module1', // the name of the module, configured in ocLazyLoad above
'type': 'ocLazyLoad' // the future state factory to use.
});
$futureStateProvider.futureState({
'stateName': 'index.module2',
'urlPrefix': '/index/module2',
'module': 'module2',
'type': 'ocLazyLoad'
});
If you want the list of future states to be provided asynchronously:
$futureStateProvider.addResolve(['$http', function ($http) {
return $http({method: 'GET', url: '/url'}).then(function (states) {
$futureStateProvider.futureState({
'stateName': 'index.module2',
'urlPrefix': '/index/module2',
'module': 'module2',
'type': 'ocLazyLoad'
});
});
}]);
Then we configure the modules as follows:
module1/module.js
$stateProvider.state('index.module1', {
url: "/module1",
abstract: true,
resolve: {
loadFiles: ['$ocLazyLoad', function($ocLazyLoad){
return return $ocLazyLoad.load(['list of all your required files']);
}]
}
})
$stateProvider.state('index.module1.sub1', {
url: "/sub1",
views: {
// override your ui-views in here. this one overrides the view named 'main-content' from the 'index' state
'main-content#index': {
templateUrl: "module1/views/sub1.html"
}
}
})

invoking an angular route from angular service to load a new view and controller

I'm trying to invoke a route through and angular service and since I am using $http.post I can't get the route to invoke. I may be going at this all wrong so I'm hoping someone can make a suggestion or point me in the right direction. Initally I have a page load with a controller which once the search command is called it passes a json object with the request to an angular service which then calls webAPI to pass the request onto my other business layers. Here is a logical diagram of the workflow. The response in blue is a new data object being returned to the UI with the users search results.
From my app I have the following routes setup
(function () {
app = angular.module('app', ['ui.bootstrap', 'ngRoute', 'ngAnimate']).value('ngToastr', toastr);
function router($routeProvider) {
$routeProvider.
when('/search/query', {
templateUrl: '../../AngularTemplates/searchResults.html',
controller: 'searchResultCtrl'
}).
otherwise({
templateUrl: '../../AngularTemplates/splashPage.html'
});
}
app.config(['$routeProvider', router]);
//added toaster as factory so it can be injected into any controller
angular.module('app').factory('ngNotifier', function (ngToastr) {
return {
notify: function (msg) {
ngToastr.success(msg);
},
notifyError: function (msg) {
ngToastr.error(msg);
},
notifyInfo: function (msg) {
ngToastr.info(msg);
}
}
});
})();
The initial page calls the controller which has a service dependency
app.controller('searchController', ['$scope', '$filter', 'searchService', 'ngNotifier', '$log', '$timeout', 'searchAttributes' , function ($scope, $filter, searchService, ngNotifier, $log, $timeout, searchAttributes) {
var vm = this;
vm.search = search;
vm.updateEntities = updateEntitySelection;
//bootstraped data from MVC
$scope.userOptions = searchAttributes.mvcData;
//scoped variables
$scope.searchTerm = null;
//ui container for search response
$scope.searchResponse;
$scope.entityList = [
'Search All ',
'Search in Departments ',
'Search in Automotive '
]
$scope.selectedEntity = 'Search All';
function buildSearchRequest() {
var searchResponse = {
searchTerm: $scope.searchTerm,
pageSize: 10,//this will be set by configuration from the UI
pagesReturned: 0,
entityFilter: $scope.selectedEntity
};
return searchResponse;
}
function onError(msg) {
$log.error('An error has occured: ' + msg.data);
}
function updateEntitySelection(entityName) {
$scope.selectedEntity = entityName;
}
function search() {
var request = buildSearchRequest();
searchService.search(request);
}
}]);
and the search service
app.factory('searchService', ['$http', function($http) {
var myEsResults;
function getSearchResults(searchRequest) {
return $http.post('search/query', searchRequest, {}).then(function (response) {
myEsResults = response.data});
}
var getResults = function () {
return myEsResults;
};
return{
search: getSearchResults,
getResults: getResults
};
}]);
What I am trying to accomplish is when the document loads a splash screen is displayed (which works). when the search is executed the request is passed to webapi and then the response is returned as an objectback to the view and a new controller so it can render the search results. I have passed data back and forth between controllers in the past however where I am stuck is using an angular service to call route in webapi. Making this call does not update the page URL and therefore the route is not invoked nor is the second controller loaded to display the results. In the past I have invoked angular routes using a url http://#/route however in this instance I am using an input button with ng-click. I would appreciate any suggestions as to how on the return of data get the 'result view' and controller to load. Is routing the correct approach or is there another way to load the view and controller when using an angular service?
Thanks in advance
<button type="button" class="btn btn-primary btn-lg" ng-click="vm.search()"><span class="glyphicon glyphicon-search"></span></button>
Should be able to do it using $location.path('/search/query')
function getSearchResults(searchRequest) {
return $http.post('search/query', searchRequest, {}).then(function (response) {
myEsResults = response.data;
$location.path('/search/query');
});
}
however workflow seems like it would make more sense to add either routeParams to the url or a search query param and pass url encoded query term to url and make request based on that. Then the request would be made by the searchResultCtrl controller or a resolve in the router config.
Something like:
$routeProvider.
when('/search/query/:queryterm', {
templateUrl: '../../AngularTemplates/searchResults.html',
controller: 'searchResultCtrl'
}).
And path would be generated by:
$location.path('/search/query/' + encodeURIComponent($scope.searchTerm) );

Dynamically Creating Routes in Angular JS

We're trying to make the switch to angular, but we have a pretty big issue with routing. Our current site has something like 10,000 unique routes -- ever page has a unique ".html" identifier. There's no particular convention that would allow us to assign the controller to them, so I created a lookup API endpoint.
Here's the workflow I'm trying to create:
Angular app loads. One "otherwise" route is set up.
When someone clicks a link, I don't know if the resource is a product or a category, so a query is made to the lookup endpoint with the unique ".html" identifier. The endpoint returns two things: the name of the resource, and an ID ("product" and "10" for example). So to be clear, they hit a page like, "http://www.example.com/some-identifier.html," I query the lookup API to find out what kind of resource this is, and get a result like, "product" and "10" -- now I know it's the product controller/template and I need the data from product id 10.
The app assigns the controller and template ("productController" and "product.html"), queries the correct endpoint for data ("/api/product/10"), and renders the template.
The problems I'm facing:
$http isn't available during config, so I can't hit the lookup table.
Adding routes after the config is sloppy at best -- I've done it successfully by assigning $routeProvider to a global variable and doing it after the fact, but man, it's ugly.
Loading all the routes seems impractical -- just the size of the file would be pretty heavy for a lot of connections/browsers.
We can't change the convention now. We have 4 years of SEO and a lot of organic traffic to abandon our URLs.
I feel like I might be thinking about this the wrong way and there's something missing. The lookup table is really the problem -- not knowing what kind of resource to load (product, category, etc). I read this article about loading routes dynamically, but again, he's not making an external query. For us, loading the controllers isn't the problem, it's resolving the routes and then assigning them c
How would you solve the problem?
Solution
Huge thanks to #user2943490 for pointing me in the right direction. Don't forget to upvote his answer! I made it a little more general so that I don't have to define the route types.
API Structure
This configuration requires at least two endpoints: /api/routes/lookup/:resource_to_lookup:/ and /api/some_resource_type/id/:some_resource_id:/. We query the lookup to find out what kind of resource it points to and what the ID of the resource is. This allows you to have nice clean urls, like, "http://www.example.com/thriller.html" (a single) and "http://www.example.com/michaeljackson.html" (a collection).
In my case, if I query something like, "awesome_sweatshirt.html" my lookup will return a JSON object with "{type: 'product', id: 10}". Then I query "/api/product/id/10" to get the data.
"Isn't that slow?" you ask. With varnish in front, all of this happens in way less than 1 second. We're seeing pageload times locally of less than 20ms. Across the wire from a slow dev server was closer to half a second.
app.js
var app = angular.module('myApp', [
'ngRoute'
])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.otherwise({
controller: function($scope, $routeParams, $controller, lookupService) {
/* this creates a child controller which, if served as it is, should accomplish your goal behaving as the actual controller (params.dashboardName + "Controller") */
if ( typeof lookupService.controller == "undefined" )
return;
$controller(lookupService.controller, {$scope:$scope});
delete lookupService.controller;
//We have to delete it so that it doesn't try to load again before the next lookup is complete.
},
template: '<div ng-include="templateUrl"></div>'
});
$locationProvider.html5Mode(true);
})
.controller('appController', ['$scope', '$window', '$rootScope', 'lookupService', '$location', '$route', function($scope, $window, $rootScope, lookupService, $location, $route){
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
window.scrollTo(0,0)
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl($location.path())) {
return;
}
// Show the page load spinner
$scope.isLoaded = false
lookupService.query($location.path())
.then(function (lookupDefinition) {
$route.reload();
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
}]);
lookupService.js
myApp.factory('lookupService', ['$http', '$q', '$location', function lookupService($http, $q, $location) {
return {
id: null,
originalPath: '',
contoller: '',
templateUrl: '',
query: function (url) {
var deferred = $q.defer();
var self = this;
$http.get("/api/routes/lookup"+url)
.success(function(data, status, headers, config){
self.id = data.id;
self.originalPath = url;
self.controller = data.controller+'Controller';
self.templateUrl = '/js/angular/components/'+data.controller+'/'+data.controller+'.html';
//Our naming convention works as "components/product/product.html" for templates
deferred.resolve(data);
})
return deferred.promise;
}
}
}]);
productController.js
myApp.controller('productController', ['$scope', 'productService', 'cartService', '$location', 'lookupService', function ($scope, productService, cartService, $location, lookupService) {
$scope.cart = cartService
// ** This is important! ** //
$scope.templateUrl = lookupService.templateUrl
productService.getProduct(lookupService.id).then(function(data){
$scope.data = data
$scope.data.selectedItem = {}
$scope.$emit('viewLoaded')
});
$scope.addToCart = function(item) {
$scope.cart.addProduct(angular.copy(item))
$scope.$emit('toggleCart')
}
}]);
Try something like this.
In the route config you set up a definition for each resource type and their controllers, templates and a resolve:
$routeProvider.when('/products', {
controller: 'productController',
templateUrl: 'product.html',
resolve: {
product: function ($route, productService) {
var productId = $route.current.params.id;
// productService makes a request to //api/product/<productId>
return productService.getProduct(productId);
}
}
});
// $routeProvider.when(...
// add route definitions for your other resource types
Then you listen for $locationChangeStart. If the URL being navigated to is a "unique identifer", query the lookup. Depending on the resource type returned by the lookup, navigate to the correct route as defined above.
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl(currentUrl)) {
return;
}
// Stop the default navigation.
// Now you are in control of where to navigate to.
event.preventDefault();
lookupService.query(currentUrl)
.then(function (lookupDefinition) {
switch (lookupDefinition.type) {
case 'product':
$location.url('/products');
break;
case 'category':
$location.url('/categories');
break;
// case ...
// add other resource types
}
$location.search({
// Set the resource's ID in the query string, so
// it can be retrieved by the route resolver.
id: lookupDefinition.id
});
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
You can use $routeParams for this.
e.g.
route/:type/:id
so type and id can be totally dynamic, the different type handling will be up to the route's controller.
What if you have a json file with the information of the routes (and if there is not a security issue) and iterate over it to attach routes to the app?
e.g.
JSON:
routes: [
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
},
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
}
]
And then iterate over the contents of the JSON and attach them to $routeProvider.when ?
I am not sure if it is a good idea, depends how big would be the JSON file and if you dont want to expose all your routes to a possible attacker.
From the AngularJS documentation,
The $routeParams service allows you to retrieve the current set of
route parameters.
Dependencies: $route
Example look like
// Given:
// URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
// Route: /Chapter/:chapterId/Section/:sectionId
// Then
$routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}

Stop resolving controller data on search

Is there any way to stop resolving controller data if $location.search() is used.
Below is my code
App.config(function($routeProvider, $locationProvider) {
$routerProvider.when('/overview' {
title:"Overview",
controller:"OverViewCtrl",
templateUrl:"pages/overview.html",
resolve:{
mainData:function(Service){
return Service.getOverviewData();
}
});
});
In my controller I am using the same
App.controller("OverViewCtrl", function($scope, $location, mainData, Service){
$scope.data = mainData.data;
$scope.otherData = null;
Service.getOtherData($location.search(), function(data){
$scope.otherData = data;
});
$scope.search = function(param){
$location.search(param);
};
});
Each time search method is called mainData data is getting loaded from service, is there any way to call the getOtherData service on search.
$routeProvider configuration has reloadOnSearch property. From the documentation
[reloadOnSearch=true] - {boolean=} - reload route when only
$location.search() or $location.hash() changes.
Set it to false.
In your controller then subscribe to $route event $routeUpdate
$scope.$on('$routeUpdate',function(event,args) {
//Here you can get the other data.
});
Check the second argument to see what is passed in the args object hash. I believe it contains the current route and some other data.

AngularJS: forward to login page for authentication, retain route as GET parameter?

I'm a bit of an Angular newbie. I'm trying to write an Angular service that on any page, will check if a user is logged in, and if not, forward them to a login page, passing their current path as a a GET parameter.
I'm almost there, but it's not quite working. The problem I'm having is as follows: if the user goes to #/articles/my-articles/, they get forwarded to #/login/?next=%2Farticles%2F:module%2F.
In other words, it looks as though Angular is passing the route pattern, not the actual URL.
This is my authentication code:
auth.run(['$rootScope', '$location', '$user', 'TOKEN_AUTH', 'PROJECT_SETTINGS', function ($rootScope, $location, $user, TOKEN_AUTH, PROJECT_SETTINGS) {
var MODULE_SETTINGS = angular.extend({}, TOKEN_AUTH, PROJECT_SETTINGS.TOKEN_AUTH);
$rootScope.$on('$routeChangeStart', function (e, next, current) {
if (next.$$route && !next.$$route.anonymous && !$user.authenticated) {
var nextParam = next.$$route.originalPath;
$location.url(MODULE_SETTINGS.LOGIN + '?next=' + nextParam);
}
});
}]);
I can get the original path in a hacky way using current.params.module - but that doesn't help me, because it seems that routeChangeStart is fired several times and the current object is undefined on all but the last fire.
This is my routes file:
articles.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/articles/:module/', {
templateUrl: 'views/articles/article_list.html',
controller: 'ArticlesListCtrl'
})
.when('/articles/:module/:id/', {
templateUrl: 'views/articles/article_detail.html',
controller: 'ArticlesDetailCtrl'
});
}]);
How can I fix this problem?
auth.run(['$rootScope', '$location', '$user', 'TOKEN_AUTH', 'PROJECT_SETTINGS', function ($rootScope, $location, $user, TOKEN_AUTH, PROJECT_SETTINGS) {
var MODULE_SETTINGS = angular.extend({}, TOKEN_AUTH, PROJECT_SETTINGS.TOKEN_AUTH);
$rootScope.$on('$routeChangeStart', function (e, next, current) {
if (!$user.authenticated) {
$location.url(MODULE_SETTINGS.LOGIN + '?next=' + $location.path());
$location.replace();
}
});
}]);
If logging in is not a AngularJS view, you may have to provide an otherwise route:
(depends on your $locationProvider config)
$routeProvider.otherwise({
template: 'Redirecting…',
controller : 'Redirect'
});
...
articles.controller('Redirect', ['$location', function($location) {
if (someConditionThatChecksIfUrlIsPartOfApp) {
location.href = $location.path();
return;
} else {
// Show 404
}
}]);
Side note: you shouldn't read $$-prefixed properties, they are private AngularJS variables.
Also note: don't use $ prefixes ($user) in your own code, these are public properties, reserved for AngularJS.
My solution works on Angular 1.2.13 :
preventDefault stops angular routing and $window.location sends me out to login page. This is working on a ASP.NET MVC + Angular app.
$rootScope.$on("$locationChangeStart", function (event, next, current) {
event.preventDefault();
$window.location = '/Login';
}
});

Categories

Resources