I would like to create an abstract parent state, that has only one job: to resolve the current user through an ajax server call, and then pass this object to the child state. The problem is that the child state never gets loaded. Please have a look at this plunker: Example
a state
angular.module('test', ['ui.router'])
.config(function($stateProvider, $urlRouterProvider){
// Parent route
$stateProvider.state('main', {
abstract:true,
resolve: {
user: function(UserService){
return UserService.getUser();
}
}
});
// Child route
$stateProvider.state('home', {
parent: 'main',
url: '/',
controller: 'HomeController',
controllerAs: '$ctrl',
template: '<h1>{{$ctrl.user.name}}</h1>'
});
$urlRouterProvider.otherwise('/');
});
a factory
angular.module('test').factory('UserService', function($q){
function getUser() {
var deferred = $q.defer();
// Immediately resolve it
deferred.resolve({
name: 'Anonymous'
});
return deferred.promise;
}
return {
getUser: getUser
};
});
a controller
angular.module('test').controller('HomeController', function(user){
this.user = user;
});
In this example, the home state will never display the template, I don't really understand why. If I remove the parent: 'main' line, then it displays the template, but of course I get an error because it cannot find the user dependency in the HomeController.
What am I missing? I did everything like it is described in ui-router's documentation, I think this should work.
Every parent must have a target ui-view in template for its child
$stateProvider.state('main', {
abstract:true,
resolve: {
user: function(UserService){
return UserService.getUser();
}
}
template: '<div ui-view=""></div>'
});
NOTE: Another option is to use absolute names and target index.html .. but in this case the above is the way to go (Angularjs ui-router not reaching child controller)
Related
I have a view state like this, with 3 views:
(function() {
'use strict';
angular.module('pb.tracker').config(function($stateProvider) {
$stateProvider.state('tracker', {
url: '/tracker',
controller: 'TrackerController as tracker',
data: {
pageTitle: 'Parcel Tracker',
access: 'public',
bodyClass: 'tracker'
},
resolve: {
HistoryResolve: function($log, MockDataFactory) {
return MockDataFactory.query({
filename: 'trackingdata'
});
}
},
views: {
'': {
templateUrl: 'modules/tracker/templates/tracker.html'
},
'controls#tracker': {
templateUrl: 'modules/tracker/templates/tracker-controls.html'
},
'content#tracker': {
templateUrl: 'modules/tracker/templates/tracker-details.html'
}
}
});
});
})();
I want to use the controller TrackerController for all the views in the state. I thought they would simple inherit the parent one.
But so far, even a simple log does not show in the console. The controller is
(function() {
'use strict';
angular.module('pb.tracker').controller('TrackerController', function($log, HistoryResolve) {
var _this = this;
// _this.packageHistory = HistoryResolve;
$log.debug('foo');
});
})();
So, my console should read "foo" regardless, yes? Nothing in the console. No errors. The works fine, the views load the templates. I am only stuck on the controller. I've never run into this.
UPDATE
OK, I am trying to define a parent state, and assign the controller to that. However, what I have below is no yielding nothing at all in the browser...
(function() {
'use strict';
angular.module('pb.tracker').config(function($stateProvider) {
$stateProvider.state('tracker', {
url: '/tracker',
abstract: true,
controller: 'TrackerController as tracker',
data: {
pageTitle: 'Parcel Tracker',
access: 'public',
bodyClass: 'tracker'
},
resolve: {
HistoryResolve: function($log, MockDataFactory) {
return MockDataFactory.query({
filename: 'trackingdata'
});
}
}
})
.state('tracker.details', {
url: '/tracker/details',
views: {
'': {
templateUrl: 'modules/tracker/templates/tracker.html'
},
'controls#tracker': {
templateUrl: 'modules/tracker/templates/tracker-controls.html'
},
'content#tracker': {
templateUrl: 'modules/tracker/templates/tracker-details.html'
}
}
});
});
})();
When you define named views (using the views property, aka "named views"), the template properties of the state are overriden by each named view. From the documentation:
If you define a views object, your state's templateUrl, template and templateProvider will be ignored. So in the case that you need a parent layout of these views, you can define an abstract state that contains a template, and a child state under the layout state that contains the 'views' object.
Note that a template is always paired with a controller. So since it doesn't use the template properties, there's no need for it to instantiate the controller. You have two choices:
Use specify the controller for each view. This will instantiate a controller for each named view, probably not what you want.
Create a parent state to this state, which is abstract and uses the controller. Note that your state above doesn't have a child/parent relationship, it's just one state w/some named views.
I have an AngularJS service which communicates with the server and returns
translations of different sections of the application:
angular
.module('utils')
.service('Translations', ['$q','$http',function($q, $http) {
translationsService = {
get: function(section) {
if (!promise) {
var q = $q.defer();
promise = $http
.get(
'/api/translations',
{
section: section
})
.success(function(data,status,headers,config) {
q.resolve(result.data);
})
.error(function(data,status,headers,config){
q.reject(status);
});
return q.promise;
}
}
};
return translationsService;
}]);
The name of the section is passed as the section parameter of the get function.
I'm using AngularJS ui-router module and following design pattern described here
So I have the following states config:
angular.module('app')
.config(['$stateProvider', function($stateProvider) {
$stateProvider
.state('users', {
url: '/users',
resolve: {
translations: ['Translations',
function(Translations) {
return Translations.get('users');
}
]
},
templateUrl: '/app/users/list.html',
controller: 'usersController',
controllerAs: 'vm'
})
.state('shifts', {
url: '/shifts',
resolve: {
translations: ['Translations',
function(Translations) {
return Translations.get('shifts');
}
]
},
templateUrl: '/app/shifts/list.html',
controller: 'shiftsController',
controllerAs: 'vm'
})
This works fine but as you may notice I have to explicitly specify translations in the resolve parameter. I think that's not good enough as this duplicates the logic.
Is there any way to resolve translations globally and avoid the code duplicates. I mean some kind of middleware.
I was thinking about listening for the $stateChangeStart, then get translations specific to the new state and bind them to controllers, but I have not found the way to do it.
Any advice will be appreciated greatly.
Important note:
In my case the resolved translations object must contain the translations data, not service/factory/whatever.
Kind regards.
Let me show you my approach. There is a working plunker
Let's have a translation.json like this:
{
"home" : "trans for home",
"parent" : "trans for parent",
"parent.child" : "trans for child"
}
Now, let's introduce the super parent state root
$stateProvider
.state('root', {
abstract: true,
template: '<div ui-view=""></div>',
resolve: ['Translations'
, function(Translations){return Translations.loadAll();}]
});
This super root state is not having any url (not effecting any child url). Now, we will silently inject that into every state:
$stateProvider
.state('home', {
parent: 'root',
url: "/home",
templateUrl: 'tpl.html',
})
.state('parent', {
parent: 'root',
url: "/parent",
templateUrl: 'tpl.html',
})
As we can see, we use setting parent - and do not effect/extend the original state name.
The root state is loading the translations at one shot via new method loadAll():
.service('Translations', ['$http'
,function($http) {
translationsService = {
data : {},
loadAll : function(){
return $http
.get("translations.json")
.then(function(response){
this.data = response.data;
return this.data;
})
},
get: function(section) {
return data[section];
}
};
return translationsService;
}])
We do not need $q at all. Our super root state just resolves that once... via $http and loadAll() method. All these are now loaded, and we can even place that service into $rootScope:
.run(['$rootScope', '$state', '$stateParams', 'Translations',
function ($rootScope, $state, $stateParams, Translations) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.Translations = Translations;
}])
And we can access it anyhwere like this:
<h5>Translation</h5>
<pre>{{Translations.get($state.current.name) | json}}</pre>
Wow... that is solution profiting almost from each feature coming with UI-Router... I'd say. All loaded once. All inherited because of $rootScope and view inheritance... all available in any child state...
Check that all here.
Though this is a very old question, I'd like to post solution which I'm using now. Hope it will help somebody in the future.
After using some different approaches I came up with a beautiful angularjs pattern by John Papa
He suggest using a special service routerHelperProvider and configure states as a regular JS object. I'm not going to copy-paste the entire provider here. See the link above for details. But I'm going to show how I solved my problem by the means of that service.
Here is the part of code of that provider which takes the JS object and transforms it to the states configuration:
function configureStates(states, otherwisePath) {
states.forEach(function(state) {
$stateProvider.state(state.state, state.config);
});
I transformed it as follows:
function configureStates(states, otherwisePath) {
states.forEach(function(state) {
var resolveAlways = {
translations: ['Translations', function(Translations) {
if (state.translationCategory) {
return Translations.get(state.translationCategory);
} else {
return {};
}
}],
};
state.config.resolve =
angular.extend(state.config.resolve || {}, resolveAlways || {});
$stateProvider.state(state.state, state.config);
});
});
And my route configuration object now looks as follows:
{
state: ‘users’,
translationsCategory: ‘users’,
config: {
controller: ‘usersController’
controllerAs: ‘vm’,
url: ‘/users’.
templateUrl: ‘users.html'
}
So what I did:
I implemented the resolveAlways object which takes the custom translationsCategory property, injects the Translations service and resolves the necessary data. Now no need to do it everytime.
I got a routeProvider for my states.
$routeProvider.
when("/register",{
templateUrl: "templates/register.html",
controller: "RegisterCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}).
when("/home",{
templateUrl: "templates/home.html",
controller: "HomeCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}). .... [.....]
Every state got a promise which resolves, when user-state is loggedIn. Then the code of the different controllers is executed. Now I want to have a mainController for the navigation bar, which should be present on all sites. The controller needs the userdata for checking for new messages etc.
Now: how is it possible to define the resolve globally in a root state (so i can access the userdata in the root controller for all sites) and all the other controllers execute their code only, if the promise from this roote state is resolved?
I hope I formulated my question understandable...
I think you're looking for something like $routeChangeStart, that is a way to execute something you want everytime the user changes his route inside your web app. Take a look at Route and this other question from stackoverflow. Hope it helps.
You can do this by defining your routes outside of the $routeProvider.when statements:
var routes = [
{
url: "/register",
config: {
templateUrl: "templates/register.html",
controller: "RegisterCtrl"
}
},
{
url: "/home",
config: {
templateUrl: "templates/home.html",
controller: "HomeCtrl"
}
}
];
Then iterating through your routes to extend the resolve property before registering them with the $routeProvider:
angular.forEach(routes, function (route) {
var url = route.url;
var routeConfig = route.config;
routeConfig.resolve = angular.extend(routeConfig.resolve || {}, {
// add your global resolves here
user: function(Auth) {
return Auth.resolveUser();
}
});
$routeProvider.when(url, routeConfig);
});
Your Auth.resolveUser() should be responsible for returning the fulfilled promise if it was already resolved previously.
I am currently setting up resolves for my admin panel routes and am wondering what the best way of storing them is as ideally I don't want to have my router filled with methods like so:
when('/admin', {
templateUrl: 'app/private/admin/view.html',
controller: 'admin',
resolve: ['$q', '$location', 'api', function($q, $location, api){
var deferred = $q.defer(),
session = api.session();
if(session){
deferred.resolve(session);
} else {
api.authorise().success(function(response){
deferred.resolve(response);
}).error(function(error){
$location.path('/login');
deferred.reject(error);
});
}
return deferred.promise;
}]
})
I think an ideal structure would be to store the resolves in the controller I'm using for that route, so something like:
when('/admin', {
templateUrl: 'app/private/admin/view.html',
controller: 'admin',
resolve: adminCtrl.resolve
})
However the admin controller is not accessible from the config so this leaves me with having to use a provider which is still going to be messy when expanding my application.
How do you all handle your resolves/is it possible to store it in my controller?
I usually use services for the things I want to get resolved:
when('/admin', {
templateUrl: 'app/private/admin/view.html',
controller: 'admin',
resolve: { adminData: function(myService) { return myService.list(); } }
});
More advanced angular routers like UI-router allow for states to inherit from parent-states. If you want to have a resolve in multiple states you could use inheritance, and define the resolve in your parent-state. (https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views).
I was trying to wrap my head around ui-router and I've tried to implement the following logic:
if there is no state, go to state /items
when processing /items, retrieve a list of "items" from the server
when "items" are received go to state /items/:item, where "item" is the first in the list of items, returned by the server
in state /items/:item render a list of items with the corresponding "item" being "highlighted" (the highlighting part is not included in my code)
However, the child state's "controller" function is not executed. I bet it's something really obvious.
Here's the js (I also have it on plunkr with the accompanying templates).
angular.module('uiproblem', ['ui.router'])
.config(['$stateProvider', '$urlRouterProvider',
function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/items');
$stateProvider
.state('items', {
url: '/items',
resolve: {
items: function($q, $timeout){
var deferred = $q.defer();
$timeout(function() {
deferred.resolve([5, 3, 6]);
}, 1000);
return deferred.promise;
}
},
controller: function($state, items) {
// We get to this point successfully
console.log(items);
if (items.length) {
// Attempt to transfer to child state
return $state.go('items.current', {id: items[0]});
}
}
})
.state('items.current', {
url: '/:id',
templateUrl: 'item.html',
controller: function($scope, items) {
// This is never reached, but the I can see the partial being
// loaded.
console.log(items);
// I expect "items" to reflect to value, to which the "items"
// promise resolved during processing of parent state.
$scope.items = items;
}
});
}]);
Plunk: http://plnkr.co/edit/K2uiRKFqe2u5kbtTKTOH
Add this to your items state:
template: "<ui-view></ui-view>",
States in UI-Router are hierarchical, and so are their views. As items.current is a child of items, so is it's template. Therefore, the child template expects to have a parent ui-view to load into.
If you prefer to have the child view replace the parent view, change the config for items.current to the following:
{
url: '/:id',
views: {
"#": {
templateUrl: 'item.html',
controller: function($scope, items) {
// ...
}
}
}
}