angular ui-router: get $state info of toState in resolve - javascript

Update: this should be possible in angular-ui-router as of 1.0.0alpha0. See the release notes https://github.com/angular-ui/ui-router/releases/tag/1.0.0alpha0 and the issue https://github.com/angular-ui/ui-router/issues/1018 I created.
I would like to access the state's name and other attributes the app is navigating to using angular ui-router when working on the resolve.
The reason: I want load some user data (including their access rights) asynchronously before allowing the app the enter that page.
Currently this is not possible because injecting $state into the resolve points to the state you're navigating away form, not to the one you're navigating to.
I know I can:
get the toState somewhere else with $rootScope('$stateChangeStart') and save it in my settings service for instance. But I think it's a little messy.
hard code the state into the resolve, but I don't want to reuse my resolve for all pages
I also created an issue on the ui-router github (Please + 1 if you are interested!):
https://github.com/angular-ui/ui-router/issues/1018
Here's my code so far. Any help appreciated!
.config(function($stateProvider) {
$stateProvider.state('somePage', {
// ..
resolve: {
userData: function($stateParams, $state, Settings) {
return Settings.getUserData() // load user data asynchronously
.then(function (userData) {
console.log($stateParams);
console.log($state);
// Problem: $state still points to the state you're navigating away from
});
}
}
});
});

Update for Ui-Router 1.x
$provide.decorator('$state', ($delegate, $transitions) => {
$transitions.onStart({}, (trans) => {
$delegate.toParams = trans.params()
$delegate.next = trans.to().name
})
return $delegate
})
Ui-Router 0.x
You can always decorate $state with next and toParams properties:
angular.config(function($provide) {
$provide.decorator('$state', function($delegate, $rootScope) {
$rootScope.$on('$stateChangeStart', function(event, state, params) {
$delegate.next = state;
$delegate.toParams = params;
});
return $delegate;
});
});
And use as such:
.state('myState', {
url: '/something/{id}',
resolve: {
oneThing: function($state) {
console.log($state.toParams, $state.next);
}
}
});

So I discovered the answer to this myself. If you're code is behaving like mine, the $stateParams object is properly injected, but $state is an empty (or old) state object.
What worked for me was referencing this in the resolve function:
.state('myState', {
url: '/something/{id}',
templateUrl: '/myTemplate.html',
controller: function() {},
resolve: {
oneThing: function($stateParams) {
console.log($stateParams); // comes through fine
var state = this;
console.log(state); // will give you a "raw" state object
}
}
})
The first log will return what you'd expect. The second log will return a "raw" (for lack of a better term) state object. So, for instance, to get the state's name, you can access, this.self.name.
I realize this isn't preferred...it would be a lot nicer if $state (or another standardized object) could provide this information for us at the resolve, but this is the best I could find.
Hope that helps...

this.toString() will give you the state name

This has been asked here.
It looks like they built into 1.0.0-rc.2 $state$ which you can inject into the resolve function and get this information.
resolve: {
oneThing: function($state$) {
console.log($state$);
}
}

Related

AngularJS - href redirect to same domain cause page display before data are ready

