Can a UI-Router parent state access it's child's members? - angularjs

I'm using AngularJS's UI-Router to manage routes for my web application.
I have two states: parent_state and child_state arranged as shown below.
$stateProvider
.state('parent_state', {
abstract: true,
views: {
'#' : {
templateUrl: 'http://example.com/parent.html',
controller: 'ParentCtrl'
}
}
})
.state('child_state', {
parent: 'parent_state',
url: '/child',
params: {
myArg: {value: null}
},
views: {
'mainarea#parent_state': {
templateUrl: 'http://example.com/child.html',
controller: 'ChildCtrl'
}
}
})
From within ChildCtrl, I can access myArg like this:
app.controller("ChildCtrl", function($stateParams) {
console.log('myArg = ', $stateParams.myArg);
});
Is it possible for me to access myArg and have it displayed in the html page parent.html? If so, how can it be done? I see that the ParentCtrl controller for the abstract state is never even called.
This question addresses a related topic. But it doesn't show me how to display a parameter to the child state in a template of the parent state.

The first thing that comes to my mind is to use events for notifying parent after child param change. See the following (you can even run it here).
Child, after rendering, emits an event to the parent with the changed value of the parameter. Parent grabs and displays it in its own template.
angular.module('myApp', ['ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('parent_state', {
abstract: true,
template: "<h1>Parent! Value from child: {{ paramFromChild }}</h1><div ui-view></div>",
controller: function ($scope) {
$scope.$on('childLoaded', function (e, param) {
$scope.paramFromChild = param;
});
}
})
.state('child_state', {
parent: 'parent_state',
url: '/child',
params: {
myArg: {value: null}
},
template: '<h2>Child! Value: {{ param }}</h2>',
controller: function($stateParams, $scope){
$scope.param = $stateParams.myArg;
$scope.$emit('childLoaded', $stateParams.myArg);
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.10/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.20/angular-ui-router.js"></script>
<div ng-app="myApp">
<a ui-sref="child_state({myArg: 'first'})">First link</a>
<a ui-sref="child_state({myArg: 'second'})">First second</a>
<div ui-view></div>
</div>

Is it possible for me to access myArg and have it displayed in the
html page parent.html?
That is against the principle of the UI-Router. Parent params can be consumed in children, but not vice versa. How would parent view know about changes WITHOUT re-initializing the controller? You need something like watching.
The true way is to employ Multiple Named Views. Look at this working plunkr.

Yes, this is possible.
Using $stateChangeSuccess:
You can use $stateChangeSuccess to achieve this.
For example:
.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 in the runblock call it 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;
}
});
Then you can access the $rootScope.title from the child controller since it is globally available.
Using a Factory or Service:
By writing setters and getters you can pass data between controllers. So, you can set the data from the child controller and get the data from the parent controller.
'use strict';
(function () {
var storeService = function () {
//Getters and Setters to keep the values in session
var headInfo = [];
return {
setData: function (key, data) {
headInfo[key] = data;
},
getData: function (key) {
return headInfo[key];
}
};
};
angular.module('MyApp')
.factory('StoreService', storeService);
})(angular);
Set data from child controller
StoreService.setData('title', $scope.title)
Get data
StoreService.getData('title');
Using events $emit, $on:
You can emit the scope value from the child controller and listen for it in the parent scope.

Related

How do I show multiple views in an abstract state template?

