Refresh ui-router view when state parameters change? - angularjs

What's the correct way to update a ui-router view when state parameters change?
For example, if I've got a state like:
.state("page.view", {
url: "/pages/:slug",
views: {
"": {
controller: "PageCtrl",
templateUrl: "page-view.html",
},
},
})
And an (incorrect) controller which looks like this:
.controller("PageCtrl", function($scope, $state) {
$scope.page = loadPageFromSlug($state.params.slug);
})
How can I correctly load a new $scope.page when the $state.slug changes?
Note that the above does not work when moving from page to another because the controller is only run once, when the first page loads.

I would do something like this:
.controller("PageCtrl", function($scope, $state) {
$scope.$on("$stateChangeSuccess", function updatePage() {
$scope.page = $state.params.slug;
});
});
I'd be curious if you find a better way - there may be some way to just watch the value of the state slug, but this is clean and clearly articulates what it is that you're watching for.

I am really not fully sure, if I do not miss something here - but, based on the snippets shown in your question:
PageCtrl is related to state "page.view" and will be run as many times as "page.view" state is triggered
"page.view" state has declared param slug - url: "/pages/:slug",, which will trigger state change - whenever it is changed
If the above is true (if I do not oversee something) we can use stateConfig setting - resolve
there is no need to use $state.params. We can use $stateParams (more UI-Router way I'd personally say)
Well if all that is correct, as shown in this working plunker, we can do it like this
resolver:
var slugResolver = ['$stateParams', '$http'
, function resolveSlug($stateParams, $http){
return $http
.get("slugs.json")
.then(function(response){
var index = $stateParams.slug;
return response.data[index];
});
}];
Adjusted state def:
.state("page.view", {
url: "/pages/:slug",
views: {
"": {
controller: "PageCtrl",
templateUrl: "page-view.html",
resolve: { slug : slugResolver },
},
},
})
And the PageCtrl:
.controller('PageCtrl', function($scope,slug) {
$scope.slug = slug;
})
Check it all in action here

I had this problem in ui-router 0.2.14. After upgrading to 0.2.18 a parameter change does fire the expected $stateChange* events.

Related

$stateParams Query parameter undefined

I am trying to get the query parameters from my angular UI router application. However I cannot seem to get them when I use the query method. The state params are always coming through as undefined.
$stateProvider.state("message",
{
views: {
"main-view": {
templateUrl: "admin/templates/message.html",
controller: ["$scope", "$stateParams", function ($scope, $stateParams )
{
// Get the params
$scope.message = $stateParams.message; // undefined
$scope.error = $stateParams.status; // undefined
}]
}
},
url: "/admin/message?message&status"
})
}
The URL i am using to access this state is:
http://localhost:8080/admin/message?message=hello&status=error
However if I use the Basic Parameters method it works fine. I.e:
url: "/admin/message/:message/:status"
...
http://localhost:8080/admin/message/hello/error
I have html5 mode activated (not sure if that affects it?)
My code has been simplified for the above - am I doing something wrong here? Have I forgot to include anything?
Thanks

How to access state params (ui-sref) without including them in URL propery of state in angularjs