I'm using Angular 1.4.1.
I made a directive, a generic card with thumbnail and a link. The link is a generic absolute url, but usually lead to somewhere inside the app.
So assuming I am in https://stackoverflow.com/section/ there could be a card with a link like https://stackoverflow.com/section/detail
I have an app.js similar to this (I just post the interested route section)
.state('base.section', {
url: 'section/',
views: {
'#': {
templateUrl: 'views/section.html',
controller: 'SectionController'
}
},
resolve: {
'items': function (SomeService) {
console.log("items resolve triggered");
return SomeService.doThingsReturnPromise();
}
}
}
.state('base.section.detail', {
url: 'detail',
views: {
'#': {
templateUrl: 'views/detail.html',
controller: 'DetailController'
}
},
resolve: {
'items': function (items) {
return items;
},
'stuff': function(SomeOtherService) {
console.log("stuff resolve triggered.");
return SomeOtherService.doThingsReturnPromise();
}
}
}
So the problem is that when I click
<a ng-href="https://stackoverflow.com/section/detail">
I get correct redirected to the detail page, but before the promise in the resolve is fully resolved and I gets all angular like variables displayed ( {{ somethingLikeThis }} ).
Even if resolve gets triggered, (I can see the log in the console) seems just that Angular doesn't wait till the resolve is complete before loading the controller, as it should behave.
Of course, if I reload the detail page, I get all the values fully loaded. The same if I move to another site section (https://stackoverflow.com/baloons).
The problem seems occur just when I move from "section" to "detail", from the same running app
Hope to have provided a good problem explanation
UPDATE
I solved the problem, but to better understand I want to specify the services functions. They're something like this
angular.module('myApp')
.factory('SomeService',
function ($resource, serverURL, protocol, requestFormat) {
return $resource(serverURL + 'backendUrl/',
{
callback: 'JSON_CALLBACK',
format: requestFormat
},
{
doThingsReturnPromise: {
method: protocol,
isArray: true
}
});
});
just for completeness, the problem turned out even using $q.all(promises) in the resolve
The problem solved using .$promise in the resolve (no pun intended)
resolve: {
'items': function (SomeService) {
console.log("items resolve triggered");
return SomeService.doThingsReturnPromise().$promise;
}
I had the same problem with $q.all and the solution was the same, just slightly different
resolve: {
'items': function (SomeService, SomeOtherService, $q) {
var promises = [
SomeService.doThingsReturnPromise().$promise,
SomeOtherService.doThingsReturnPromise().$promise
];
return $q.all(promises);
}
}
$q.all return a promise so it works without specifying .$promise, but the services requests have to

Resolve a function and use its side effect to record data in a service

I am making a web page with ui-router. Before we enter the controller, I want some operations to be done: 1) create a temporary folder in the server and write some files; 2) record the name of the folder and some other data. So naturally I choose to use resolve.
.state('panels', {
controller: 'PanelsCtrl',
resolve: {
init: ['codeService', function (codeService) {
return codeService.init()
}]
},
...
});
app.service('codeService', ['$http', function ($http) {
var srcP = "default" // private variable
this.getSrcP = function () { return srcP };
this.init = function () {
return $http.post("/writeFiles", ...) // write files in a temporary folder of the server
.then(function (res) {
srcP = res.data;
return srcP
}
}
};
app.controller('PanelsCtrl', ['$scope', 'codeService', 'init', function($scope, codeService, init) {
$scope.src = codeService.getSrcP();
...
}
The above code works. However, I feel odd about 1) I resolve an asynchronous function (ie, init) rather than data (that people usually resolve); 2) I use a side effect of init to record data (ie, srcP) in a service.
It seems that, in comparaison with resolving data, it is easier when we have more data to be recorded, we just need to have more private variables and make more side effects in the service.
Does anyone know if what I do is a bad/common/good practice? Additionally, why codeService in resolve: {...} and codeService injected to PanelsCtrl share same private variables?
I think that's a better approach if you clean up the service and don't resolve the promise inside it. You can let UI-ROUTER to resolve the promise and inject the result data in the controller...
.state('panels', {
controller: 'PanelsCtrl',
resolve: {
init: ['codeService', function (codeService) {
return codeService.init()
}]
}
});
app.service('codeService', ['$http', function ($http) {
this.init = function () {
return $http.post("/writeFiles");
}
};
app.controller('PanelsCtrl', ['$scope', 'init', function($scope, init) {
$scope.src = init; //init is the resolved data of codeService...
}
I've just made an example. Check this jsFiddle.
I think theoretically there's nothing wrong causing side effects in resolve, that's what it's there for. Take Restangular as an example. You keep calling for resources in resolve, the cacheing is handled by Restangular, which is basically a side effect.
However, I see some problems with the server-side approach. Storing files on the server is usually a bad idea (think about scaling/compromised immutability of the infrastructure). So I'd rather utilize the DB for this, and you could turn your folder abstraction to a DB resource.

Test Angular resolve method

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.

Prevent route from loading until data is available in Angular

In the app I'm working on there is a situation in which data is pulled from a JSON file and is referenced in all subsequent routes. I want to ensure that a route does not load until this data is available, and if not available request it before loading the route. I'm trying to use a route resolve to accomplish this but am finding that the route will load regardless since the request to get the data returns a promise in the resolve. Here's an example of how the code for this is set up, is there a way to not load the route until the promise is resolved? I think the use of promises are throwing me off some.
Factory which pulls the data from the JSON file.
.factory('jsonFactory', function($q, $http) {
return {
getFormStuff: function() {
var deferred = $q.defer(),
httpPromise = $http.get('json/MasterObject.json');
httpPromise.then(function(response) {
deferred.resolve(response);
}, function(error) {
console.log(error);
});
return deferred.promise;
}
};
})
ngRoute config with resolve that checks if Model.formStuff is available and if not attempts to get it before loading the route which needs the data.
app.config(function ($routeProvider) {
$routeProvider.when('/someRoute', {
controller: 'someController',
templateUrl: 'views/someView.html',
resolve: {
getFormTemplate: function (Model, jsonFactory) {
if (!Model.formStuff) {
Model.formStuff = jsonFactory.getFormStuff();
return Model.formStuff;
} else {
return Model.formStuff;
}
}
}
})
EDIT: Adding the Model factory and controller where Model.formStuff is referenced. The Model.formStuff is dynamically added in a different controller and is not a pre-defined property...I inherited this code so not sure why it is handled like that.
angular.module('example', [])
.factory('Model', ['$resource',
function($resource) {
return {
query: function() {
return data;
},
get: function(id) {
return findById(id);
},
set: function(item) {
addItem(item);
},
put: function(item) {
updateItem(item);
},
del: function(id) {
removeItem(id);
},
getLoginUser: function(id) {
removeItem(id);
},
islogin: false
};
}
])
basic controller example showing how Model.formStuff is normally used.
angular.module(...)
.controller("someController", function(Model) {
$scope.someField = Model.formStuff[0].someProp;
var someVar = Model.formStuff.[0].otherProp;
// bunch of other code...
});
The code doesn't look that wrong. Please be sure to also handle the error case, otherwise the promise you return will never be rejected and the router will wait forever in case of some error. So you should call deferred.reject(error) in your error callback.
If you don't need any special processing on the data, you could directly return the promise of the $http.get() call like so:
getFormStuff = function() {
return $http.get('json/MasterObject.json');
}
What could possibly be the problem in your case is the controller definition. Do you inject a value named like the key in your resolve object into the controller? Otherwise the data will not be passed there...
angular.module(...)
.controller("someController", function(getFormTemplate) {
// do anything with the resolved data...
});
Another suggestion: Instead of handling the caching stuff directly in the resolve block, why not creating a special service that caches the data and just resolving like so:
resolve: {
getFormTemplate: function (MyCachingModel) {
return MyCachingModel.promiseToTemplate()
}
}
... and then moving the current logic into this caching service. This makes reasoning much clearer as your logic is not spread into the router config.

How do I deal with Angular-UI-Router resolve with an unfulfilled promise?

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;
});
}
}
});
});

Categories