Using Angularjs UI-Router I'm trying to build a typical page with a main view and a side refine/filter results view.
I've forked the sample app and now modifying it to have a parent abstract state that displays the filters view and the main view so that I can share the data that the abstract state resolves.
The problem is I cannot get the filters view to appear without breaking the scope inheritance. If I add the 'views' param it breaks the scope so how can I get this to work?
Here's my code and Plunker
$stateProvider
.state('applications', {
abstract: true,
url: '/applications',
templateUrl: 'planning.html',
controller: function($scope){
$scope.planning = [{ id:0, name: "Alice" }, { id:1, name: "Bob" }];
$scope.filterPlanning = function(data) {
var output = $scope.planning;
// test filter
output = $filter('filter')({ name: "Alice" });
return output;
}
},
/* this breaks the scope from being inherited by the child views
views: {
'': {
templateUrl: 'applications.html'
},
'filters#applications': {
templateUrl: 'applications.filters.html'
}
},*/
onEnter: function(){
console.log("enter applications");
}
})
child states:
.state('applications.list', {
url: '/list',
// loaded into ui-view of parent's template
templateUrl: 'planning.list.html',
onEnter: function(){
console.log("enter applications.list");
}
})
.state('applications.map', {
url: '/map',
// loaded into ui-view of parent's template
templateUrl: 'planning.map.html',
onEnter: function(){
console.log("enter applications.map");
}
})
applications.html
<div class="row">
<div class="span4">
<div ui-view="filters"></div>
</div>
<div class="span8">
<div ui-view></div>
</div>
</div>
I already have the list, map and filters views built as directives so I'm hoping once I can get this demo working I can swap them in relatively easily.
There is updated plunker
I would say, that the issue here is:
Any controller belongs to view not to state
so this is the adjusted code:
.state('applications', {
abstract: true,
url: '/applications',
// templateUrl and controller here are SKIPPED
// not used
// templateUrl: 'applications.html',
// controller: ...
views: {
'': {
templateUrl: 'applications.html',
controller: function($scope, $filter){
$scope.planning = [
{ id:0, name: "Alice" },
{ id:1, name: "Bob" }
];
$scope.filterPlanning = function(data) {
var output = $scope.planning;
// test filter
output = $filter('filter')({ name: "Alice" });
return output;
}
},
},
'filters#applications': {
templateUrl: 'applications.filters.html'
}
},
Check the doc:
Views override state's template properties
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.
The plunker to check it in action

How to hide content in the parent view when the user goes to a nested state and show it again when the user backs to the parent one?

I use ui-router and have two nested views.
I’d like to hide some html-content in the parent view when the user goes to child state and show it again when the user backs to parent one.
Could anybody give an advice how to achieve that?
It’s easy to do that using root scope and state change events but it looks like a dirty way, doesn’t it?
app.js
'use strict';
var myApp = angular.module('myApp', ['ui.router']);
myApp.controller('ParentCtrl', function ($scope) {
$scope.hideIt = false;
});
myApp.controller('ChildCtrl', function ( $scope) {
$scope.$parent.hideIt = true;
});
myApp.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('parent', {
url: '/parent',
templateUrl: 'parent.html',
controller: 'ParentCtrl'
})
.state('parent.child', {
url: '/child',
template: '<h2>Child view</h2>',
controller: 'ChildCtrl'
});
});
parent.html
<h2>Parent view</h2>
<div ng-hide="hideIt">
<p>This text should be hidden when the user goes to the nested state.</p>
</div>
<a ui-sref="parent.child">Go to the nested view</a>
<div ui-view></div>
One simple solution is to fill ui-view tag in the parent template with the content that you want gone when child state is loaded.
<ui-view>
<h2>Parent view</h2>
<div ng-hide="hideIt">
<p>This text should be hidden when the user goes to the nested state.</p>
<a ui-sref="parent.child">Go to the nested view</a>
</ui-view>
You should check out named views for this. That would probably be the best way to go. https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views
Also, there's another thread that answered this question over here: https://stackoverflow.com/a/19050828/1078450
Here's the working code for nested named views, taken from that thread:
angular.module('myApp', ['ui.state'])
.config(['$stateProvider', '$routeProvider',
function($stateProvider, $routeProvider) {
$stateProvider
.state('test', {
abstract: true,
url: '/test',
views: {
'main': {
template: '<h1>Hello!!!</h1>' +
'<div ui-view="view1"></div>' +
'<div ui-view="view2"></div>'
}
}
})
.state('test.subs', {
url: '',
views: {
'view1#test': {
template: "Im View1"
},
'view2#test': {
template: "Im View2"
}
}
});
}
])
.run(['$state', function($state) {
$state.transitionTo('test.subs');
}]);
http://jsfiddle.net/thardy/eD3MU/
Edit:
Adding some thoughts re the angular-breadcrumbs comment. I haven't used it myself, but at first glance it seems like subroutes shouldn't break the breadcrumbs. I'm just looking at the source code of their demo, around line 62. I'd have to spin it all up to really go about testing it, but it looks like they're doing almost the same thing with specifying views, and it works there: https://github.com/ncuillery/angular-breadcrumb/blob/master/sample/app.js#L62
.state('room', {
url: '/room',
templateUrl: 'views/room_list.html',
controller: 'RoomListCtrl',
ncyBreadcrumb: {
label: 'Rooms',
parent: 'sample'
}
})
.state('room.new', {
url: '/new',
views: {
"#" : {
templateUrl: 'views/room_form.html',
controller: 'RoomDetailCtrl'
}
},
ncyBreadcrumb: {
label: 'New room'
}
})
.state('room.detail', {
url: '/{roomId}?from',
views: {
"#" : {
templateUrl: 'views/room_detail.html',
controller: 'RoomDetailCtrl'
}
},
ncyBreadcrumb: {
label: 'Room {{room.roomNumber}}',
parent: function ($scope) {
return $scope.from || 'room';
}
}
})
Edit2:
This solution will not combine routes into one crumb
See the official demo
re: But I use angular-breadcrumb and in your solution they will be combined into one crum.

