UI-router: nested state resolving - angularjs

I have two states: a 'contacts' and a 'contacts.all' which is a child state of the previuos. I want this child state to pre-load data from server:
.state('contacts', {
url: "/contacts",
controller: 'ContactsCtrl'
})
.state('contacts.all', {
url: "/all",
resolve: {
initialContacts: function (contactsCRUD) {
return contactsCRUD.getAll().$promise;
}
}
})
and then inject the to the ContactsCtrl controller:
function ContactsCtrl($scope, initialContacts) {
$scope.Contacts = initialContacts || {};
}
I do need these data preloaded only in the 'contacts.all', not 'contacts' state. If I leave this code as is and try to navigate any of these states then I'll get an inject error saying "unknown provider" for the "initialContacts" because the controller seems to be instantiated before the initialContacts is known.
What is normal way to do it? I know I can add a "resolve" option to the parent state and set initialContacts to something like {} but I dont think it's a good practice.

Related

How to get current route within resolve function?

I have a service which checks whether the current user has access to the route.
I would like that service to be called as part of the resolve, so that the view never loads if the user should not have access. However, within the resolve function the $state dependency does not yet contain the actual state of the router.
.state('home', {
url: '/home',
templateUrl: 'app/home.html',
controller: 'HomeController',
controllerAs: 'main',
resolve: {
allowed: function(auth, $state) {
return auth.isAllowed($state.current.name);
}
},
})
... however $state.current.name is empty at the point which it is used. How can I pass the state's name (i.e. "home")?
Try with
resolve: {
allowed: function(auth) {
return auth.isAllowed(this.self.name);
}
},
For UI router 1.0+: inject $state$ to get access to future state (to which transition is in progress). However, if resolve is in parent or abstract state, it will point to that abstract state, not child state to which transition is.
resolve: {
allowed: function($state$) {
...
}
}

Ui-Router $stateParams with nested routes

I'm trying to route to a url with the selected object's id value within a nested state but it's not working.
ROUTER:
.state('sessions.editor', {
url: '/editor/:id',
templateUrl: 'views/editor.html',
controller: 'EditorCtrl'
})
CONTROLLER (EditorCtrl):
$scope.theSession.$id = $stateParams.id;
//$scope.session object is successfully returned with the $id property.
PREVIOUS CONTROLLER:
//when button is clicked
$state.go('sessions.editor');
However, when I try to set it to $stateParams, the id property becomes undefined. I tested this with another property on $scope.session and that property becomes undefined also when I try to set it $stateParams.
I'm thinking the trouble has something to do with the state being nested. Is this correct?
I believe you need to add the params key to the state for UI-Router, example:
.state('sessions.editor', {
url: '/editor/:id',
controller: 'EditorCtrl',
templateUrl: 'views/editor.html',
params: {
id: {
value : '',//Default
squash: false
}
}
})
Then use your existing implementation in the EditorCtrl for $stateParams.

Angular UI Router child resolves

I am trying to create a parent-child relationship in my UI router config with using different resolves and it doesn't seem to be working. I want to have a common parent state with child states that control whether the state is in edit mode or new mode. Depending on the mode, the resolve of the state is different.
What I have is essentially this:
.state('main.details', {
url: "/details",
templateUrl: "modules/details.html",
abstract: true
})
.state('main.details.new', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'NewService', function ($stateParams, NewService) {
return NewService.getDetail($stateParams.detailId, true);
}]
}
})
.state('main.details.edit', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'EditService', function ($stateParams, EditService) {
return EditService.getDetail($stateParams.detailId, true);
}]
}
})
Doing it this way, I get this:
Error: [$injector:unpr] Unknown provider: detailProvider <- detail <- DetailsCtrl
Is there a way to have a parent state that just defines the URL and templateUrl and then have child states with varying resolves?
One option to help with this would be to pull out the repeating resolve on the routes and put it into a factory that the controller can call on

Can I have a child state that is the index using ui-router?

