tl:dr;
I'm trying to iterate through each state in $state.get() to see if the current URL matches the pattern of one of the states.
preface
I have all of my app's states inheriting from a base abstract state that utilizes resolved dependencies to ensure that the user has logged in before loading the state.
abstract state
$stateProvider.state 'nx', state =
abstract: true
url: ''
template: '<ui-view></ui-view>'
resolve: { session: [
'$q'
'coreService'
($q, sessionService)->
sessionService.withSession()
.then (rsp)-> rsp
.catch (err)-> $q.reject err
]}
example child state
This state inherits all the resolved dependencies.
$stateProvider.state 'test', state =
parent: 'nx'
url: '^/2112'
controller: class TestController
constructor: (#session)-> # the resolved session data, assuming it's valid
controllerAs: 'vc'
templateUrl: './views/view.sandbox.html'
problem
In some cases the user hasn't logged in so the resolved session data errors out. After the user logs in, I trigger a reload via this code:
reload after login
$state.go($state.current, {}, {reload:true})
When the app first loads, if the user hasn't logged in, then the intended state doesn't load leaving the app in it initial abstract state. Since you can't navigate directly to an abstract state it throws the error:
Error: Cannot transition to abstract state '[object Object]'
attempted solution
I was hoping to loop through all the registered states in $state.get() and checking the current URL against each state to see if there is a match, then using that state for the reload logic. Something like this:
url = $location.path()
for state in $state.get()
if url matches state.url # pseudo-code here
$state.go(state, {}, {reload:true})
The problem is that I don't have a good way to match the current URL against the state's URL pattern. It's not a standard regular expression and when attempting to define a UrlMatcher I can't seem to load any states.
Related
I have a simple form that can be used to initiate a forecast request. I created this as a parent state requests (Initiate Forecast).
Desired behavior
When a request is submitted, that immediate request is shown in a child state (View Status) as most recent request. This View Status state will also hold a grid of all past requests, meaning I will be refreshing the grid with data every time this state is invoked.
Both parent and child states are navigable from a sidebar menu.
So, if a user clicks on parent (Initiate Forecast), he should be able to see only the form to submit a request. If a user directly clicks on the 'View Status'(child), then he should be able to see both the form and the grid of requests.
app.js
function statesCallback($stateProvider) {
$stateProvider
.state('requests', {
url: '',
views: {
'header': {
templateUrl: 'initiateforecasting.html',
controller: 'requestsInitiateController'
},
'content': {
template: '<div ui-view></div>'
}
},
params: {
fcId: null,
fcIndex: null
}
})
.state('requests.viewStatus', {
url: '/ViewStatus',
templateUrl: 'viewstatus.html',
controller: 'requestsStatusController'
});
}
var requestsApp = angular.module('requestsApp', ['ui.router']);
requestsApp.config(['$stateProvider', statesCallback]);
requestsApp.run(['$state', function($state) {
$state.go('requests');
}]);
Plunker of my attempts so far.
This is all working for me as shown in the plunker, but I am doing it by not setting a URL to the parent. Having no URL for the parent state is allowing me to see both states. If I add a URL to parent state, then clicking on View Status link is not going anywhere (it stays on Initiate).
How do I change this so that I can have a URL for parent state and still retain the behaviour I need from the parent and child states as described above?
Note: I am fine without the URL for parent state in standalone sample code, but when I integrate this piece with backend code, having no URL fragment on the parent state is making an unnecessary request to the server. This is visible when I navigate to the child state and then go to the parent state. It effectively gives the impression of reloading the page which I think is unnecessary and can be avoided if a URL can be set to the parent state.
You shall not directly write url when using ui.router, try like this:
<a ui-sref="requests.viewStatus">View Status</a>
You are writing state name in ui-sref directive and it automatically resolves url. It's very comfortable because you can change urls any time and it will not break navigation.
I am using express, angular, and ui-router for my webpage. I would like the url for each user's page to be very simple: www.mysite.com/username
This is similar to Twitter's design. My angular state provider for the user pages looks like this:
$stateProvider
.state('userPage', {
url: '/:username',
templateUrl: 'js/user-page/user-page.html',
controller: 'UserPageCtrl'
});
The only issue is now when I try to navigate to any other page whose state is defined with only one URL part (ie. www.mysite.com/login), the app always parses the URL as a user page (but without being able to find a user).
Is there any way to tell angular to try and load the URL as a defined state before treating the url as a dynamic parameter?
I can simply require all other routes to have two parameters (ie. www.mysite.com/login/userlogin), but that doesn't seem very elegant.
You just need to define the login state first. Order is important.
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'somewhere/login.html',
controller: 'LoginPageCtrl'
},
.state('userPage', {
url: '/:username',
templateUrl: 'js/user-page/user-page.html',
controller: 'UserPageCtrl'
},
});
If a user navigates to /login then a matching state will be searched for. It will check your first state, then the second and so on until a matching state is found. In this case, the login state will match so the searching for another matching state will cease.
I have an Angular application using ui-router and I am having issues whenever I refresh the page. I am using nested views, named views to build the application. Whenever I refresh the page, ui-router doesn't reload the current state and just leaves the page blank.
On page load $state.current is equal to
Object {name: "", url: "^", views: null, abstract: true}
I am reading my navigation from a .json file via $http and looping through the states. So this is what I can show:
stateProvider.state(navElement.stateName, {
url: navElement.regexUrl ? navElement.regexUrl : url,
searchPage: navElement.searchPage, //something custom i added
parent: navElement.parent ? navElement.parent : "",
redirectTo: navElement.redirectTo,
views: {
'subNav#index': {
templateUrl: defaults.secondaryNavigation,
controller: 'secondaryNavigationController as ctrl' //static
},
'pageContent#index': {
template: navElement.templateUrl == null
? '<div class="emptyContent"></div>'
: undefined,
templateUrl: navElement.templateUrl == null
? undefined
: navElement.templateUrl,
controller: navElement.controller == null
? undefined
: navElement.controller + ' as ctrl'
}
}
});
This code gets executed for each item in the nested json object. If there is anything else that would be helpful, let me know.
There is a question: AngularJS - UI-router - How to configure dynamic views with one answer, which shows how to do that.
What is happening? On refresh, the url is evaluated sooner, then states are registered. We have to postpone that. And solution is driven by UI-Router native feature deferIntercept(defer)
$urlRouterProvider.deferIntercept(defer)
As stated in the doc:
Disables (or enables) deferring location change interception.
If you wish to customize the behavior of syncing the URL (for example, if you wish to defer a transition but maintain the current URL), call this method at configuration time. Then, at run time, call $urlRouter.listen() after you have configured your own $locationChangeSuccess event handler.
In a nutshell, we will stop URL handling in config phase:
app.config(function ($urlRouterProvider) {
// Prevent $urlRouter from automatically intercepting URL changes;
// this allows you to configure custom behavior in between
// location changes and route synchronization:
$urlRouterProvider.deferIntercept();
})
And we will re-enable that in .run() phase, once we configured all dynamic states from JSON:
.run(function ($rootScope, $urlRouter, UserService) {
...
// Once the user has logged in, sync the current URL
// to the router:
$urlRouter.sync();
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.listen();
});
There is a plunker from the linked Q & A
I don't know how are all your routes.. but if you refresh a page of a child state, you need to pass all parameters of the parents states to be resolved correctly.
I have a UI-Router site with the following states:
$stateProvider
.state('order', {
url: '/order/:serviceId',
abstract:true,
controller: 'OrderController'
})
.state('order.index', {
url:'',
controller: 'order-IndexController'
})
.state('order.settings', {
url:'',
controller: 'order-SettingsController'
})
Where my two states do NOT have a url set, meaning they should only be reachable through interaction with the application. However the order.index state is automatically loaded by default because of the order in which I have defined the states.
I am noticing that trying to do a ui-sref="^.order.settings" or $state.go("^.order.settings") then ui-router first navigates to the order.settings state and then it immediately navigates to the order.index state (default state). I think this is happening because the url changing is causing a second navigation to occur and since the state.url == '' for both, it automatically defaults to the order.index state...
I tested this out by setting the {location: false} object in the $state.go('^order.settings', null, {location:false}). This caused the correct state to load (order.settings), but the url did not change. Therefore I think the url changing is triggering a second navigation.
I'd understand, can imagine, that you do not like my answer, but:
DO NOT use two non-abstract states with same url defintion
This is not expected, therefore hardly supported:
.state('order.index', {
url:'', // the same url ...
...
})
.state('order.settings', {
url:'', // .. used twice
...
})
And while the UI-Router is really not requiring url definition at all (see How not to change url when show 404 error page with ui-router), that does not imply, that 2 url could be defined same. In such case, the first will always be evaluated, unless some special actions are used...
I would strongly sugest, provide one of (non default) state with some special url
.state('order.settings', {
url:'/settings', // ALWAYS clearly defined
...
})
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