UI-router accessing parent state from child - javascript

I'd like to set the title based on the state in my angular app. It works nicely, but if there's no title defined on the child state, I'd like to fall back to the parent's title property.
This is what I have so far:
In my app run block:
...
$rootScope.$on('$stateChangeSuccess', function (event, current, previous) {
if (current.hasOwnProperty('title')) {
$rootScope.title = current.title;
}
});
...
In my module's route
.state('main.parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'vm',
templateUrl: 'app/parent.html',
title: 'Parent',
})
.state('main.parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'vm',
templateUrl: 'app/child.html'
})
During the code inspection, I found that $stateChangeSuccess is triggered twice as expected, first for the parent and then for the child. I tried to access current.parent when the child was called, but it has no such property. Is there any way to access it?
UPDATE
I saw these questions:
get parent state from ui-router $stateChangeStart,
get parent state from ui-router $stateChangeStart
But none of them had a clear answer to this problem (except the state splitting hack in the latter one but I'd rather avoid that).

The both ways are hacks a bit.
Don't forget to import $state (and $stateParams for 2nd variant) providers in your controller.
You can use:
$state.$current.parent.self.title
or
var tree = current.name.split('.');
tree.splice(tree.length-1, 1);
var parent = tree.join('.');
$state.get(parent, $stateParams);

It seems that you just need the title property to fall back to the parent's title property if there's no title defined on the child state, I will suggest you to use this repository: angular-ui-router-title.

So finally I came up with the following solution:
I store title inside the state's data object, because from $state you can't access the parent's custom properties, only the ones stored in data (which makes sense - not to pollute the state definition with custom properties).
.state('main.parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'vm',
templateUrl: 'app/parent.html',
data: {
title: 'Parent'
}
})
.state('main.parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'vm',
templateUrl: 'app/child.html'
})
And I changed the runblock as follows:
...
$rootScope.$on('$stateChangeSuccess', function (event, toState, fromState) {
var current = $state.$current;
if (current.data.hasOwnProperty('title')) {
$rootScope.title = current.data.title;
} else if(current.parent && current.parent.data.hasOwnProperty('title')) {
$rootScope.title = current.parent.data.title;
} else {
$rootScope.title = null;
}
});
...
This is not the perfect solution yet, as in the case of multiple nested states it'd be nice to fall back to the title of the youngest ancestor's title but it does it for now.

Simply below code is working for me
$state.$current.parent

Looking at the UI-Router documentation in the "to" section, you could do something like below:
$state.go('^');
to go back to the parent state.

Related

How to do variable routing in angular js

