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.
Related
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)
I know it sounds counter-intuitive, but please hear my situation out. I have a "topbar" view and a "main" view. The topbar has no specific data associated with any one particular view, but it can change depending on the user's login status. This really helps to keep that part of my website robust, however it comes at a terrible cost: When I want to use "resolve", the topbar goes completely missing. I understand the logic behind this, but I am wondering if there is anyway to target which view to actually block with resolve?
If I could only block my "main" view, but allow my "topbar" view to render, then that would be perfect. I also don't want to avoid using resolve to avoid the main view flashing nonsense when the view is first loaded.
What my routes look like:
$stateProvider..state("about", {
url: "/about",
views: {
"topbar": {
templateUrl: "/app/templates/topnav.html"
},
"main": {
templateUrl: "/app/home/about.html",
controller: ["$rootScope", function ($rootScope) {
$rootScope.pageTitle = "About";
}]
}
}
}).state("courses", {
url: "/courses",
views: {
"topbar": {
templateUrl: "/app/templates/topnav-loggedin.html"
},
"main": {
templateUrl: "/app/courses/courses.html",
controller: "coursesController"
}
},
resolve: {
currentCourse: ["$http", function($http) {
// load course
}]
}
});
I'd probably make a parent, abstract state for all states requiring the logged-in topbar template. For example
.state('loggedin', {
abstract: true,
views: {
topbar: {
templateUrl: '/app/templates/topnav-loggedin.html'
}
}
}).state('courses', {
parent: 'loggedin', // or name the state 'loggedin.courses'
url: '/courses',
views: {
'main#': { // the "main" view in the root state
templateUrl: '/app/courses/courses.html',
controller: 'coursesController'
}
},
resolve: { ... }
});
I design my SPA like this:
angular.module('app', ['submodule0', 'submodule1']);
Main module:
$stateProvider.state("sub0index", {
url: "/sub0",
// pass states defined in submodule0, is that possible?
}).state("sub1index", {
url: "/sub1",
// pass states defined in submodule1
})
And here are some states defined in submodule0
$stateProvider.state("index", {
url: "/index",
templateUrl: "template/index.html"
}).state("info", {
url: "/info",
templateUrl: "template/info.html"
})
So is that possible that I pass sub-state from sub-module to the main module? I ask this because now I define all my state in my main module, I think it may be more elegant to define the state of one submodule in the submodule itself.
And another question is: I'm not sure my module design is reasonable or not, is my submodules not necessary? Or just keep my whole app logic to one module? Thanks.
====Edited====
And here is the problem I've met.
var app = angular.module('test', ['ui.router', 'app.sub']);
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a",
views: {
"general": {
templateUrl: "/template.html"
}
},
resolve: {
data: 'GetDataService'
}
});
}
The service GetDataService is defined in my submodule app.sub, and here is the service:
angular.module('app.sub',['ui.router'])
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null; // return null just for demo
}]);
The output of console.log($stateParams) is an empty object. But if use the service which is defined in its own module, the current state can be get correctly. So whats the issue?
===Edit===
Thanks for the example, it works fine if give a factory to data directly. But how about I give it a string?
I check the document of ui-router, and there is something about map object in resolve:
factory - {string|function}: If string then it is alias for service.
So if I use the code like this:
resolve: {
data: "GetDataService"
}
And the definition of GetDataService:
.service('GetDataService', ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}])
But output of console.log($stateParams) is always an empty object.
Do I have some misunderstanding about the api document?
===Edit again===
If I use code like this:
resolve: {
// data: "GetDataService"
data: ['$stateParams', function($stateParams) {
console.log($stateParams);
return null;
}]
}
I can get the params object.
I would say, that modules should not stop us... we can split the app into many if needed.
But I would suggest: Services should be independent on $state.current. We should pass to them function parameters as needed, but these should be resolved outside of the Service body.
Bette would be to show it in action - there is one working example
This is the service:
angular.module('app.sub',['ui.router'])
.service('DataService', ['$state', function($state) {
return {
get: function(stateName, params){
console.log(stateName);
console.log(params);
return stateName;
}
}
}]);
And here is some adjsuted state def:
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: "/a/{param1}",
views: {
"general": {
templateUrl: "tpl.html"
}
},
resolve: {
data: ['DataService','$stateParams'
, function(DataService,$stateParams, $state){
return DataService.get('index', $stateParams)
}],
},
});
}])
Hope it helps a bit. The plunker link
Because this approach is ready to test service without any dependency on some "external" $state.current. We can just pass dummy, testing params
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'm currently working on an app where I have multiple nested views, they sort of look like this:
- ui-view
- ui-view="header"
- ui-view="nav"
- ui-view="body"
My states are defined as follows:
.state('index', {
url: '', // default route
templateUrl: 'welcome.html'
})
.state('app', {
abstract: true,
templateUrl: 'app.template.html' // This template contains the 3 different ui-views
})
// I'm using a different state here so I can set the navigation and header by default
.state('in-app', {
parent: 'app',
abstract: true,
views: {
'nav#app': { '...' },
'header#app': { '...' }
}
})
// In-app routes
.state('dashboard', {
parent: 'in-app',
url: '/app/dashboard'
views: {
'body#app': { '...' }
}
})
.state('users', {
parent: 'in-app',
url: '/app/users'
views: {
'body#app': { '...' }
}
})
.state('settings', {
parent: 'in-app',
url: '/app/settings'
views: {
'body#app': { '...' }
}
})
At the moment this works great, but for the in-app routes I would like to define a title that is displayed in the header#app view.
What would be the best way to do this? At the moment I can only think of either setting a variable on the $rootScope, or sending out an event. But for both of those I would need a controller.
Is there a way I could do this directly from my routes config?
The sample applicaiton of the UI-Router, uses this code:
ui-router / sample / app / app.js
.run(
[ '$rootScope', '$state', '$stateParams',
function ($rootScope, $state, $stateParams) {
// It's very handy to add references to $state and $stateParams to the $rootScope
// so that you can access them from any scope within your applications.For example,
// <li ng-class="{ active: $state.includes('contacts.list') }"> will set the <li>
// to active whenever 'contacts.list' or one of its decendents is active.
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
}])
And that means, that with data : {} feature:
Attach Custom Data to State Objects
You can attach custom data to the state object (we recommend using a data property to avoid conflicts).
// Example shows an object-based state and a string-based state
var contacts = {
name: 'contacts',
templateUrl: 'contacts.html',
data: {
customData1: 5,
customData2: "blue"
}
}
we can do this:
.state('in-app', {
parent: 'app',
abstract: true,
views: {
'nav#app': { '...' },
'header#app': { '...' }
}
data: { title : "my title" },
})
And use it in some template like:
<div>{{$state.current.data.title}}</div>
Some summary.
We can place state and params into $rootScope, so we can access it without any controller anyhwere.
We can declare some more custom stuff via data and use it as a title ... anyhwere