angularjs route to resolve - javascript

I have a big number of angularjs routes in my app. I would like to set access to these route based on some user permission levels.
angular.module('myApp').run(['$rootScope', 'someAuthFactory', function($rootScope, someAuthFactory) {
$rootScope.$on('$stateChangeStart', function (event, toState) {
$rootScope.permissions = someAuthFactory.getPermssionLevels();
$rootScope.specialRights = $rootScope.permissions.indexOf('superRole') > -1;
...
and here is one of my routes:
.state("dashboard.overview", {
url: "/dashboard",
templateUrl: "app/dashboard.html",
resolve: {
roles: ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
},
so this code works, but if i want to add this:
resolve: {
roles: ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
}
to every route, it is gonna be duplicate code, or if I want to lookup some other role, it is gonna be boring.
Could we make the resolve part much smaller and much cleaner?

create a variable in config function on top of the routes like this
var resolveRoles = ['rootScope', function (rootScope) {
return $rootScope.specialRights;}]
}
and use it in every route like this,
.state("dashboard.overview", {
url: "/dashboard",
templateUrl: "app/dashboard.html",
resolve: {
roles: resolveRoles
},
});

You can create a service/factory, which shares $rootScope.specialRights. Just init $rootScope.specialRights in the service in the first resolve of your route e.g.
.state("dashboard", {
url: ...
templateUrl: ...
resolve: {
roles: ['rootScope', function (rootScope) {
YourServiceOrFactory.setSpecialRights($rootScope.specialRights);
return $rootScope.specialRights
}]
},
And there where you need it
YourServiceOrFactory.getSpecialRights()
It does not have to be in other resolves, just in your controller of your route or in your directive/component. Much cleaner and performanter than multiple resovles.

Since you're already setting this property in $rootScope, it is an overkill to use resolve in all of your routes, just to get a variable value, and of course, resolve is not designed for such purposes.
Instead, just inject the $rootScope in your controller and use $rootScope.specialRights in the controller.
Example:
In your controller, service, directive, or component, you can inject the $rootScope like this:
angular.module('your_module')
.controller('your_controller', ['$rootScope', function($rootScope) {
// Access $rootScope.permissions or $rootScope.specialRights here
}];

Related

angularjs throws Error: [ng:areq] in resolving the lazyloading the scripts and adding a view controller separately

I don't know why this first state works but the second one doesn't:
Working state:
.state('app.pages.invoice', {
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Not working state, throwing the Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
.state('app.form.validation', {
controller: "validationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
This second one works only when the controller is injected via this:
resolve: loadSequence('validationCtrl')
that is (controller is moved into the resolve and there is not currentAuth anymore):
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: loadSequence('validationCtrl')
})
and then I don't know how to integrate my currentAuth element into the resolve again. How can I inject the validationCtrl.js via resolve and add the currentAuth element also into resolve?
here is the loadsequence function:
// Generates a resolve object previously configured in constant.JS_REQUIRES (config.constant.js)
function loadSequence() {
var _args = arguments;
return {
deps: ['$ocLazyLoad', '$q',
function ($ocLL, $q) {
var promise = $q.when(1);
for (var i = 0, len = _args.length; i < len; i++) {
promise = promiseThen(_args[i]);
}
return promise;
function promiseThen(_arg) {
if (typeof _arg == 'function')
return promise.then(_arg);
else
return promise.then(function () {
var nowLoad = requiredData(_arg);
if (!nowLoad)
return $.error('Route resolve: Bad resource name [' + _arg + ']');
return $ocLL.load(nowLoad);
});
}
function requiredData(name) {
if (jsRequires.modules)
for (var m in jsRequires.modules)
if (jsRequires.modules[m].name && jsRequires.modules[m].name === name)
return jsRequires.modules[m];
return jsRequires.scripts && jsRequires.scripts[name];
}
}]
};
}
My first state doesn't have any controller, so I am fine resolving the currentAuth alone. But when the view has a controller, add the currentAuth causes the controller not to work anymore.
note:
my currentAuth is taken from here.
update:
herer is the validationCtrl.js:
app.controller('ValidationCtrl', ["$scope", "$state", "$timeout", "SweetAlert", "$location",
function ($scope, $state, $timeout, SweetAlert, $location) {
...
update 2:
basically the question is to allow only currently signed in users to view the pages which are children of app.; so my parent view is like this: so basically I am looking to inject the currentAuth factory into the main parent view and the children should inherit this. They cannot be viewed unless the currentAuth in the parent is resolved.
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl'),
abstract: true
})
edit 1:
I have put the question in other words as well here and trying to find an answer to multiple resolve states.
edit 2:
here is the main.js: pastebin url
and the validationCtrl.js pastebin url.
actually, the validationCtrl is just an example controller among other controllers I have.
Question is how to block view permission for child views unless the parent currentAuth is resolved? given that I don't know how to handle multiple resolve with loadsequence and a singleton factory.
Assuming you are using ui router as a routing framework for your SPA app.
Error: [ng:areq]
the error you get:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
is due to the declaration of the controller within the state, the name of the controller function is not resolved because "ValidationCtrl" is not equal to "validationCtrl" then correct state is:
.state('app.form.validation', {
controller: "ValidationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Abstract States - Nested States
to answer the second question, a useful example for your case may be this:
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: {
scripts: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl').deps,
currentAuth: function(Auth){ return Auth.$requireSignIn();}
},
abstract: true
})
.state('app.pages.invoice', {
// url will become '/app/invoice'
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice'
})
.state('app.form.validation', {
controller: "ValidationCtrl",
// url will become '/app/validation'
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate'
})
As you can see from the example in the resolve of the abstract state you can define different factory functions, ui router will wait until all dependencies are resolved before resolving the children states.
Resolve property explanation :
The resolve property is a map object. The map object contains key/value pairs of:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}:
If string, then it is an alias for a service.
Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before the controller is instantiated and its value is injected into the controller.
for more details I refer you to ui router doc.
As said in my comment i suggest you to try the following :
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
controller: "validationCtrl",
resolve:{
"myCtrl": loadSequence('validationCtrl'),
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
The other part of my comment was about the fact that child states inherits parent's resolve and children can override it.
So you can just do the following :
.state('app', {
// all states require logging by default
resolve:{
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
// i'm guessing we're redirecting toward app.login if not logged
}]
}
})
.state('app.login', {
resolve:{
"currentAuth": ["Auth", function(Auth) {
return true;// just be sure to not do infinite redirections
}]
}
})
Note if you have some trouble because Auth isn't yet loaded with lazy loading, you should be able to load it in a angular.run.
Firstly coming to the error
Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
controller: "validationCtrl", change it according to the main controller
that is
This means there is no validationCtrl function.
I may be wrong but i think there is a small typo' in this line in your controller controller: "validationCtrl", change it according to the **main controller** controller: "ValidationCtrl"
that is
This error happens due to either defining two angular.modules with the same name in different files containing different arguments as you may be trying to implement the dependancy injection.
It causes the problem as the script loaded inyour main html file won't know which angular.module to be configured.
To resolve this define the angular.modules with different names.
How to block view permission for child views unless the parent currentAuth is resolved
You can install this package angular middlewareThis middleware package contains some pre-defined route functions or you can also create your own functions.Along with this $http documentation using the success and callback functions,you can create your own middleware and the auth service while using a singleton factory
OR
Assuming that you are using node.js as your backend you can use [middleware][3] routing in your server using express and map it to the frontend routes.
Here is a perfect tutorial for middleware authentication in nodejs

Parent scope not initiated for nested state (angular ui)

For the following state definitions and the url http://localhost/#/foo/bar, I expect to receive the console output
/foo
/foo/bar
However, I only get one /foo. Is my expectation wrong? What can I do to get the parent controller invoked as well?
$stateProvider
.state('foo', {
url: '/foo',
controller: function($scope) {
console.log('/foo')
},
abstract: true
})
.state('bar', {
parent: 'foo',
url: '/bar',
controller: function($scope) {
console.log('/foo/bar')
}
})
Update
Even more strange, when I add
onEnter: function() {
console.log('enter')
}
enter is printed (but not /foo/bar).
Update 2
I'd like to add a resolve attribute to the parent route and have all children wait for a deferral to be resolved until their controllers get instantiated. This is what it follows: https://github.com/angular/angular.js/issues/5854 The scope is used to store the result of the deferral (which is a $http response).
On the abstract parent state, there should be a ui-view directive so the child state knows where to insert itself.
.state('foo', {
url: '/foo',
abstract: true,
controller: function($scope) {
console.log("/foo");
},
template: '<ui-view/>'
})
Here is a working example: http://plnkr.co/edit/OskGCBSQGhEXlxaNLZtD?p=preview
Update 2 response
You should be able to just add a resolve to the abstract parent state:
resolve: {
user: function($q, $timeout) {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve("a resolved user");
}, 1000);
return deferred.promise;
}
}
Here is a working example of that: http://plnkr.co/edit/cm0xUP8LmruHiVBhccuf?p=preview
Yeah, your expectation is wrong: only one route will be matched. If you want to match the second route, you'll need to set the url to /foo/bar, but there's no way to hit them both with one URL.
I'm not sure what you're trying to do but if you want to access url parameters you can use /foo/:paramName and the $routeParams service.

Resolving data in Angular Controller

I'm pretty new to Angular and I've been going round in circles on this one for a while now.
A bit of background first, I'm using the MEAN stack from mean.io which uses Angular UI Router.
I have a Post model in my DB which can have a category id assigned to it.
When I create a new post I want to load the existing categories from the DB and display them in a select box.
From what I can see I need to use resolve, first of all it doesn't feel right having logic in the resolve property which is in a file called config.js - so far I've placed the call to a service in there and im getting the categories back using the following code:
.state('create post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
resolve: {
loadCategories: function (Categories) {
Categories.query(function(categories) {
return categories;
});
}
}
})
The first problem is that I can't access the returned data in my controller or view.
Secondly I only want to load Categories that belong to a certain Organisation. I will be assigning an organisation id to each user so how can I access the currently signed in user when I'm in config.js - again this doesn't feel like the right place to be doing this sort of logic though.
Any help would be really appreciated.
Thanks
config.js:
register post state :
.state('post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
resolve: PostsController.resolve
})
register posts controller:
.controller({
PostsController: ['$scope', 'loadCategories', PostsController],
...
})
controller function:
function PostsController($scope, loadCategories){
$scope.categories = loadCategories;
};
PostsController.resolve = {
loadCategories: ['dependencies', function(dependencies){
return dependencies.query(...)
}]
};
Angular manage your dependency injection
Assuming Categories is an angular resource, you should be able to just
loadCategories: function (Categories) {
return Categories.query();
}
And then in your controller:
.controller('PostsController', function ($scope, loadCategories) {
$scope.categories = loadCategories;
});
Ok, reading your comments, it sounds like you'll have some issue because you want to inject this into the controller, but only in certain states. You could try:
.state('create post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
data: {
categories: Categories.query()
}
})
and then
.controller('PostsController', function ($scope, $state){
console.log($state.current.data.categories);
});
Which should work...

$routeProvider - Injecting same dependency for all routes

The following code:
$routeProvider
.when("/page1", { controller: "MyController", resolve: {Strategy: "StrategyOne"}})
waits for the Strategy dependency to be resolved before to instantiate the controller "MyController".
In my application I have a function which returns a promise, which when resolved, gives the current user. Let's called that function Authentication.currentUser()
I would like all the pages of my app to wait for that promise to be resolved before to render a page. I could happily add a line for each route declaration but I would rather avoid duplication.
I have a controller called 'MainCtrl' which is called for all pages thanks to this line in my template:
<html ng-app="clientApp" ng-controller="MainCtrl">
I think one possible way to address this would be if it was possible to specify Authentication.currentUser() as a dependency of "MainCtrl" at the controller level (not at the route level because this dependency does not depend on a particular route).
Thanks for your help guys!
For those who want to address this with the standard $routeProvider, this is what I came out with:
$rootScope.$on('$routeChangeStart', function (event, next, current) {
if (!next.resolve){ next.resolve = {} }
next.resolve.currentUser = function(Authentication){
return Authentication.currentUser();
};
});
If you can move from the default router, to ui-router, then you can do this with nested states. Just copying the example from https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-resolved-dependencies :
$stateProvider.state('parent', {
resolve:{
resA: function(){
return {'value': 'A'};
}
},
controller: function($scope, resA){
$scope.resA = resA.value;
}
})
.state('parent.child', {
resolve:{
resB: function(resA){
return {'value': resA.value + 'B'};
}
},
controller: function($scope, resA, resB){
$scope.resA2 = resA.value;
$scope.resB = resB.value;
}

Delaying AngularJS route change until model loaded to prevent flicker

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetched using its respective services.
For example, if there were a ProjectsController that listed all Projects and project_index.html which was the template that showed these Projects, Project.query() would be fetched completely before showing the new page.
Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).
$routeProvider resolve property allows delaying of route change until data is loaded.
First define a route with resolve attribute like this.
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: PhoneListCtrl.resolve}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
notice that the resolve property is defined on route.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function(Phone, $q) {
// see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
}
Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phones is injected into the controller and it is defined in the resolve property.
The resolve.phones function is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.
Working demo: http://mhevery.github.com/angular-phonecat/app/#/phones
Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
Here's a minimal working example which works for Angular 1.0.2
Template:
<script type="text/ng-template" id="/editor-tpl.html">
Editor Template {{datasets}}
</script>
<div ng-view>
</div>
JavaScript:
function MyCtrl($scope, datasets) {
$scope.datasets = datasets;
}
MyCtrl.resolve = {
datasets : function($q, $http) {
var deferred = $q.defer();
$http({method: 'GET', url: '/someUrl'})
.success(function(data) {
deferred.resolve(data)
})
.error(function(data){
//actually you'd want deffered.reject(data) here
//but to show what would happen on success..
deferred.resolve("error value");
});
return deferred.promise;
}
};
var myApp = angular.module('myApp', [], function($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/editor-tpl.html',
controller: MyCtrl,
resolve: MyCtrl.resolve
});
});​
​
http://jsfiddle.net/dTJ9N/3/
Streamlined version:
Since $http() already returns a promise (aka deferred), we actually don't need to create our own. So we can simplify MyCtrl. resolve to:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: 'GET',
url: 'http://fiddle.jshell.net/'
});
}
};
The result of $http() contains data, status, headers and config objects, so we need to change the body of MyCtrl to:
$scope.datasets = datasets.data;
http://jsfiddle.net/dTJ9N/5/
I see some people asking how to do this using the angular.controller method with minification friendly dependency injection. Since I just got this working I felt obliged to come back and help. Here's my solution (adopted from the original question and Misko's answer):
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: {
phones: ["Phone", "$q", function(Phone, $q) {
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
]
},
delay: ["$q","$defer", function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
]
},
}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}]);
Since this code is derived from the question/most popular answer it is untested, but it should send you in the right direction if you already understand how to make minification friendly angular code. The one part that my own code didn't requires was an injection of "Phone" into the resolve function for 'phones', nor did I use any 'delay' object at all.
I also recommend this youtube video http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , which helped me quite a bit
Should it interest you I've decided to also paste my own code (Written in coffeescript) so you can see how I got it working.
FYI, in advance I use a generic controller that helps me do CRUD on several models:
appModule.config ['$routeProvider', ($routeProvider) ->
genericControllers = ["boards","teachers","classrooms","students"]
for controllerName in genericControllers
$routeProvider
.when "/#{controllerName}/",
action: 'confirmLogin'
controller: 'GenericController'
controllerName: controllerName
templateUrl: "/static/templates/#{controllerName}.html"
resolve:
items : ["$q", "$route", "$http", ($q, $route, $http) ->
deferred = $q.defer()
controllerName = $route.current.controllerName
$http(
method: "GET"
url: "/api/#{controllerName}/"
)
.success (response) ->
deferred.resolve(response.payload)
.error (response) ->
deferred.reject(response.message)
return deferred.promise
]
$routeProvider
.otherwise
redirectTo: '/'
action: 'checkStatus'
]
appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->
$scope.items = items
#etc ....
]
This commit, which is part of version 1.1.5 and above, exposes the $promise object of $resource. Versions of ngResource including this commit allow resolving resources like this:
$routeProvider
resolve: {
data: function(Resource) {
return Resource.get().$promise;
}
}
controller
app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {
$scope.data = data;
}]);
This snippet is dependency injection friendly (I even use it in combination of ngmin and uglify) and it's a more elegant domain driven based solution.
The example below registers a Phone resource and a constant phoneRoutes, which contains all your routing information for that (phone) domain. Something I didn't like in the provided answer was the location of the resolve logic -- the main module should not know anything or be bothered about the way the resource arguments are provided to the controller. This way the logic stays in the same domain.
Note: if you're using ngmin (and if you're not: you should) you only have to write the resolve functions with the DI array convention.
angular.module('myApp').factory('Phone',function ($resource) {
return $resource('/api/phone/:id', {id: '#id'});
}).constant('phoneRoutes', {
'/phone': {
templateUrl: 'app/phone/index.tmpl.html',
controller: 'PhoneIndexController'
},
'/phone/create': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
phone: ['$route', 'Phone', function ($route, Phone) {
return new Phone();
}]
}
},
'/phone/edit/:id': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
form: ['$route', 'Phone', function ($route, Phone) {
return Phone.get({ id: $route.current.params.id }).$promise;
}]
}
}
});
The next piece is injecting the routing data when the module is in the configure state and applying it to the $routeProvider.
angular.module('myApp').config(function ($routeProvider,
phoneRoutes,
/* ... otherRoutes ... */) {
$routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });
// Loop through all paths provided by the injected route data.
angular.forEach(phoneRoutes, function(routeData, path) {
$routeProvider.when(path, routeData);
});
$routeProvider.otherwise({ redirectTo: '/' });
});
Testing the route configuration with this setup is also pretty easy:
describe('phoneRoutes', function() {
it('should match route configuration', function() {
module('myApp');
// Mock the Phone resource
function PhoneMock() {}
PhoneMock.get = function() { return {}; };
module(function($provide) {
$provide.value('Phone', FormMock);
});
inject(function($route, $location, $rootScope, phoneRoutes) {
angular.forEach(phoneRoutes, function (routeData, path) {
$location.path(path);
$rootScope.$digest();
expect($route.current.templateUrl).toBe(routeData.templateUrl);
expect($route.current.controller).toBe(routeData.controller);
});
});
});
});
You can see it in full glory in my latest (upcoming) experiment.
Although this method works fine for me, I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object; it would make things soooOOOOOooOOOOO much easier.
Edit: used Angular v1.2(rc2)
Delaying showing the route is sure to lead to an asynchronous tangle... why not simply track the loading status of your main entity and use that in the view. For example in your controller you might use both the success and error callbacks on ngResource:
$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
$scope.httpStatus = 200;
}, function(response) {
$scope.httpStatus = response.status;
});
Then in the view you could do whatever:
<div ng-show="httpStatus == 0">
Loading
</div>
<div ng-show="httpStatus == 200">
Real stuff
<div ng-repeat="project in projects">
...
</div>
</div>
<div ng-show="httpStatus >= 400">
Error, not found, etc. Could distinguish 4xx not found from
5xx server error even.
</div>
I worked from Misko's code above and this is what I've done with it. This is a more current solution since $defer has been changed to $timeout. Substituting $timeout however will wait for the timeout period (in Misko's code, 1 second), then return the data hoping it's resolved in time. With this way, it returns asap.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
}
Using AngularJS 1.1.5
Updating the 'phones' function in Justen's answer using AngularJS 1.1.5 syntax.
Original:
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
Updated:
phones: function(Phone) {
return Phone.query().$promise;
}
Much shorter thanks to the Angular team and contributors. :)
This is also the answer of Maximilian Hoffmann. Apparently that commit made it into 1.1.5.
You can use $routeProvider resolve property to delay route change until data is loaded.
angular.module('app', ['ngRoute']).
config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
$routeProvider.
when('/entities', {
templateUrl: 'entities.html',
controller: 'EntitiesCtrl',
resolve: EntitiesCtrlResolve
}).
when('/entity/:entityId', {
templateUrl: 'entity.html',
controller: 'EntityCtrl',
resolve: EntityCtrlResolve
}).
otherwise({redirectTo: '/entities'});
}]);
Notice that the resolve property is defined on route.
EntitiesCtrlResolve and EntityCtrlResolve is constant objects defined in same file as EntitiesCtrl and EntityCtrl controllers.
// EntitiesCtrl.js
angular.module('app').constant('EntitiesCtrlResolve', {
Entities: function(EntitiesService) {
return EntitiesService.getAll();
}
});
angular.module('app').controller('EntitiesCtrl', function(Entities) {
$scope.entities = Entities;
// some code..
});
// EntityCtrl.js
angular.module('app').constant('EntityCtrlResolve', {
Entity: function($route, EntitiesService) {
return EntitiesService.getById($route.current.params.projectId);
}
});
angular.module('app').controller('EntityCtrl', function(Entity) {
$scope.entity = Entity;
// some code..
});
I like darkporter's idea because it will be easy for a dev team new to AngularJS to understand and worked straight away.
I created this adaptation which uses 2 divs, one for loader bar and another for actual content displayed after data is loaded. Error handling would be done elsewhere.
Add a 'ready' flag to $scope:
$http({method: 'GET', url: '...'}).
success(function(data, status, headers, config) {
$scope.dataForView = data;
$scope.ready = true; // <-- set true after loaded
})
});
In html view:
<div ng-show="!ready">
<!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
<div class="progress progress-striped active">
<div class="bar" style="width: 100%;"></div>
</div>
</div>
<div ng-show="ready">
<!-- Real content goes here and will appear after loading -->
</div>
See also: Boostrap progress bar docs
I liked above answers and learned a lot from them but there is something that is missing in most of the above answers.
I was stuck in a similar scenario where I was resolving url with some data that is fetched in the first request from the server. Problem I faced was what if the promise is rejected.
I was using a custom provider which used to return a Promise which was resolved by the resolve of $routeProvider at the time of config phase.
What I want to stress here is the concept of when it does something like this.
It sees the url in url bar and then respective when block in called controller and view is referred so far so good.
Lets say I have following config phase code.
App.when('/', {
templateUrl: '/assets/campaigns/index.html',
controller: 'CampaignListCtr',
resolve : {
Auth : function(){
return AuthServiceProvider.auth('campaign');
}
}
})
// Default route
.otherwise({
redirectTo: '/segments'
});
On root url in browser first block of run get called otherwise otherwise gets called.
Let's imagine a scenario I hit rootUrl in address bar AuthServicePrivider.auth() function gets called.
Lets say Promise returned is in reject state what then???
Nothing gets rendered at all.
Otherwise block will not get executed as it is for any url which is not defined in the config block and is unknown to angularJs config phase.
We will have to handle the event that gets fired when this promise is not resolved. On failure $routeChangeErorr gets fired on $rootScope.
It can be captured as shown in code below.
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// Use params in redirection logic.
// event is the routeChangeEvent
// current is the current url
// previous is the previous url
$location.path($rootScope.rootPath);
});
IMO It's generally a good idea to put event tracking code in run block of application. This code run just after the config phase of the application.
App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
$rootScope.rootPath = "my custom path";
// Event to listen to all the routeChangeErrors raised
// by the resolve in config part of application
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// I am redirecting to rootPath I have set above.
$location.path($rootScope.rootPath);
});
}]);
This way we can handle promise failure at the time of config phase.
I have had a complex multi-level sliding panel interface, with disabled screen layer. Creating directive on disable screen layer that would create click event to execute the state like
$state.go('account.stream.social.view');
were producing a flicking effect. history.back() instead of it worked ok, however its not always back in history in my case. SO what I find out is that if I simply create attribute href on my disable screen instead of state.go , worked like a charm.
<a class="disable-screen" back></a>
Directive 'back'
app.directive('back', [ '$rootScope', function($rootScope) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
}
};
} ]);
app.js I just save previous state
app.run(function($rootScope, $state) {
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
$rootScope.previousState = fromState.name;
$rootScope.currentState = toState.name;
});
});
One possible solution might be to use the ng-cloak directive with the element where we are using the models e.g.
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
I think this one takes least effort.

Categories

Resources