I have this problem & I am unable to find the solution for it.
This is an example of code where I am trying to route to variable URL routing
$routeProvider.when('/Book', {
template: 'examples/book.html',
controller: BookCntl,
});
$routeProvider.when('/Book/chapter01', {
template: 'examples/chapter01.html',
controller: ChapterCntl,
});
If I want to fix the url till /Book/chapter and 01 can be a variable. Like if user changes 02 or 03 till 100. Do I need to write the $routeProvider 100 times or can be a simple solution, where I can use the number part as a variable and write single $routeProvider to handle 01 to 100?
No, you do not need to add 100 seperate route definitions. You add a variable to your url template by adding /:some_variable, and then you are to fetch that variable by using the $routeParams service.
Example
$routeProvider.when('/Book/chapter/:chapterid', {
templateUrl: 'examples/chapter-view.html',
controller: ChapterCntl,
});
And then inject $routeParams into your controller:
function ChapterCntl($routeParams) {
var chapterId = $routeParams.chapterid;
//use the id to fetch content.
}
It does seem like you have a different html page for each chapter. If that is the case you can set a function to the template field to generate the path for the html file:
$routeProvider.when('/Book/chapter/:chapterid', {
template: function(routeParams) {
var id = routeParams.id;
return 'examples/chapter'+id+'.html';
},
controller: ChapterCntl,
});
If that case is that you are fetching the data from an API through a service, it might be useful to be using the resolve field instead. The resolve field will loaded the data and be injectable into the controller. Which means that the data will be loaded before transitioning in to the new route.
$routeProvider.when('/Book/chapter/:chapterid', {
templateUrl: 'examples/chapter-view.html',
controller: ChapterCntl,
//Will run the below function before transitioning into new route.
resolve: {
chapter: function($routeParams, chaptersDataService) {
var id = $routeParams.chapterid;
return chaptersDataService.getChapter(id);
}
}
});
And the inject the chapter into your controller:
function ChapterCntl($scope, chapter) {
$scope.chapter = chapter;
console.log( chapter );
}
Have you considered UI Route Provider? You could easily use stateparams..
$stateProvider
.state('book.chapter', {
url: "/book/chapter/:chapterId",
templateUrl: 'book.chapter.detail.html',
controller: function ($stateParams) {
....
}
})
Sources:
https://github.com/angular-ui/ui-router/wiki/url-routing#url-parameters
http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider
You could also stick with routeprovider in a slightly different way than suggested in other answers.
$routeProvider.when('/Book/:chapter', {
templateUrl : { function (dynamicUrl) {
return '/Book/' + dynamicUrl.chapter + '.html';
},
controller: 'ChapterCntl'
});

Angular UI-router: ok to leave first child empty? Or hidden?

I want to have a nested view with UI router, which I've done before (see image) with a main section and then a nav which loads sub-sections into the nested UI-View. This I can do, no issues.
My question is: this time I need to have the initial child state not show to the user until a button is clicked, like this:
Can I do this? Or is it better to load the "baseball" view but hide it and the nav with ng-hide?
UPDATE
Someone asked how I would do the simple nested states in a case like this:
(function() {
'use strict';
angular.module('elements').config(function($stateProvider) {
$stateProvider.state('elements', {
url: '/elements',
abstract: true,
templateUrl: 'modules/elements/templates/elements.html',
controller: 'ElementsController as elements'
})
.state('elements.buttons', {
url: '/elements/buttons',
templateUrl: 'modules/elements/templates/elements-buttons.html'
})
.state('elements.accordion', {
url: '/elements/accordion',
templateUrl: 'modules/elements/templates/elements-accordion.html',
controller: 'AccordionController as accordion'
})
.state('elements.colorcharts', {
url: '/elements/colorcharts',
templateUrl: 'modules/elements/templates/elements-colors-charts.html',
controller: 'ChartColorsController as charts'
})
.state('elements.grid', {
url: '/elements/grid',
templateUrl: 'modules/elements/templates/elements-grid.html'
});
});
})();
Yes it is absolutely possible. I usually accomplish this by using programmatically defined states, which looks like it should work for your situation.
If you have a state for baseball then you could control it as such:
state config
.state('baseball', {
url: '/views/baseball',
template: 'imabaseball!'
})
html
<div ui-view="{{state}}">
<button ng-click="state = 'baseball'">Show Baseball</button>
Then the view in question would not be rendered until the user clicked the button

Access url params in master controller

My application has two controllers. I have a pageCtrlwhich I use to handle my navigation and sidebar. I also have a calendarCtrl for handling the data on my page. This controller is configured as shown below:
$stateProvider
.state('calendar', {
url: '/calendar/:category/:competition/:team',
controller: 'calendarCtrl',
templateUrl: 'app/modules/calendar/calendarView.html',
})
To make my navigation work I also need access to the :category/:competition/:team-params in my pageCtrl. Can I configure this using the same way? Something like:
$stateProvider
.state("page", {
abstract: true,
controller: 'pageCtrl',
// params: :category/:competition/:team
})
Edit: Using $stateParams in the calendarCtrl works fine. I just can't figure out how I can make sure my pageCtrl also can get read the url.
Since you're using ui.router, inject $stateParams in your controller(s) and then you can access those values like so:
controller.js
function($stateParams){
$stateParams.category
$stateParams.competition
$stateParams.team
My suggestion would be - use more views - the UI-Router built feature.
Multiple Named Views
There is a working plunker
Let's have the 'Parent' state which has this template:
This blue is the parent template. Orange are child views
<!-- HERE is one named view target -->
<div ui-view="title">This is a title filled by child having access to param</div>
...
<!-- HERE is other view target un-named -->
<div ui-view></div>
And its state is very simple. The interesting is the child state, which is taking care about both views:
.state('parent', {
abstract: true,
url: "/parent",
templateUrl: 'tpl.parent.html',
})
.state('parent.child', {
url: "/child/:id",
views : {
'': {
templateUrl: 'tpl.child.html',
},
'title': {
templateUrl: 'tpl.title.html',
controller: 'TitleCtrl',
},
}
})
So, we do have a target for "some other view" title or side bar. Check it here
And we can even place some default implementation there inside of our "non-abstract" parent state.
There is extended plunker with non abstract parent state definition:
.state('parent', {
url: "/parent",
views : {
'': {
templateUrl: 'tpl.parent.html',
},
'title#parent': {
template: 'the parent own TITLE',
},
}
})
Check it here
There is a way, how to grant access to latest/up-to-date $stateParams - including current state and its child(ren) as well. (working example here)
It is surprisingly easy:
.run(['$rootScope', '$state', '$stateParams',
function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
}])
And that's it. (check similar answer and some discussion here)
With this approach, we will even in parent $scopes have updated reference to the latest $stateParams. While in our own, we will still receive just our own part
.controller('ParentCtrl', ['$scope', '$stateParams', function ($scope, $stateParams) {
$scope.currentStateParams = $stateParams;
}])
The above is valid for states like these:
.state('parent', {
url: "/parent?area",
templateUrl: 'tpl.html',
controller: 'ParentCtrl',
})
.state('parent.child', {
url: "/child/:id",
templateUrl: 'tpl.html',
controller: 'ChildCtrl',
})
Working example to play here.
But I still would say, that this is a bit ... against the UI-Router. I would prefer this answer. Because in that case, each view (while injected into some parent area) is really aware about $stateParams, which belongs to that state. What we are doing here is introduction of some observer pattern (we should watch changes if we want to react in parent) and that would later bring more issues then profit

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 UI-Router passing data between states with go function does not work

I just started learning angularjs and I am using angular-ui-router. I am trying to send data from one state to another using $state.go but I have no success. Here is what I have so far:
I have not included the html intentionally because I assumed it was not needed if it is needed please tell me and I will add it.
I have configured my states as below:
$stateProvider
.state('public', {
abstract: true,
templateUrl: 'App/scripts/main/views/PublicContentParent.html'
})
.state('public.login', {
url: '/login',
templateUrl: 'App/scripts/login/views/login.html',
controller: 'loginCtrl'
})
$stateProvider
.state('private', {
abstract: true,
templateUrl: 'App/scripts/main/views/PrivateContentParent.html'
})
.state('private.visits', {
url: '/visits',
views: {
'main': {
controller: 'visitsListCtrl',
templateUrl: 'App/scripts/visits/views/VisitsList.html'
}
}
});
When my LoginController is invoked it will execute the below code:
loginModule.controller('loginCtrl', ['$state', function ($scope, $state) {
$state.go('private.visits', { name : "Object"});
}]);
When the private.visits page is active, I am trying to print the $stateParams:
visitsModule.controller('visitsListCtrl', ['$stateParams',
function ($stateParams) {
console.log($stateParams);
}]);
As things state $stateParams is an empty object. I expected it to to contain the object I passed in loginCtrl.
EDIT
It seems that if private.visits url has this url format '/visits/:name' and I also add the property params: ["name"] I get access to the object I send from the public.login state.
The side effect is that the parameters are added to the url which is logical.
I tried doing the same thing with a child state with no url, and in this case it seems that I do not have access to the params I passed from public.login.
How do I send data in child states?
What you have to do is to define the name param in the private.visits state like:
$stateProvider
.state('public', {
abstract: true,
templateUrl: 'App/scripts/main/views/PublicContentParent.html'
})
.state('public.login', {
url: '/login',
templateUrl: 'App/scripts/login/views/login.html',
controller: 'loginCtrl'
})
$stateProvider
.state('private', {
abstract: true,
templateUrl: 'App/scripts/main/views/PrivateContentParent.html'
})
.state('private.visits', {
// NOTE!
// Previously, we were only passing params as an array
// now we are sending it as an object. If you
// send this as an array it will throw an error
// stating id.match is not a function, so we updated
// this code. For further explanation please visit this
// url http://stackoverflow.com/a/26204095/1132354
params: {'name': null},
url: '/visits',
views: {
'main': {
controller: 'visitsListCtrl',
templateUrl: 'App/scripts/visits/views/VisitsList.html',
resolve: {
name: ['$stateParams', function($stateParams) {
return $stateParams.name;
}]
}
}
}
});
And then in the controller access to the name:
visitsModule.controller('visitsListCtrl', ['name',
function (name) {
console.log(name);
}]);
Hope it help you!
When you say:
$state.go('private.visits', { name : "Object"});
You're not passing data to the private.visits state, but rather you're setting a parameter to the private.visits state, which doesn't even support parameters as you have not defined parameters for it in the state config. If you want to share data between states use a service, or if your states have a parent-child relationship then the child state will have access to the parent states data. Seeing as how you don't want the data to sow up in your URLs, I would use a service (getters/setters) to achieve this.

Categories