I have this state:
.state('admin.category',{
url: '/category',
templateUrl:'views/admin.category.html',
resolve:{
category: ['CategoryLoader', function(CategoryLoader){
return new CategoryLoader();
}]
},
})
This is my service which I resolve.
.factory('CategoryLoader',['Category', '$state', '$q',
//console.log($state)
function(Category, $state, $q){
return function(){
var delay = $q.defer();
Category.get({cat_id:$state.params.id}, //not working
function(category){
delay.resolve(category);
},
function(){
//delay.reject('Unable to fetch category ' + $state.params.cat_id)
});
return delay.promise;
}
}]);
Everything works if I change $state.params.id to a number. If I console the $state in my service, I get everything, including params. But I can't seem to be using it. It should be equiliant to use $route.current.params.id, which I've used in other projects. How do I do the same thing with ui-router?
Update: some more info
Parent state:
.state('admin',{
abstract: true,
url: '/admin',
templateUrl:'views/admin.html'
})
Category factory:
factory('Category', function($resource){
return $resource('/api/category/byId/:id/', {id: '#id'});
})
I'll put together a fiddle if necassary
The issue I see here is that you are trying to access the $stateParams before the state is ready. There are several events that happen when changing states.
First is $stateChangeStart where you are notified of the state you are changing to and coming from as well as all of the params in both states.
After this, the resolve starts to happen. The resolve in your case is calling a function that uses $stateParams
After everything is resolved and good (no rejections or errors), and just prior to the $stateChangeSuccess event, the state params are updated in the $stateParams object.
Basically, you cannot access the params for the state you are changing to via the $stateParams object until after the state change is completed. This is done in case the state change is rejected and cannot be changed. In this case, the previous state remains and the previous state's params will be in $stateParams.
As a quick work-around, you can use the $stateChangeStart event to access the toParams (the params for the state you are changing to) and put them somewhere ($rootScope, or a service) so you can access them when you need them.
Here is a quick example with $rootScope
.run(['$rootScope', function($rootScope){
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
$rootScope.toParams = toParams;
});
}]);
.factory('CategoryLoader',['$rootScope', 'Category', '$q',
function($rootScope, Category, $q){
return function(){
var delay = $q.defer();
Category.get({cat_id: $rootScope.toParams.id}, // <-- notice its using the object we put in $rootScope, not $state or $stateParams
function(category){
delay.resolve(category);
},
function(){
//delay.reject('Unable to fetch category ' + $state.params.cat_id)
});
return delay.promise;
}
}]);
Also, I believe your resource is not setup to correctly use the id you are passing. You were using cat_id above, in order to link it to the :id of the URL, you would have to map it as `{id: '#cat_id'}
factory('Category', function($resource){
return $resource('/api/category/byId/:id/', {id: '#cat_id'});
});
console.log is by reference and asynchronous
If you see it in the console it's because the state already became activated.
resolve happens before a state is activated , so $state.current.params is still not available at that phase.
See: Javascript unexpected console output with array assignment;
UPDATE
I looked inside the source code, It looks like $stateParams that is injected into resolve functions is a local copy and not the global $stateParams. The global $stateParams is also updated only after the state is activated.
Also the state URL should contain the parameter: url: '/category/:id',
From $stateParams Service docs:
$stateParams Service:
As you saw previously the $stateParams service is an object that will have one key per url parameter. The $stateParams is a perfect way to provide your controllers or other services with the individual parts of the navigated url.
Note: $stateParams service must be specified as a state controller, and it will be scoped
so only the relevant parameters defined in that state are available on the service object.
So I guess you must do something like this:
State:
.state('admin.category',{
url: '/category',
templateUrl:'views/admin.category.html',
resolve:{
category: ['CategoryLoader','$stateParams', function(CategoryLoader, $stateParams){
return CategoryLoader($stateParams);
}]
},
})
Factory:
.factory('CategoryLoader',['Category', '$q',
function(Category, $q) {
return function($stateParams){
var delay = $q.defer();
Category.get({cat_id:$stateParams.id},
function(category){
delay.resolve(category);
},
function(){
//delay.reject('Unable to fetch category ' + $stateParams.cat_id)
});
return delay.promise;
}
}]);
Related
Given the following state in ui-router:
.state('some.state', {
url: '/some/:viewType',
templateUrl: 'myTemplate.html',
controller: 'SomeStateController',
controllerAs: 'vm',
data: {
authorizedFor: [SOME_ROLE]
}
}
I'm trying to use the "data" object for a state to help control access to authorized states. Separately, I handle the $stateChangeStart event to look at data.authorizedFor and act accordingly.
The problem, though, is that the list of authorized roles might change based on the value of :viewType. I thought I could let data:{} be a function, inject $stateParams, and handle the logic there...but that won't do.
So, I tried using the params object instead, but at the $stateChangeStart time, the :viewType is not yet accessible from $state.params or $stateParams.
Stepping through in dev tools, I noticed that $state.transitionTo.arguments is populated, but it seems awfully hacky to go that route.
params: {
authorizedFor: function($state) {
console.log($state.transitionTo.arguments[1].viewType); // has value I need
}
}
Any suggestions?
My suggestion is to use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
for example:
$stateProvider
.state('profile', {
url: '/profile',
templateUrl: 'profile.html',
resolve:{
'ProfileService': function(ProfileService){
return ProfileService.promise_skillRecommendation_mock;
}
}
})
The profileService code:
var app = angular.module('app').service("ProfileService", function($http){
var myData = null;
var promise_skillRecommendation_mock =
$http.get('Mock/skillRecommendation-mock.json')
.success(function(data){
myData = data;
});
return{
promise_skillRecommendation_mock: promise_skillRecommendation_mock,
get_skillRecommendation: function(){
return myData;
}
};
});
and the controller code which will use this service is:
angular.module('app').controller('ProfileController', function($scope, $http, ProfileService){
$scope.skillRecommendation = ProfileService.get_skillRecommendation();
The object in resolve below must be resolved (via deferred.resolve() if they are a promise) before the controller is instantiated. Notice how each resolve object is injected as a parameter into the controller.
by using this code, the page will be displayed only after that the promise will be resolved.
for more info please view this page: https://github.com/angular-ui/ui-router/wiki
I am getting unexpected results from both methods.
I have my $state configed
$stateProvider
.state('status', {
url: "/status/:payment",
controller: 'QuestCtrl',
templateUrl: "index.html"
});
And on the Controller I have:
angular.module('quest').controller('QuestCtrl',function($scope,$stateParams,$state){
console.log($stateParams.payment); // undefined
console.log($state); // Object {params: Object, current: Object, $current: extend, transition: null}
}
I already used $stateParams in other projects and it worked but now I can't figure out what is going on here..
['$scope','$stateParams','$state',
function($scope, $http, $stateParams, $state)
The names of the services don't match with the variables.
So $http is actually the $stateParams service, $stateParams is actually the $state service, and $state is undefined.
My advice: stop using this array notation, which clutters the code and is a frequent source of bugs. Instead, use ng-annotate as part of the build procedure, which will do it, correctly, for you.
As I already commented above You forgot to inject $http service
angular.module('quest').controller('QuestCtrl',
['$scope','$http','$stateParams','$state',function($scope,$http,$stateParams,$state){
console.log($stateParams); // Empty Object
console.log($stateParams.payment); // Empty Object
console.log($state); // I get the entire state, I can see that my params are there.
console.log($state.params);
}
So your parameters mismatch and it turns out you will get $state in $stateparms and $state is empty.
And $http hold $state :P
Hope it helps :)
With the ng-annotate library, the controller can be also initiated like this:
angular.module('quest')
.controller('QuestCtrl', function ($scope,$http,$stateParams,$state) {
});
In this case you are avoiding problems with the injected objects ordering. Look at: https://github.com/olov/ng-annotate
If you are building your application with Grunt, use: grunt-ng-annotate package.
Missing parameter in routes.js
My example:
.state('menu.cadastroDisplay', {
url: '/page9',
views: {
'side-menu21': {
templateUrl: 'templates/cadastroDisplay.html',
controller: 'cadastroDisplayCtrl'
}
},
params: { 'display': {} }
})
Without this params in routes the $stateParams.yourParam always returns undefined.
Call 'ngInject'
constructor($scope, $reactive, $stateParams, $state, $sce) {
'ngInject'
I'm making an app using the angular ui router.
When navigating to a particular state, which always has parameters passed to it, I need to be able to change the url and update the parameters without reloading the view.
In my state config I'm using:
reloadOnSearch: false
This works and allows me to update the parameters in the URL without page reload.
However, I need to be notified of these changes on the $stateParams so I can run various methods that will do things with the new data.
Any suggestions?
Thanks, James
This is relevant to me so I did a bit of work.
Here is my state that uses reloadOnSearch but fires off a function each time the URL is updated.
.state('route2.list', {
url: "/list/:listId",
templateUrl: "route2.list.html",
reloadOnSearch: false,
controller: function($scope, $stateParams){
console.log($stateParams);
$scope.listId = $stateParams.listId;
$scope.things = ["A", "Set", "Of", "Things"];
//This will fire off every time you update the URL.
$scope.$on('$locationChangeSuccess', function(event) {
$scope.listId = 22;
console.log("Update");
console.log($location.url());
console.log($stateParams);
});
}
}
Returns in console:
Update
/route2/list/7 (index):70
Object {listId: "8"} (index):71
$locationChangeStart and $locationChangeSuccess should get you where you want to go. Keep in mind that your location functions will always fire off, even on the first load.
Inspiration taken from this SO post and the $location documentation
EDIT 2
According to the Angular docs for ngRoute, $routeParams aren't updated until after the success of the changes. Chances are that ui-route has a similar restriction. Since we're inturrupting the resolution of the state, $stateParams is never updated. What you can do is just emulate how $stateParams are passed in the first place using $urlMatcherFactory.
Like so:
.state('route2.list', {
url: "/list/:listId",
templateUrl: "route2.list.html",
reloadOnSearch: false,
controller: function($scope, $stateParams, $state, $urlMatcherFactory){
console.log($stateParams.listId + " :: " + $location.url());
$scope.listId = $stateParams.listId;
$scope.things = ["A", "Set", "Of", "Things"];
$scope.$on('$locationChangeSuccess', function(event) {
$scope.listId = 22;
//For when you want to dynamically assign arguments for .compile
console.log($state.get($state.current).url); //returns /list/:listId
//plain text for the argument for clarity
var urlMatcher = $urlMatcherFactory.compile("/route2/list/:listId");
//below returns Object {listId: "11" when url is "/list/11"}
console.log(urlMatcher.exec($location.url()));
var matched = urlMatcher.exec($location.url());
//this line will be wrong unless we set $stateParams = matched
console.log("Update " + $stateParams.listId);
console.log($location.url());
//properly updates scope.
$scope.listId = matched.listId;
});
}
})
The scope actually updates, so that's a good thing. The thing to remember is that you're stoppng the normal resolution of everything but setting reloadOnSearch to false so you'll need to handle pretty much everything on your own.
Following on from Romans code, i built a service that returns the state params of of the current state
/**
* Get parameters from URL and return them as an object like in ui-router
*/
eventaApp.service( 'getStateParams', ['$urlMatcherFactory', '$location', '$state', function( $urlMatcherFactory, $location, $state ){
return function() {
var urlMatcher = $urlMatcherFactory.compile($state.current.url);
return urlMatcher.exec($location.url());
}
}]);
With ui-router, it's possible to inject either $state or $stateParams into a controller to get access to parameters in the URL. However, accessing parameters through $stateParams only exposes parameters belonging to the state managed by the controller that accesses it, and its parent states, while $state.params has all parameters, including those in any child states.
Given the following code, if we directly load the URL http://path/1/paramA/paramB, this is how it goes when the controllers load:
$stateProvider.state('a', {
url: 'path/:id/:anotherParam/',
controller: 'ACtrl',
});
$stateProvider.state('a.b', {
url: '/:yetAnotherParam',
controller: 'ABCtrl',
});
module.controller('ACtrl', function($stateParams, $state) {
$state.params; // has id, anotherParam, and yetAnotherParam
$stateParams; // has id and anotherParam
}
module.controller('ABCtrl', function($stateParams, $state) {
$state.params; // has id, anotherParam, and yetAnotherParam
$stateParams; // has id, anotherParam, and yetAnotherParam
}
The question is, why the difference? And are there best practices guidelines around when and why you should use, or avoid using either of them?
The documentation reiterates your findings here: https://github.com/angular-ui/ui-router/wiki/URL-Routing#stateparams-service
If my memory serves, $stateParams was introduced later than the original $state.params, and seems to be a simple helper injector to avoid continuously writing $state.params.
I doubt there are any best practice guidelines, but context wins out for me. If you simply want access to the params received into the url, then use $stateParams. If you want to know something more complex about the state itself, use $state.
Another reason to use $state.params is for non-URL based state, which (to my mind) is woefully underdocumented and very powerful.
I just discovered this while googling about how to pass state without having to expose it in the URL and answered a question elsewhere on SO.
Basically, it allows this sort of syntax:
<a ui-sref="toState(thingy)" class="list-group-item" ng-repeat="thingy in thingies">{{ thingy.referer }}</a>
EDIT: This answer is correct for version 0.2.10. As #Alexander Vasilyev pointed out it doesn't work in version 0.2.14.
Another reason to use $state.params is when you need to extract query parameters like this:
$stateProvider.state('a', {
url: 'path/:id/:anotherParam/?yetAnotherParam',
controller: 'ACtrl',
});
module.controller('ACtrl', function($stateParams, $state) {
$state.params; // has id, anotherParam, and yetAnotherParam
$stateParams; // has id and anotherParam
}
There are many differences between these two. But while working practically I have found that using $state.params better. When you use more and more parameters this might be confusing to maintain in $stateParams. where if we use multiple params which are not URL param $state is very useful
.state('shopping-request', {
url: '/shopping-request/{cartId}',
data: {requireLogin: true},
params : {role: null},
views: {
'': {templateUrl: 'views/templates/main.tpl.html', controller: "ShoppingRequestCtrl"},
'body#shopping-request': {templateUrl: 'views/shops/shopping-request.html'},
'footer#shopping-request': {templateUrl: 'views/templates/footer.tpl.html'},
'header#shopping-request': {templateUrl: 'views/templates/header.tpl.html'}
}
})
I have a root state which resolves sth. Passing $state as a resolve parameter won't guarantee the availability for $state.params. But using $stateParams will.
var rootState = {
name: 'root',
url: '/:stubCompanyId',
abstract: true,
...
};
// case 1:
rootState.resolve = {
authInit: ['AuthenticationService', '$state', function (AuthenticationService, $state) {
console.log('rootState.resolve', $state.params);
return AuthenticationService.init($state.params);
}]
};
// output:
// rootState.resolve Object {}
// case 2:
rootState.resolve = {
authInit: ['AuthenticationService', '$stateParams', function (AuthenticationService, $stateParams) {
console.log('rootState.resolve', $stateParams);
return AuthenticationService.init($stateParams);
}]
};
// output:
// rootState.resolve Object {stubCompanyId:...}
Using "angular": "~1.4.0", "angular-ui-router": "~0.2.15"
An interesting observation I made while passing previous state params from one route to another is that $stateParams gets hoisted and overwrites the previous route's state params that were passed with the current state params, but using $state.params doesn't.
When using $stateParams:
var stateParams = {};
stateParams.nextParams = $stateParams; //{item_id:123}
stateParams.next = $state.current.name;
$state.go('app.login', stateParams);
//$stateParams.nextParams on app.login is now:
//{next:'app.details', nextParams:{next:'app.details'}}
When using $state.params:
var stateParams = {};
stateParams.nextParams = $state.params; //{item_id:123}
stateParams.next = $state.current.name;
$state.go('app.login', stateParams);
//$stateParams.nextParams on app.login is now:
//{next:'app.details', nextParams:{item_id:123}}
Here in this article is clearly explained: The $state service provides a number of useful methods for manipulating the state as well as pertinent data on the current state. The current state parameters are accessible on the $state service at the params key. The $stateParams service returns this very same object. Hence, the $stateParams service is strictly a convenience service to quickly access the params object on the $state service.
As such, no controller should ever inject both the $state service and its convenience service, $stateParams. If the $state is being injected just to access the current parameters, the controller should be rewritten to inject $stateParams instead.
I'm not sure if the way I am doing it is correct, any advice would be appreciated.
I have a Restaurant Selector, which the user can select a restaurant from. Then all other child states load up content specific to the restaurant selected. But I need to have a child state selected by default, including a restaurant, which will be either selected based on the users closest location or on cookie data if they have previously selected one. But, i'm not sure how I can redirect to a child state by default without knowing the restaurant id already?
A bastardised version of my code below to illustrate.
app.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/restaurant/[some id based on cookie info]/our-food"); // if no id is set, then I want to get it from a cookie, but then how do i do the redirect?
$stateProvider
.state('restaurant', {
url: '/restaurant/:restaurantId',
template: "<ui-view/>",
controller: function($state, $stateParams, Data, RestaurantService, $cookieStore) {
if(typeof $stateParams.restaurantId !== 'undefined') {
// get the restaurantID from the cookie
}
}
})
.state('restaurant.our-food', {
url: '/our-food',
templateUrl: function($stateParams) {
return 'templates/food-food.html?restaurantId=' + $stateParams.restaurantId;
},
controller: 'SubNavCtrl'
})
.state('restaurant.glutenfree-vegetarian', {
url: '/glutenfree-vegetarian',
templateUrl: function($stateParams) {
return 'templates/food-vegetarian.html?restaurantId=' + $stateParams.restaurantId;
},
controller: 'SubNavCtrl'
})
An image below to illustrate what is happening on the front end:
www.merrywidowswine.com/ss.jpg
I would create an event that is fired every time you open that specific state.
Check out their doc: https://github.com/angular-ui/ui-router/wiki#onenter-and-onexit-callbacks
So my guess is either something like this
1. onEnter callback to restaurant state (recommended)
$stateProvider.state("contacts", {
template: '<ui-view>',
resolve: ...,
controller: function($scope, title){
},
onEnter: function(){
if(paramNotSet){ $state.go(...)}
}
});
I've never used an event as such myself so you might have to do some gymnastics with resolve, but I believe this is the cleanest, easiest to understand and most maintainable solution.
2 Global onStateChangeStart event
A global event (although this would get fired for every state change)
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){ ... //check cookie, change state etc ...})
3 In the controller
Alternatively If you want to use the controller like you started doing.
controller: ['$state', '$stateParams', 'Data', 'RestaurantService', '$cookieStore',
function($state, $stateParams, Data, RestaurantService, $cookieStore) {
if(typeof $stateParams.restaurantId !== 'undefined') {
sate.go('restaurant', $cookieStore['restaurant'])
}
}]
This is probably the fastest solution in terms of development but I believe using events is cleaner and makes for more understandable code.
Note: I haven't actually run any of this code so it might not work, it's just pseudo-code to give you an idea of where to go. Let me know if you run into issues.
EDIT: Acutally I'm not sure if stateParams are passed on to the controller. You might have to use resolve to access them.
EDIT: to access stateParams in onEnter callback or the controller, I believe you have to use resolve as such:
resolve: {
checkParam: ['$state','$stateParams', '$cookieStore', function($state, $stateParams, $cookieStore) {
//logic in here, what it returns can be accessed in callback or controller.
}]
see the ui-router doc on resolve for more examples/better explanation
I needed to get this working, so to add details to the answer from #NicolasMoise:
.state('restaurant', {
url: '/restaurant/:restaurantId',
template: "<ui-view/>",
onEnter: function ($state, $stateParams, $cookies) {
console.log($stateParams.restaurantId);
if (!$stateParams.restaurantId) {
$stateParams.restaurantId = $cookies.restaurantId;
$state.go('restaurant.restaurantID');
}
},
})
I didn't test the cookies portion of this, but the conditional and state change worked.