Let's say in 90-95% of my routes I need to check if user is in (let's say jail).
Then, I'm currently doing something like:
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
injail: function(jailservice){
return jailservice.injail();
}
}
})
Do I really need to do this resolve on each route? (the routes are in different folders, each route file contains route for a specific module).
Is it something better I can do than call the resolve injail on every route?
A few options.
Option 1 - use parent state with resolve
$stateProvider
.state('parent', {
resolve:{
resA: function () {}
}
})
.state('parent.child', {
// resA from parent state available to all child states
controller: function (resA) {}
});
More info
Option 2 - external resolve functions (less duplicated code)
Declaration:
resolve: {
myResolve: myResolve
}
If using ES2015, you can shorten it to resolve: {myResolve} (see enhanced object literals)
Definition (in a separate file containing all resolves):
myResolve.$inject = ['myService'];
function myResolve(myService) {
return myService.getStuff();
}
More info
EDIT - example using your code:
In your routes declaration, change resolve to: resolve: {injail: injailResolve}
In separate file, the definition:
injailResolve.$inject = ['jailservice'];
function injailResolve(jailservice) {
return jailservice.injail();
}
Related
I have Angular with ui-router, so toResolve variable will be resolved in my SomeController
.state('some.state', {
url: '/some',
controller: 'SomeController',
templateUrl: '/static/views/some-state.html',
resolve: {
toResolve: function(Resource) {
return Resource.get(function(response) {
return response;
});
},
But how to test this functionality with Jasmine? Let's suppose that I forget return statement, therefore the toResolve in my scope will be undefined.
Use services to make resolvers efficiently testable (and also mockable in integration/e2e tests).
Note: Angular services are singletons, state/route resolvers are not.
If caching a resolution is expected, a resolver may be moved to factory service.
app.factory('someResolver', function(Resource) {
return Resource.get(function(response) {
return response;
})
});
...
resolve: { toResolve: 'someResolver' },
On the other hand, if the resolver is expected to be evaluated on each route change, this may lead to undesirable app behaviour. In this case the appropriate recipe may be constant annotated function:
app.constant('someResolver', ['Resource', function(Resource) {
return Resource.get(function(response) {
return response;
})
}]);
app.config(function (someResolver, ...) {
...
resolve: { toResolve: someResolver },
...
Otherwise the specs may end encumbered with a pile of boilerplate code:
var toResolveFactory = $state.get('some.state').resolve.toResolve;
var toResolve = $injector.invoke(toResolveFactory);
One good article on related topic is http://nikas.praninskas.com/angular/2014/09/27/unit-testing-ui-router-configuration/, but it's not the solution. One idea is to pass another object to resolve and then test that unit of code separately which is good when you're resolving a bunch of items.
Lets say I have a an angular ui router route set up. When I change to that state, I'm telling Angular that I want it to resolve a factory call first then load the view. But what happens when that api call is empty? I would like to inform the user that there was no results found and stay on at my original state. Not transition to another view with no data to display. What is the best way to achieve this?
The route (which works as expected so far when I know there will be a return)
'use strict';
angular.module('testApp')
.config(function ($stateProvider) {
$stateProvider
.state('spinnerTest', {
url: '/spinner_test',
templateUrl: 'app/spinnerTest/spinnerTest.html',
controller: 'SpinnerTestCtrl',
resolve: {
names: function(NamesService){
//What happens if I return an empty array []?
//How do I return to the previous state?
NamesService.getNames();
}
}
});
});
You can simply reject promise in resolve in case of empty array:
resolve: {
names: function(NamesService) {
return NamesService.getNames().then(function(names) {
return names.length == 0 ? $q.reject('no names') : names;
});
}
}
This is a cross cutting concern, it is probably not unique to the Name service, but other services you are using as well.
Since you didn't post the code to the Name service (NameService service is redundant) I will assume it uses either the $http or $resource service. You can then use a $httpInterceptor that will trigger the display of a message to the user that "The selection is unavailable at this time".
You could call $state.go in your resolve, if you'd like
'use strict';
angular.module('testApp')
.config(function ($stateProvider) {
$stateProvider
.state('spinnerTest', {
url: '/spinner_test',
templateUrl: 'app/spinnerTest/spinnerTest.html',
controller: 'SpinnerTestCtrl',
resolve: {
names: function(NamesService, $state){
//What happens if I return an empty array []?
//How do I return to the previous state?
return NamesService.getNames().then(function(names){
if (!names.length) {
return $state.go('otherState');
}
return names;
});
}
}
});
});
I design my SPA like this:
angular.module('app', ['submodule0', 'submodule1']);
Main module:
$stateProvider.state("sub0index", {
url: "/sub0",
// pass states defined in submodule0, is that possible?
}).state("sub1index", {
url: "/sub1",
// pass states defined in submodule1
})
And here are some states defined in submodule0
$stateProvider.state("index", {
url: "/index",
templateUrl: "template/index.html"
}).state("info", {
url: "/info",
templateUrl: "template/info.html"
})
So is that possible that I pass sub-state from sub-module to the main module? I ask this because now I define all my state in my main module, I think it may be more elegant to define the state of one submodule in the submodule itself.
And another question is: I'm not sure my module design is reasonable or not, is my submodules not necessary? Or just keep my whole app logic to one module? Thanks.
====Edited====
And here is the problem I've met.
var app = angular.module('test', ['ui.router', 'app.sub']);
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a",
views: {
"general": {
templateUrl: "/template.html"
}
},
resolve: {
data: 'GetDataService'
}
});
}
The service GetDataService is defined in my submodule app.sub, and here is the service:
angular.module('app.sub',['ui.router'])
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null; // return null just for demo
}]);
The output of console.log($stateParams) is an empty object. But if use the service which is defined in its own module, the current state can be get correctly. So whats the issue?
===Edit===
Thanks for the example, it works fine if give a factory to data directly. But how about I give it a string?
I check the document of ui-router, and there is something about map object in resolve:
factory - {string|function}: If string then it is alias for service.
So if I use the code like this:
resolve: {
data: "GetDataService"
}
And the definition of GetDataService:
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}])
But output of console.log($stateParams) is always an empty object.
Do I have some misunderstanding about the api document?
===Edit again===
If I use code like this:
resolve: {
// data: "GetDataService"
data: ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}]
}
I can get the params object.
I would say, that modules should not stop us... we can split the app into many if needed.
But I would suggest: Services should be independent on $state.current. We should pass to them function parameters as needed, but these should be resolved outside of the Service body.
Bette would be to show it in action - there is one working example
This is the service:
angular.module('app.sub',['ui.router'])
.service('DataService', ['$state', function($state) {
return {
get: function(stateName, params){
console.log(stateName);
console.log(params);
return stateName;
}
}
}]);
And here is some adjsuted state def:
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a/{param1}",
views: {
"general": {
templateUrl: "tpl.html"
}
},
resolve: {
data: ['DataService','$stateParams'
, function(DataService,$stateParams, $state){
return DataService.get('index', $stateParams)
}],
},
});
}])
Hope it helps a bit. The plunker link
Because this approach is ready to test service without any dependency on some "external" $state.current. We can just pass dummy, testing params
I have an AngularJS service which communicates with the server and returns
translations of different sections of the application:
angular
.module('utils')
.service('Translations', ['$q','$http',function($q, $http) {
translationsService = {
get: function(section) {
if (!promise) {
var q = $q.defer();
promise = $http
.get(
'/api/translations',
{
section: section
})
.success(function(data,status,headers,config) {
q.resolve(result.data);
})
.error(function(data,status,headers,config){
q.reject(status);
});
return q.promise;
}
}
};
return translationsService;
}]);
The name of the section is passed as the section parameter of the get function.
I'm using AngularJS ui-router module and following design pattern described here
So I have the following states config:
angular.module('app')
.config(['$stateProvider', function($stateProvider) {
$stateProvider
.state('users', {
url: '/users',
resolve: {
translations: ['Translations',
function(Translations) {
return Translations.get('users');
}
]
},
templateUrl: '/app/users/list.html',
controller: 'usersController',
controllerAs: 'vm'
})
.state('shifts', {
url: '/shifts',
resolve: {
translations: ['Translations',
function(Translations) {
return Translations.get('shifts');
}
]
},
templateUrl: '/app/shifts/list.html',
controller: 'shiftsController',
controllerAs: 'vm'
})
This works fine but as you may notice I have to explicitly specify translations in the resolve parameter. I think that's not good enough as this duplicates the logic.
Is there any way to resolve translations globally and avoid the code duplicates. I mean some kind of middleware.
I was thinking about listening for the $stateChangeStart, then get translations specific to the new state and bind them to controllers, but I have not found the way to do it.
Any advice will be appreciated greatly.
Important note:
In my case the resolved translations object must contain the translations data, not service/factory/whatever.
Kind regards.
Let me show you my approach. There is a working plunker
Let's have a translation.json like this:
{
"home" : "trans for home",
"parent" : "trans for parent",
"parent.child" : "trans for child"
}
Now, let's introduce the super parent state root
$stateProvider
.state('root', {
abstract: true,
template: '<div ui-view=""></div>',
resolve: ['Translations'
, function(Translations){return Translations.loadAll();}]
});
This super root state is not having any url (not effecting any child url). Now, we will silently inject that into every state:
$stateProvider
.state('home', {
parent: 'root',
url: "/home",
templateUrl: 'tpl.html',
})
.state('parent', {
parent: 'root',
url: "/parent",
templateUrl: 'tpl.html',
})
As we can see, we use setting parent - and do not effect/extend the original state name.
The root state is loading the translations at one shot via new method loadAll():
.service('Translations', ['$http'
,function($http) {
translationsService = {
data : {},
loadAll : function(){
return $http
.get("translations.json")
.then(function(response){
this.data = response.data;
return this.data;
})
},
get: function(section) {
return data[section];
}
};
return translationsService;
}])
We do not need $q at all. Our super root state just resolves that once... via $http and loadAll() method. All these are now loaded, and we can even place that service into $rootScope:
.run(['$rootScope', '$state', '$stateParams', 'Translations',
function ($rootScope, $state, $stateParams, Translations) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.Translations = Translations;
}])
And we can access it anyhwere like this:
<h5>Translation</h5>
<pre>{{Translations.get($state.current.name) | json}}</pre>
Wow... that is solution profiting almost from each feature coming with UI-Router... I'd say. All loaded once. All inherited because of $rootScope and view inheritance... all available in any child state...
Check that all here.
Though this is a very old question, I'd like to post solution which I'm using now. Hope it will help somebody in the future.
After using some different approaches I came up with a beautiful angularjs pattern by John Papa
He suggest using a special service routerHelperProvider and configure states as a regular JS object. I'm not going to copy-paste the entire provider here. See the link above for details. But I'm going to show how I solved my problem by the means of that service.
Here is the part of code of that provider which takes the JS object and transforms it to the states configuration:
function configureStates(states, otherwisePath) {
states.forEach(function(state) {
$stateProvider.state(state.state, state.config);
});
I transformed it as follows:
function configureStates(states, otherwisePath) {
states.forEach(function(state) {
var resolveAlways = {
translations: ['Translations', function(Translations) {
if (state.translationCategory) {
return Translations.get(state.translationCategory);
} else {
return {};
}
}],
};
state.config.resolve =
angular.extend(state.config.resolve || {}, resolveAlways || {});
$stateProvider.state(state.state, state.config);
});
});
And my route configuration object now looks as follows:
{
state: ‘users’,
translationsCategory: ‘users’,
config: {
controller: ‘usersController’
controllerAs: ‘vm’,
url: ‘/users’.
templateUrl: ‘users.html'
}
So what I did:
I implemented the resolveAlways object which takes the custom translationsCategory property, injects the Translations service and resolves the necessary data. Now no need to do it everytime.
I got a routeProvider for my states.
$routeProvider.
when("/register",{
templateUrl: "templates/register.html",
controller: "RegisterCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}).
when("/home",{
templateUrl: "templates/home.html",
controller: "HomeCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}). .... [.....]
Every state got a promise which resolves, when user-state is loggedIn. Then the code of the different controllers is executed. Now I want to have a mainController for the navigation bar, which should be present on all sites. The controller needs the userdata for checking for new messages etc.
Now: how is it possible to define the resolve globally in a root state (so i can access the userdata in the root controller for all sites) and all the other controllers execute their code only, if the promise from this roote state is resolved?
I hope I formulated my question understandable...
I think you're looking for something like $routeChangeStart, that is a way to execute something you want everytime the user changes his route inside your web app. Take a look at Route and this other question from stackoverflow. Hope it helps.
You can do this by defining your routes outside of the $routeProvider.when statements:
var routes = [
{
url: "/register",
config: {
templateUrl: "templates/register.html",
controller: "RegisterCtrl"
}
},
{
url: "/home",
config: {
templateUrl: "templates/home.html",
controller: "HomeCtrl"
}
}
];
Then iterating through your routes to extend the resolve property before registering them with the $routeProvider:
angular.forEach(routes, function (route) {
var url = route.url;
var routeConfig = route.config;
routeConfig.resolve = angular.extend(routeConfig.resolve || {}, {
// add your global resolves here
user: function(Auth) {
return Auth.resolveUser();
}
});
$routeProvider.when(url, routeConfig);
});
Your Auth.resolveUser() should be responsible for returning the fulfilled promise if it was already resolved previously.