This question is might be similar to this but with different requirements. As I was not able to make comment (required 50 points) I am replicating the question.
I want to simply access the parameters sent from ui-sref in template inside the controller without mentioning them in state URL .
Something like using the link below for transitioning the state to home with foo and bar parameters:
<a ui-sref="home({foo: 'fooVal', bar: 'barVal'})">Go to home state with foo and bar parameters </a>
Receiving foo and bar values in a controller:
state('home', {
url: '/:foo',
views: {
'***whatIsThis***': {
templateUrl: 'home.html',
controller: 'MainRootCtrl'
},
app.controller('SomeController', function($scope, $stateParam) {
//..
var foo = $stateParam.foo; //getting fooVal
var bar = $stateParam.bar; //getting barVal
//..
});
I get undefined for $stateParam in the controller.
Could somebody help me understand how to get it done? I want to get bar as well without adding it to URL
If some of your params are not supposed to show up in the URL, you need to combine url and params:
.state('home', {
url: '/:foo',
params: {
foo: 'default value of foo',
bar: 'default value of bar'
},
...
})
and then use $stateParams properly:
.controller('SomeController', ['$scope', '$stateParams', function ($scope, $stateParams) {
// ... (use $stateParams)
}])
(see documentation).
And, if you're still stuck, please take a look at this working codepen demo.

UI-Router modal changes existing views

I'm trying to use ui-router to trigger a modal for a certain state, rather than changing the entire view. To do this, I implemented a slightly adapted form of what is given in the FAQ, as I'm using angular-foundation rather than bootstrap. When I trigger the state, the modal is shown, however it also clears out the existing views even though no views are specified in my state:
.state('selectModal',
onEnter: ($modal, $state, $sessionStorage) ->
$modal.open(
templateUrl: 'views/select_modal.html'
windowClass: 'tiny'
controller: 'SelectCtrl'
resolve:
options: (Restangular) -> Restangular.all('options').getList()
)
.result.then (result) ->
$sessionStorage.selected = result
$state.go 'resource', id: result.id
)
Should I be configuring views/parents for this state e.g. <div ui-view='modal'></div> or parent:'main' (I'd like it to be accessible from any state without changing that state when toggled)?
specify that it has a parent state by using the "." naming convention. (replace "parentState" with the name of the actual parent): .state('parentState.selectModal',...
Have you considered putting the modal code into a service and just calling that service in each controller that uses the modal?
angular.module('app').service('modal', function(){
function open(){
$modal.open(
templateUrl: 'views/select_modal.html',
windowClass: 'tiny',
controller: 'SelectCtrl',
resolve: {
options: function(Restangular) { Restangular.all('options').getList() }
}
) // .result... if it's generic, otherwise put it in the controller
}
});
angular.module('myapp').controller('main', function($scope, modal){
modal.open().result.then(function(result) {
$sessionStorage.selected = result;
$state.go('resource', id: result.id);
});
}

AngularJS UI Router - change url without reloading state

Currently our project is using default $routeProvider, and I am using this "hack", to change url without reloading page:
services.service('$locationEx', ['$location', '$route', '$rootScope', function($location, $route, $rootScope) {
$location.skipReload = function () {
var lastRoute = $route.current;
var un = $rootScope.$on('$locationChangeSuccess', function () {
$route.current = lastRoute;
un();
});
return $location;
};
return $location;
}]);
and in controller
$locationEx.skipReload().path("/category/" + $scope.model.id).replace();
I am thinking of replacing routeProvider with ui-router for nesting routes, but cant find this in ui-router.
Is it possible - do the same with angular-ui-router?
Why do I need this?
Let me explain with an example :
Route for creating new category is /category/new
after clicking on SAVE I show success-alert and I want to change route /category/new to /caterogy/23 (23 - is id of new item stored in db)
Simply you can use $state.transitionTo instead of $state.go . $state.go calls $state.transitionTo internally but automatically sets options to { location: true, inherit: true, relative: $state.$current, notify: true } . You can call $state.transitionTo and set notify: false . For example:
$state.go('.detail', {id: newId})
can be replaced by
$state.transitionTo('.detail', {id: newId}, {
location: true,
inherit: true,
relative: $state.$current,
notify: false
})
Edit: As suggested by fracz it can simply be:
$state.go('.detail', {id: newId}, {notify: false})
Ok, solved :)
Angular UI Router has this new method, $urlRouterProvider.deferIntercept()
https://github.com/angular-ui/ui-router/issues/64
basically it comes down to this:
angular.module('myApp', [ui.router])
.config(['$urlRouterProvider', function ($urlRouterProvider) {
$urlRouterProvider.deferIntercept();
}])
// then define the interception
.run(['$rootScope', '$urlRouter', '$location', '$state', function ($rootScope, $urlRouter, $location, $state) {
$rootScope.$on('$locationChangeSuccess', function(e, newUrl, oldUrl) {
// Prevent $urlRouter's default handler from firing
e.preventDefault();
/**
* provide conditions on when to
* sync change in $location.path() with state reload.
* I use $location and $state as examples, but
* You can do any logic
* before syncing OR stop syncing all together.
*/
if ($state.current.name !== 'main.exampleState' || newUrl === 'http://some.url' || oldUrl !=='https://another.url') {
// your stuff
$urlRouter.sync();
} else {
// don't sync
}
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.listen();
}]);
I think this method is currently only included in the master version of angular ui router, the one with optional parameters (which are nice too, btw). It needs to be cloned and built from source with
grunt build
The docs are accessible from the source as well, through
grunt ngdocs
(they get built into the /site directory) // more info in README.MD
There seems to be another way to do this, by dynamic parameters (which I haven't used).
Many credits to nateabele.
As a sidenote, here are optional parameters in Angular UI Router's $stateProvider, which I used in combination with the above:
angular.module('myApp').config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('main.doorsList', {
url: 'doors',
controller: DoorsListCtrl,
resolve: DoorsListCtrl.resolve,
templateUrl: '/modules/doors/doors-list.html'
})
.state('main.doorsSingle', {
url: 'doors/:doorsSingle/:doorsDetail',
params: {
// as of today, it was unclear how to define a required parameter (more below)
doorsSingle: {value: null},
doorsDetail: {value: null}
},
controller: DoorsSingleCtrl,
resolve: DoorsSingleCtrl.resolve,
templateUrl: '/modules/doors/doors-single.html'
});
}]);
what that does is it allows to resolve a state, even if one of the params is missing.
SEO is one purpose, readability another.
In the example above, I wanted doorsSingle to be a required parameter. It is not clear how to define those. It works ok with multiple optional parameters though, so not really a problem. The discussion is here https://github.com/angular-ui/ui-router/pull/1032#issuecomment-49196090
After spending a lot of time with this issue, Here is what I got working
$state.go('stateName',params,{
// prevent the events onStart and onSuccess from firing
notify:false,
// prevent reload of the current state
reload:false,
// replace the last record when changing the params so you don't hit the back button and get old params
location:'replace',
// inherit the current params on the url
inherit:true
});
Calling
$state.go($state.current, {myParam: newValue}, {notify: false});
will still reload the controller, meaning you will lose state data.
To avoid it, simply declare the parameter as dynamic:
$stateProvider.state({
name: 'myState',
url: '/my_state?myParam',
params: {
myParam: {
dynamic: true, // <----------
}
},
...
});
Then you don't even need the notify, just calling
$state.go($state.current, {myParam: newValue})
suffices. Neato!
From the documentation:
When dynamic is true, changes to the parameter value will
not cause the state to be entered/exited. The resolves will not be
re-fetched, nor will views be reloaded.
This can be useful to build
UI where the component updates itself when the param values change.
This setup solved following issues for me:
The training controller is not called twice when updating the url from .../ to .../123
The training controller is not getting invoked again when navigating to another state
State configuration
state('training', {
abstract: true,
url: '/training',
templateUrl: 'partials/training.html',
controller: 'TrainingController'
}).
state('training.edit', {
url: '/:trainingId'
}).
state('training.new', {
url: '/{trainingId}',
// Optional Parameter
params: {
trainingId: null
}
})
Invoking the states (from any other controller)
$scope.editTraining = function (training) {
$state.go('training.edit', { trainingId: training.id });
};
$scope.newTraining = function () {
$state.go('training.new', { });
};
Training Controller
var newTraining;
if (!!!$state.params.trainingId) {
// new
newTraining = // create new training ...
// Update the URL without reloading the controller
$state.go('training.edit',
{
trainingId : newTraining.id
},
{
location: 'replace', // update url and replace
inherit: false,
notify: false
});
} else {
// edit
// load existing training ...
}
If you need only change url but prevent change state:
Change location with (add .replace if you want to replace in history):
this.$location.path([Your path]).replace();
Prevent redirect to your state:
$transitions.onBefore({}, function($transition$) {
if ($transition$.$to().name === '[state name]') {
return false;
}
});
i did this but long ago in version: v0.2.10 of UI-router like something like this::
$stateProvider
.state(
'home', {
url: '/home',
views: {
'': {
templateUrl: Url.resolveTemplateUrl('shared/partial/main.html'),
controller: 'mainCtrl'
},
}
})
.state('home.login', {
url: '/login',
templateUrl: Url.resolveTemplateUrl('authentication/partial/login.html'),
controller: 'authenticationCtrl'
})
.state('home.logout', {
url: '/logout/:state',
controller: 'authenticationCtrl'
})
.state('home.reservationChart', {
url: '/reservations/?vw',
views: {
'': {
templateUrl: Url.resolveTemplateUrl('reservationChart/partial/reservationChartContainer.html'),
controller: 'reservationChartCtrl',
reloadOnSearch: false
},
'viewVoucher#home.reservationChart': {
templateUrl: Url.resolveTemplateUrl('voucher/partial/viewVoucherContainer.html'),
controller: 'viewVoucherCtrl',
reloadOnSearch: false
},
'addEditVoucher#home.reservationChart': {
templateUrl: Url.resolveTemplateUrl('voucher/partial/voucherContainer.html'),
controller: 'voucherCtrl',
reloadOnSearch: false
}
},
reloadOnSearch: false
})
Try something like this
$state.go($state.$current.name, {... $state.params, 'key': newValue}, {notify: false})
In Angular 2, the accepted answer from RezKesh translates to the following:
this.uiRouter.stateService.go(
"home.myRouteState",
{
"param1": this.myParam1,
"param2": this.myParam2
},
{ notify: false }
);
Assuming you have injected UIRouter into your component's constructor as follows:
constructor(
private uiRouter: UIRouter
) { }
I don't think you need ui-router at all for this. The documentation available for the $location service says in the first paragraph, "...changes to $location are reflected into the browser address bar." It continues on later to say, "What does it not do? It does not cause a full page reload when the browser URL is changed."
So, with that in mind, why not simply change the $location.path (as the method is both a getter and setter) with something like the following:
var newPath = IdFromService;
$location.path(newPath);
The documentation notes that the path should always begin with a forward slash, but this will add it if it's missing.

How to redirect to state if specific stateParam is empty

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.

Resources