My routes look like:
$stateProvider.state('repository', {
url: '/:host/:owner/:repository',
views: {
appView: {
templateUrl: '/templates/app/repository.html'
}
}
}).state('repository.analytics', {
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryAnalytics.html'
}
}
}).state('repository.commit', {
url: '/:host/:owner/:repository/commit/:commitHash',
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryCommit.html'
}
}
}).state('repository.file', {
url: '/file?path',
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryFile.html'
}
}
});
I want the base URL for all repository-like states, that's why I'm specifying the url there. As an example, if I didn't do it this way, I would have to specify everything as it's shown in the commit state. This is verbose and not something I want to do.
So is it possible to have a default child state for repository so that if someone is directed there, then that child view loads?
** UPDATE **
This seems to work just fine if I click through the app, but if I go to the /:host/:owner/:repository URL directly, the child view (analytics) never loads.
I don't know whether you can have a default child state, but you can set subview in that parent state. Like this:
$stateProvider.state('repository', {
url: '/:host/:owner/:repository',
views: {
appView: {
templateUrl: '/templates/app/repository.html'
},
'repositoryView#repository': { // it means the repositoryView of repository state.
templateUrl: '/templates/app/_repositoryAnalytics.html'
}
}
})
Then, when you open with repository state or URL, the analytics page will be loaded in repositoryView view of repo page.
[updated]
This format 'repositoryView#repository' means that, the view 'repositoryView' in the state 'repository'. Because you try to open the state 'repository', with a default sub-view. And the sub view 'repositoryView' is defined in 'repository.html'. If you didn't set the state scope, ui-router will think that the sub-view 'repositoryView' belongs to 'repository' 's parent view.
I don't know whether I explain it clearly, you can check the ui-router wiki
I created working plunker here. One way could be to use eventing to force go to child state, when parent is selected (resolved from url)
.run(['$rootScope', '$state', '$stateParams',
function($rootScope, $state, $stateParams) {
$rootScope.$on('$stateChangeStart', function(event, toState, toPrams) {
if (toState.name === "repository") {
event.preventDefault();
$state.go('repository.analytics', toPrams);
}
});
}
])
Check it here
Some other topics about redirections parent-child:
Angular UI-Router $urlRouterProvider .when not working when I click <a ui-sref="...">
Angular UI-Router $urlRouterProvider .when not working *anymore*

Controlling order of operations with services and controllers

I have two services - one to store user details and the other to make a call to retrieve those details:
userService stores user details to be used across the entire app (i.e. injected in controllers, services, etc.)
function userService($log) {
var id = '';
var username = '';
var isAuthenticated = false;
var service = {
id: id,
username: username,
isAuthenticated: isAuthenticated
};
return service;
}
authService is used (hopefully just once) to retrieve the user details from a Web API controller:
function authService($log, $http, userService) {
$log.info(serviceId + ': Inside authService method');
var service = {
getUserDetails: getUserDetails
};
return service;
function getUserDetails() {
$log.info(serviceId + ': Inside getUserDetails method');
return $http.get('api/authentication', { cache: true });
}
}
Initially, I had the call to the authService fire in a .run block like so:
.run(['$log', 'authService', 'userService', function ($log, authService, userService) {
authService.getUserDetails()
.then(querySucceeded);
function querySucceeded(result) {
userService.id = result.data.Id;
userService.username = result.data.username;
}
}]);
But the problem was that the getUserDetails-returned promise did not resolve until after I my controllers fired and, thus, too late for me. The user data was not ready.
I then looked at the resolve option in the $stateProvider (for UI-Router):
.state('dashboard', {
url: '/dashboard',
views: {
header: {
templateUrl: 'app/partials/dashboard/header.template.html',
controller: 'DashboardHeaderController',
controllerAs: 'dashboardHeaderVM',
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
}
}
})
The assumption is that the view won't be rendered until the promise in the resolve section is, well, resolved. That seems to work fine.
Here's the (relevant part of the) controller where I use the returned user property:
function DashboardHeaderController($log, user) {
var vm = this;
// Bindable members
vm.firstName = user.data.firstName;
}
However, I have two routes (more to come) and a user can navigate to either one. Do I need to have a resolve property in each state section for the authService? I want to fire the call to authService.getUserDetails just once no matter which route is served and have it available after that for any route, controller, etc.
Is there a better (best practice) way to do this?
Not sure about better or best practice, but here is a plunker with my way.
The point is to move resolve into some parent root state. The one who is ancestor of all states in the application:
$stateProvider
.state('root', {
abstract : true,
// see controller def below
controller : 'RootCtrl',
// this is template, discussed below - very important
template: '<div ui-view></div>',
// resolve used only once, but for available for all child states
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
})
This is a root state with resolve. The only state with resolve. Here is an example of its first child (any other would be defined similar way:
$stateProvider
.state('index', {
url: '/',
parent : 'root',
...
This approach will work out of the box. I just would like to mention that if the 'RootCtrl' is defined like this:
.controller('RootCtrl', function($scope,user){
$scope.user = user;
})
we should understand the UI-Router inheritance. See:
Scope Inheritance by View Hierarchy Only
small cite:
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states...
More explanation could be found in this Q & A
So, what does it mean?
Our root view can pass the resolved stuff into child state only - if their views are nested.
For example, the $scope.user will be inherited in child states/views/$scopes only if they are nested like this
.state('index', {
url: '/',
parent : 'root',
views: {
'' : { // the root view and its scope is now the ancestor
// so $scope.user is available in every child view
templateUrl: 'layout.html',
controller: 'IndexCtrl'
},
'top#index' : { templateUrl: 'tpl.top.html',},
'left#index' : { templateUrl: 'tpl.left.html',},
'main#index' : { templateUrl: 'tpl.main.html',},
},
Check it here
If I correctly understand you want that on page load you would have user info before any controller or service request it.
I had similar task in my current project.
To solve the problem I manually requested current user info before app bootstapping & store it in localStorage.
Then after app bootstrapping all controllers/services have accesss to current user info.
TIP: to get user info before app bootstrap you can still use $http service by manually injecting it:
angular.injector(['ng']).get('$http');

Resources