Angular ui-router resolve inheritance - javascript

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

Sub-views in Angular UI router and Controller inheritance?

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.

AngularJS ui-router: how to resolve typical data globally for all routes?

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.

Angular one resolve for all states

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.

Best place to store resolve methods in angularJs

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).

Child state controller is never executed

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) {
// ...
}
}
}
}

Categories