Considering the following states taken from the ui-router documentation:
.state('state1', {
url: '/state1',
templateUrl: 'partials/state1.html'
controller: 'State1Ctrl'
})
.state('state1.list', {
url: '/list',
templateUrl: 'partials/state1.list.html',
})
And the controller for "partials/state1.html" for state "state1":
.controller('State1Ctrl', function () {
});
Is there any built-in feature to determine from within the controller or within the template, what state the controller/template is associated with?
For example:
.controller('State1Ctrl', function ($state) {
console.log($state.this); // state1
});
And if there is no built-in solution, how would you "decorate" $state or $stateParams, to contain this information?
The only solution I've come up with is to use $state.get() and then find the state with the controller or template value. This seems incredibly messy, though.
You can access the current state configuratin object like this:
$state.current
For further information take a look at the $state documentation.
You can do it as follow,
$state.current.name //Return the name of current state
Couldn't find this documented anywhere, so I looked in the source code.
There is a data field named $uiView attached to the ui-view element, it contains the view name and the associated state. You can get this state like this:
elem.closest('[ui-view]').data('$uiView').state
or even
elem.inheritedData('$uiView').state
We can see what is defined for current state, using the $state.current, check this example showing:
state1
{
"url": "/state1",
"template": "<div>state1 <pre>{{current | json }}</pre><div ui-view=\"\"></div> </div>",
"controller": "State1Ctrl",
"name": "state1"
}
list
{
"url": "/list",
"template": "<div>list <pre>{{current | json }}</pre></div>",
"controller": "State1Ctrl",
"name": "state1.list"
}
the controller:
.controller('State1Ctrl', function($scope, $state) {
$scope.current = $state.current
});
check that here
EXTEND: The above example will always return current state - i.e. if there is hierarchy of three states and we access the last state ('state1.list.detail') directly:
<a ui-sref="state1.list.detail({detailId:1})">....
Each controller will be provided with the same state: $state("state1.list.detail").
Why? beause this state has enough information what are all the views (hierarchically nested) and their controllers needed. We can observe that all in the
$state.$current // not .current
Quickly discussed here cite:
In addition, users can attach custom decorators, which will generate new properties within the state's internal definition. There is currently no clear use-case for this beyond accessing internal states (i.e. $state.$current), however, expect this to become increasingly relevant as we introduce additional meta-programming features.
BUT: there is NO way, how to get information from current controller instance, to which $state it belongs! Even any iterations, searching through some $state.get('stateName') will be unreliable, because simply there is no kept relation that way. One controller Class could be used for many views as different Instances. And also, from my experience, I do not remember any case, when I needed to know such information... wish now it is a bit more clear
This is useful if you are looking for Current state name, $state.current.name
Not sure it is the same version, but on 0.3.1 you can use this to get the current state:
$state.$current.name
And to perform a check:
$state.is('contact.details.item');
Documentation:
https://ui-router.github.io/ng1/docs/0.3.1/index.html#/api/ui.router.state.$state
A working "out of the box" Controller from your code, which shows state:
.controller('State1Ctrl', function ($state) {
console.log("Current State: ", $state.current.name);
});
If you want to check the current state
console.log("state", $state.current)
If you want to check the name of the current state name
console.log("statename", $state.current.name)
Related
In the very new release of UI-Router1.0.0-alpha.1 Christopher Thielen announced dynamic parameters. For my understanding, if a param is configured to be dynamic, when changing it from the controller the URL should change accordingly. I tried several methods and couldn't make this happen. There doesn't seem to be binding between $state.params.myParam and $stateParams.myParam.
Can someone please share a working example, or tell me if it is indeed not working?
Thanks
It is not enough to just change the dynamic parameter. The location changes when performing state transition, using ui-sref or $state.go().
When transitioning to the current state where the only change is in parameters that are defined as dynamic, the controller is not reloaded.
example:
$stateProvider.state({
name: 'home',
url: '/home/:dynamicParam',
template: template,
controller: controller,
params: {
dynamicParam: {
dynamic: true
}
}
});
Change the param using: ui-sref=".({ dynamicParam: newVal })" or $state.go('.', { dynamicParam: newVal })
Here's a plunker made by Chris Thielen that demonstrates this.
I'm currently working on a small application that shows a 'drill down' type of menu. Unlike a TreeView my component only displays the current level.
There are only 3 levels: main, submenu, item. Drilling deeper (on the item) will result in a new state that corresponds to a detail view of the selected item.
I have only 2 states, the first is valid for any of the three levels. The second is the detail state:
$stateProvider.state('menu', {
url: '/menu/:submenu/:item',
templateUrl: 'src/menu/views/menu.html',
controller: 'MenuController',
controllerAs: 'MenuController',
resolve: {
data: function($stateParams, MenuService){
return MenuService.loadUri($stateParams);
}
}
}).state('menu.details', {
url: '/:selectedItem',
templateUrl: 'src/menu/views/menu-details.html',
controller: 'MenuDetailsController',
controllerAs: 'MenuDetailsController',
resolve: {
data: function($stateParams, MenuService){
return MenuService.loadUri($stateParams);
}
}
});
This solutions works correctly, however I'm a bit annoyed by the fact I need to define the resolvemethod twice. I am required to do this as $stateParams contains only the params of that exact state (excluding child states) and I need all parameters to form a valid URL to fetch the corresponding resource via the MenuService.
I know that state.params can be used to get all state params, however it doesn't seem to work in the resolve method: it contains the parameters of the previous state.
Does anybody know how I could fix this? Having a child state that should simply trigger a new view, whilst using the resolve method of the parent state with all state parameters?
If you think I'm completely misunderstanding how states work, then please go ahead and tell me :)
I have an angular app with a homepage that shows a list of things. Each thing has a type. In the nav, there are selectors corresponding to each thing type. Clicking one of these selectors causes the home controller to filter the things shown to those of the selected type. As such, I see the selectors as corresponding to states of the home page.
Now, I'd like to map each of these states to a url route: myapp.com/home loads the home page in default (unfilitered) state, myapp.com/home/foo opens the home page with the foo-type selector activated, and switching from there to myapp.com/home/bar switches to the bar-filtered state without reloading the page.
It's that last bit - triggering "state" changes without reloading the page, that's been particularly tricky to figure out. There are numerous SO/forum questions on this topic but none have quite hit the spot, so I'm wondering if I'm thinking about this in the wrong way: Should I be thinking of these "states" as states at all? Is there a simpler approach?
Also, I'm open to using either ngRoute or ui.router - is there anything about one or the other that might make it simpler to implement this?
Using ui-router, you can approach it like this:
$stateProvider
.state('home', {
url: "/home",
controller: "HomeController",
templateUrl: "home.html"
// .. other options if required
})
.state('home.filtered', {
url: "/{filter}",
controller: "HomeController",
templateUrl: "home.html"
// .. other options if required
})
This creates a filtered state as a child of the home state and means that you can think of the URL to the filtered state as /home/{filter}. Where filter is a state parameter that can then be accessed using $stateParams.
Since you don't want to switch views, you inject $stateParams into your controller, watch $stateParams.filter, and react to it how you wish.
$scope.$watch(function () { return $stateParams.filter }, function (newVal, oldVal) {
// handle it
});
As far as I understand ui.router
You have $stateProvider
in it you can write $stateProvider.state()
you can write
.state('account', {
url: '/account',
template: '<ui-view/>'
})
.state('account.register', {
url: '/register',
templateUrl: '/account/views/register.html',
data: { title: 'Create Account' },
controller: 'AccountRegister'
})
but 'account' is a child of some kind of root state.
So my thought is, the root state of ui.router has a <ui-view/>, just like 'account' has the template '<ui-view/>', so index.html would be the root state. Even 'home' with url: '/' would be or rather is a child of this root state.
and if I'm able to access the root state and say it should resolve a User service this resolved value should be available on all states (or better on every state that is a child of the root state, aka all). The User service promise makes a $http.get('/api/user/status') and it returns {"user":{id: "userid", role: "role"}} or {"user":null}
This would guarantee that the value returned by the User service is always populated.
How do I access the root state and say it should resolve e.g. User.get()?
Or let me rephrase that.
A user object should be available to all controllers.
The information about the currently logged in user is supplied by a $http.get('/api/user/status')
That's the problem and I'm looking for a DRY solution that, assuming the api is serving, is 100% safe to assume a user object is set, aka the promise is always fulfilled, aka "waiting for the promise to resolve before continuing".
As of ui-router 1.0.0-beta.3 you can now do this easily using transition hooks:
angular
.module('app')
.run(run);
function run($transitions, userService) {
// Load user info on first load of the page before showing content.
$transitions.onStart({}, trans => {
// userService.load() returns a promise that does the job of getting
// user info and populating a field with it (you can do whatever you like of course).
// Just remember to finally return `true`.
return userService
.load()
.then(() => true);
});
}
The state manager will wait for the promise to resolve before continuing loading of states.
IMPORTANT: Make sure that userService loads its data only once, and when it's already loaded it should just return an already resolved promise. Because the callback above will be called on every state transition. To deal with login/logout for example, you can create a userService.invalidate() and call it after doing the login/logout but before doing a $state.reload().
Disclaimer — messing around with you root scope is not a good idea. If you're reading this, please note that the OP, #dalu, wound up canning this route and solved his issue with http interceptors. Still — it was pretty fun answering this question, so you might enjoy experimenting with this yourself:
It might be a little late, but here goes
As mentioned in the comments and from my experience so far, this is not possible to do with the ui-router's api (v0.2.13) seeing as they already declare the true root state, named "". Looks like there's been a pull request in for the past couple years about what you're looking for, but it doesn't look like it's going anywhere.
The root state's properties are as follows:
{
abstract: true,
name: "",
url: "^",
views: null
}
That said, if you want to extend the router to add this functionality, you can do this pretty easily by decorating the $stateProvider:
$provide.decorator('$state', function($delegate) {
$delegate.current.resolve = {
user: ['User', '$stateParams', httpRequest]
};
return $delegate
});
Note that there are two currents: $delegate.current - the raw "" state - and $delegate.$current - its wrapper.
I've found a bit of a snag, though, before this becomes the solution you were looking for. Every time you navigate to a new state, you'll make another request which has to be resolved before moving forward. This means that this solution isn't too much better than event handling on $stateChangeStart, or making some "top" state. I can think of three work-arounds off the top of my head:
First, cache your http call. Except I can see how this pretty much invalidates certain use-cases, perhaps you're doing something with sessions.
Next, use one of your singleton options (controller/service) to conditionally make the call (maybe on just set a flag for first load). Since the state is being torn down, it's controller might be as well - a service might be your only option.
Lastly, look into some other way of routing - I haven't used ui.router-extras too much, but sticky states or deep state redirect might do the trick.
I guess, lastly, I'm obligated to remind you to be careful with the fact that that you're working on the root-level. So, i mean, be about as careful as you should be when doing anything in root-level.
I hope this answers your question!
Here is different approach
Add following code to your app's .run block
// When page is first loaded, it will emit $stateChangeStart on $rootScope
// As per their API Docs, event, toState, toParams, fromState values can be captured
// fromState will be parent for intial load with '' name field
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState){
// check if root scope
if(fromState.name == ''){
// add resolve dependency to root's child
if(!toState.resolve) toState.resolve = {};
toState.resolve.rootAuth = ['$_auth', '$q', '$rootScope', function($_auth, $q, $rootScope){
// from here on, imagine you are inside state that you want to enter
// make sure you do not reject promise, because then you have to use
// $state.go which will again call state from root and you will end up here again
// and hence it will be non ending infinite loop
// for this example, I am trying to get user data on page load
// and store in $rootScope
var deferred = $q.defer();
$_auth.user.basic().then(
function(authData){
// store data in rootscope
$rootScope.authData = authData;
deferred.resolve(authData);
console.log($rootScope.authData);
},
function(){
$rootScope.authData = null;
deferred.resolve(null);
console.log($rootScope.authData);
}
);
return deferred;
}];
}
});
Use $state.go('name', {}, {reload:true}); approach in case you need to refresh authData on state change (so that transition state will always start from root else resolve to load authData will never get called as once root is loaded, it never needs to get called again {except page reload}).
Use the resolve attribute on the state definition.
.state('root', {
resolve: {
user: ['$http', function($http) {
return $http.get('/api/user/status')
}]
},
controller: function($scope, user) {
$scope.user = user; // user is resolved at this point.
}
})
see:
https://github.com/angular-ui/ui-router/wiki#resolve
https://egghead.io/lessons/angularjs-resolve (not ui-router specific, but works the same)
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states
I can't figure out a reasonable way, which doesn't feel like a hack, to solve this rather trivial problem.
I want a guest to see a splash page when they access the index of the website and a logged in user to see their profile, with each page having it's own template and controller. Ideally, there would be two states for one url, and somehow I would be able to automatically alter the active one depending on the loggin status. Both of these views will have their own nested views so ng-include cannot be used (I assume).
I'm quite new to angular and ui router and think I might be overlooking an easy solution to the problem.
Could it be done with named views and ng-show?
If you're using UI Router, just create three states: the root state, with the '/' URL, and two direct descendant states with no URLs. In the onEnter of the root state, you detect the state of the user and transition to the correct child state accordingly. This gives the appearance of keeping the same URL for both child states, but allows you to have to separate states with separate configurations.
The templateUrl can be a function as well so you can check the logged in status and return a different view and define the controller in the view rather than as part of the state configuration
My Solution:
angular.module('myApp')
.config(function ($stateProvider) {
$stateProvider
.state('main', {
url: '/',
controller: function (Auth, $state) {
if (someCondition) {
$state.go('state1');
} else {
$state.go('state2');
}
}
});
});
where state 1 and state 2 are defined elsewhere.
For docs purposes, I used:
$rootScope.$on('$stateChangeStart', function(event, toState) {
if ((toState.name !== 'login') && (!$localStorage.nickname)) {
event.preventDefault();
$state.go('login');
}
});
Using $routeChangeStart didn't work for me.
It is used for me conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});
If I understand the question; you want to make sure that the user who hasn't logged in cannot see a page that requires log in. Is that correct?
I've done so with code like this inside a controller:
if(!'some condition that determines if user has access to a page'){
$location.path( "/login" );
}
Anywhere (probably in some high-level controller) you should be able to just bind a '$routeChangeStart' event to the $rootScope and do your check then:
$rootScope.$on('$routeChangeStart', function(next, current){
if(next != '/login' && !userLoggedIn){
$location.path( "/login" );
}
});
This will get fired every time a new route is set, even on the first visit to the page.
The way I've done this is pretty simple. I made one for our A/B testing strategy. This is the gist:
resolve: {
swapTemplate: function(service) {
// all of this logic is in a service
if (inAbTest) {
this.self.templateUrl = '/new/template.html';
}
}
... other resolves
}
This gets called before the template is downloaded and therefor you're allowed to swap out the template url.
In my case, if two states can share logic of same controller, conditional template is a good choice. Otherwise, creating separate states is a good option.