Getting the name of a state in its `onEnter` hook

I'm building an application where I want to toggle a property in a service the moment a user enters and leaves a route. To do this I need to know about the state's name in the onEnter and onExit hooks. This is relatively easy for the onExit hook since I can just inject the $state service and read the name of the current state. But since the current state has not been set yet when the onEnter hook is called there is no way of knowing what the state we're transitioning to.
I still need to to have fine control over other parts of the state so I'd rather not have any for loops. I'm looking for a way to be able to pass the onEnter function to the state, whilst still retrieving the state's name inside of the function itself.
Here is the code I've written:
function onEnter($state, Steps) {
var stateName = $state.current.name; // Not possible. The current state has not been set yet!
var step = Steps.getByStateName(stateName);
step.activate();
}
function onExit($state, Steps) {
var stateName = $state.current.name; // No problem. We know about the state.
var step = Steps.getByStateName(stateName);
step.deactivate();
}
$stateProvider
.state('step1', {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: onEnter,
onExit: onExit
});
My solution I'm using for now is to use a factory to create context for the onEnter function passed to the state. This is far from ideal because I still need to pass the state's name to it.
Here is an example of said workaround:
function onEnterFactory(stateName) {
return function onEnter(Steps) {
var step = Steps.getByStateName(stateName);
step.activate();
}
}
$stateProvider
.state('step1', {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: onEnterFactory('step1')
});
Use this in onEnter onExit hooks. onEnter is invoked by following command:
$injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals);
The second paramater of $injector.invoke is the value of this for the function it calls. So your code should look as follows:
function onEnter(Steps) {
var stateName = this.name;
var step = Steps.getByStateName(stateName);
step.activate();
}
function onExit(Steps) {
var stateName = this.name;
var step = Steps.getByStateName(stateName);
step.deactivate();
}
$stateProvider
.state('step1', {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: onEnter,
onExit: onExit
});
Here is a working example of accessing a state's name in the onEnter and onExit hooks:
angular.module('myApp', ['ui.router'])
.config(function($stateProvider) {
function onEnter() {
console.log('Entering state', this.name);
}
function onExit() {
console.log('Exiting state', this.name);
}
$stateProvider.state('state-1', {
url: '/state-1',
template: '<p>State 1</p>',
onEnter: onEnter,
onExit: onExit
}).state('state-2', {
url: '/state-2',
template: '<p>State 2</p>',
onEnter: onEnter,
onExit: onExit
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.6/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.3.1/angular-ui-router.js"></script>
<div ng-app="myApp">
<nav>
<a ui-sref="state-1">State 1</a>
<a ui-sref="state-2">State 2</a>
</nav>
<div ui-view></div>
</div>
In one of my projects we used something like this
app.run(function($rootScope, $state, $location) {
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams,
fromState) {
$state.previous = fromState;
});
to remember the previous state. But you might as well remember the new state and store the information somewhere.
You already know which state it will be, because you define it in the .state('statename',. To not write the same name twice, you can define the state variables beforehand:
var steps = ["step1", "step2", "step3"]; // or, something like Steps.getSteps()
$stateProvider
.state(steps[0], {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: function(Steps) {
var stateName = steps[0];
var step = Steps.getByStateName(stateName);
step.activate();
},
onExit: function($state, Steps) {
var stateName = $state.current.name; // No problem. We know about the state.
var step = Steps.getByStateName(stateName);
step.deactivate();
}
});
You can even make it dynamic this way:
var steps = [
{ name: "step1", url: "/step1" },
{ name: "step2", url: "/step2" },
{ name: "step3", url: "/step3" }
]; // Or something like Steps.getSteps();
for (var i = 0; i < states.length; i++) {
var state = steps[i];
$stateProvider.state(state.name,
url: state.url,
templateUrl: "templates/" + state.name + ".html",
onEnter: function(Steps) {
var step = Steps.getByStateName(state.name);
step.activate();
// or just: state.activate();
},
onExit: function($state, Steps) {
var stateName = $state.current.name; // No problem. We know about the state.
var step = Steps.getByStateName(stateName);
step.deactivate();
}
}
You could extend your factory solution a little bit and make it more flexible.
Maybe have a provider that reacts to the state changes.Then you could just inject this provider/service to onEnter function or where-ever you may need it.
Related plunker here http://plnkr.co/edit/6Ri2hE
angular.module('app', ['ui.router'])
.provider('myState', function myStateProvider() {
var state;
this.onEnter = function() {
console.log('provider.onEnter', state);
};
this.$get = function($rootScope) {
var myState = {};
myState.initialize = function() {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
state = toState;
});
};
myState.getState = function() {
return state;
};
return myState;
};
})
.config(function($stateProvider, myStateProvider) {
$stateProvider
.state('step1', {
url: '/step1',
template: '<div>step1 template</div>',
controller: function() {},
onEnter: myStateProvider.onEnter // usage example
})
.state('step2', {
url: '/step2',
template: '<div>step2 template</div>',
controller: function() {},
onEnter: function(myState) { // other usage example
console.log('state.onEnter', myState.getState());
}
});
})
.run(function(myState) {
myState.initialize();
});
<a ui-sref="step1">state:step1</a>
<a ui-sref="step2">state:step2</a>
<div ui-view></div>
This would console.log() the following, if links are clicked sequentially.
Add a 'name' property naming the state:
$stateProvider
.state('step1', {
name: 'step1' // <- property naming the state
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: onEnter
});
The route's name is then accessible by this.name in the onEnter callback:
function onEnter() {
var stateName = this.name; // <- Retrieve the state's name from its configuration
var step = Steps.getByStateName(stateName);
step.activate();
}
To not write the same state name twice, you could start by defining the states in a separate object and enriching the states with their name before adding them to the $stateProvider:
var routes = {
"step1": {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
onEnter: onEnter
}
};
for(var routeName in routes) {
var route = routes[routeName];
// Enrich route with its name before feeding
// it to the $stateProvider
route.name = routeName;
$stateProvider.state(route.name, route);
}
Maybe you can use resolve,
$stateProvider
.state('step1', {
url: '/step1',
templateUrl: 'templates/step1.html',
controller: 'StepOneController',
resolve: {
onenter : function( Steps ) {
// use this.self.name to access state name
var step = Steps.getByStateName(this.self.name);
step.activate();
}
}
} );
If the above this approach seems unclean then maybe one can use decorator to populate current state.
angular.config(function($provide) {
$provide.decorator('$state', function($delegate, $rootScope) {
$rootScope.$on('$stateChangeStart', function(event, state) {
$delegate.next = state;
});
return $delegate;
});
});
State will be available in $state.next inside resolve function.

Updating resolved objects in ui.router parent states

My question is two fold:
1 - I have a parent state and several child states using ui.router, there's one object (stored in mongodb) that is need in all states, but it gets updated by all states. In this scenario does it make sense to use the parent state's resolve option to populate the object?
2 - If this is the proper way to do this, how can I update that "reference" (the mock service injector created by the ui.router) to that object from every state.
To help in explain he's a example of the idea (lot's of code ommited)
.state('parent',resolve:{objectX:return x.promise;},...);
.controller('childstateCtrl',$scope,objectX){
$scope.data.objectX = objectX;
$scope.someEvent =function(){
// something updates objectX on the scope
}
}
.controller('otherChildCtrl',$scope,objectX){
// how to get the updated objectX?
}
Thanks in advance
Not fully sure if I can see where is the issue... but if you are searching for a way how to share access to updated reference, it should be easy. There is an example
Let's have these states
$stateProvider
.state('root', {
abstract: true,
template: '<div ui-view></div>',
resolve: {objectX : function() { return {x : 'x', y : 'y'};}},
controller: 'rootController',
})
.state('home', {
parent: "root",
url: '/home',
templateUrl: 'tpl.example.html',
})
.state('search', {
parent: "root",
url: '/search',
templateUrl: 'tpl.example.html',
})
.state('index', {
parent: "root",
url: '/index',
templateUrl: 'tpl.example.html',
})
Working with only one controller (for a root state):
.controller('rootController', function($scope, objectX){
$scope.data = { objectX: objectX };
})
And for this example, this is shared template:
<div>
<h3>{{state.current.name}}</3>
x <input ng-model="data.objectX.x"/>
y <input ng-model="data.objectX.y"/>
</div>
So, in this scenario, parent (root) has injected an object data into $scope. That reference is then inherit as described here:
Scope Inheritance by View Hierarchy Only
Check that example in action here. If you need more details (than in the link above, check this Q&A)
you could store it in a service.
.service("myService", function($q) {
// the actual data is stored in a closure variable
var data = undefined;
return {
getPromise: function() { // promise for some data
if (data === undefined) // nothing set yet, go fetch it
return $http.get('resourceurl').then(function(value) { data = value; return data; });
else
return $q.when(data); // already set, just wrap in a promise.
},
getData: function() { return data; }, // get current data (not wrapped)
setData: function(newDataVal) { data = newDataVal; } // reset current data
}
})
// `parent` wont' enter until getPromise() is resolved.
.state('parent', resolve:{objectX: function(myService) { return myService.getPromise(); } });
.controller('childstateCtrl', $scope, myService) {
$scope.data.objectX = myService.getData();
$scope.someEvent = function(someData){
myService.setData(someData);
}
}
.controller('otherChildCtrl', $scope, myService){
// how to get the updated objectX?
var data = myService.getData();
}

Can a controller inherit scope from a parent controller when using ui-router

I have the following:
var admin = {
name: 'admin',
url: '/admin',
views: {
'nav-sub': {
templateUrl: '/Content/app/admin/partials/nav-sub.html',
controller: function ($scope) { $scope.message = "hello"; }
}
},
controller: ['$scope', function ($scope) {
$scope.message = "hello";
}]
}
var subject = {
name: 'subject',
parent: admin,
url: '/subject',
views: {
'grid#': {
templateUrl: '/Content/app/admin/partials/grid-subject.html',
controller: 'AdminGridSubjectController',
}
}
};
I would like the AdminGridSubjectController to know what the $scope.message value is but it seems not to know anything about it. Is there something I am doing wrong?
stApp.controller('AdminGridSubjectController', ['$scope', function ( $scope ) {
var a = $scope.message;
}]);
In order to access the scope of a parent controller in Angular UI Router use:
$scope.$parent
Then the parent scope is then freely available to you.
Your problem might be that the name should reflect the parent in it:
var subject = {
name: 'admin.subject',
parent: admin,
url: '/subject',
...
Here's a complete illustration of how to inherit $scope with ui-router: plunker ex
There are several ways (and workarounds) to access parent scope data ... but controller inheritance itself, is not possible: https://github.com/angular-ui/ui-router/wiki/nested-states-&-nested-views#what-do-child-states-inherit-from-parent-states

Resources