Angular ui-router: keep same ui-view for child - javascript

I need child state be able to use parent state's resolve functions. But I also need to keep same ui-view for both states. Here's a fiddle. And there's a code
$stateProvider
.state('parent', {
url: "/",
template: '<p>Hello {{parent.one}}</p><br>'
+ '<button ng-click="goToChild()">child</button><br>',
// this one below work but I don't need it
// template: '<p>Hello {{parent.one}}</p><br>'
// + '<button ng-click="goToChild()">child</button><br>'
// + '<div ui-view></div>',
resolve: {
test: function() {
return 1;
}
},
controller: function($scope, $state, test) {
$scope.parent = { one: test };
$scope.goToChild = function() {
$state.go('parent.child');
}
}
})
.state('parent.child', {
url: "/child",
template: '<p>Hello {{child.one}}</p>',
controller: function($scope, test) {
$scope.child = { one: test };
}
})
$urlRouterProvider.otherwise('/');

There is a working plunker.
The answer should be hidden/revealed in this two states definition:
parent with multi views
.state('parent', {
url: "/",
views: {
'#': {
template: '<div ui-view=""></div>',
controller: function($scope, $state, test) {
$scope.parent = { one: test };
$scope.goToChild = function() {
$state.go('parent.child');
}
}
},
'#parent': {
template: '<p>Parent says: hello <pre>{{parent | json}}</pre></p>'
+ '<br><button ng-click="goToChild()">child</button><br>',
}
},
resolve: {
test: function() { return 1; },
},
})
Child consuming parent resolve, and having its own
.state('parent.child', {
url: "^/child/:id",
template: '<p>Child says: hello <pre>{{child | json }}</pre></p>',
resolve: {
testChild: function() { return 2; },
},
controller: function($scope, test, testChild) {
$scope.child = {
one: test,
two : testChild,
parent: $scope.parent,
};
},
})
Check it here
And how it works? Well, it all is based on the:
Scope Inheritance by View Hierarchy Only
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states.
and also:
View Names - Relative vs. Absolute Names
Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname#statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.
For example, the previous example could also be written as:
.state('report',{
views: {
'filters#': { },
'tabledata#': { },
'graph#': { }
}
})
So, the above documentation cites are the core of the plunker. The parent uses multi views, one of them is unnamed - and will be used for inheritance. Parent also injects into that view its own "parent" view. The Resolve of a parent is in place...
Child now injects into anchor of its parent, which does have all the stuff needed. That means, that child does inherit scope and also resolve stuff. It shows its own resolve as well...

Related

angular router does not select child state (nested states)

I'm having a problem with a new state I added to our web site.
Short description
I have a state (/page)'q.explorer' which contains a button; when clicked it should go to a child state (named 'q.explorer.detail') but it does not. However: in the logging I see that it does try to go to that (/state) and the new url is formatted as defined in the child state.
But still the template and controller that are actually used is the 'parent' which contains the button ...
This may be a little confusing to explain; so I have also added some code in the hope that this will clarify my problem.
The setup looks like this:
$stateProvider
.state('q', {
url: '/:locale/app',
data : { ui: "V2" },
views: {
'application' : {template: '<div ui-view=""><page-home-v2></page-home-v2></div>' }
}, controller: function($scope, $stateParams, siteNavigation) {
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer', {
url: '/explorer?:year&:month&:guide',
template: '<page-explorer-v2></page-explorer-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer");
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer.detail', {
url: '/detail',
template: '<page-explorer-detail-v2></page-explorer-detail-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer.detail");
siteNavigation.applyUrlParameters($stateParams);
}
})
angular
.module('q.components')
.service('siteNavigation', function($state, $location) {
var service = this;
service.applyUrlParameters = function($stateParams) {
if (!$stateParams) $stateParams = $state.params;
console.log('Apply state parameters for state: ' + $state.current.name);
console.log('URL >> ' + $location.url());
};
};
Somewhere deep in the template of "q.explorer" there is a button to open the detail view ("q.explorer.detail"). It uses this code:
function goToDetail() {
var ui = $state.current.data.ui;
if (ui === "V1") { /*...*/ }
else if (ui === "V2") {
var params = {
year: Store.getYear(),
month: Store.getMonth(),
locale: Store.getLocale()
}
var guide = Store.getActiveSidebarItem();
if (guide) params.guide = guide.slug;
console.log("go to explorer: " + params.guide);
$state.go('q.explorer.detail', params);
}
else console.log("Unable to go to state because the ui version is not known.");
}
And this is what I see in the console after clicking the link:
go to explorer: ebit
controller: q.explorer
Apply state parameters for state: q.explorer.detail
URL >> /nl/app/explorer/detail?year=2015&month=11&guide=ebit
As you can see, it uses the controller of the 'parent' iso the child page I want to open. Even though $state.current.name is correct ... Or maybe I should say it does not change from state ...
Any help is welcome.
(PS: We are using Angular 1.4.9)
Looks like you are using nested states such that q.explorer.detail is a child of q.explorer. To render the child state's template, you also need a specific ui-view where it can be placed into. And this will be searched in the template of the parent state. Getting the console.log() output just means the controllers are instantiated, but that even happens if the template isn't rendered at all.
So check if you have an ui-view in the template of the q.explorer state. For more details, please see: https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
You could also fix this by not making q.explorer.detail a child of q.explorer. A child state is created as soon as you need the dot notation.
Yoy have to add somewhere in 'q.explorer' state's template entry point for nested view 'q.explorer.detail', otherwise child controller will not be called.
For example:
template: '<page-explorer-v2></page-explorer-v2><ui-view></ui-view>',
instead of
template: '<page-explorer-v2></page-explorer-v2>'
See jsfiddle: http://jsfiddle.net/jcpmsuxj/42/
Upd. As #ajaegle mentioned you should to visit official docs page:
https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views

How to access parameter from nested state in parent state?

I have an example here. How i can access 'itemId' parameter in the parent state controller? Parent view must not be reloaded.
angular.module('app',['ui.router'])
.config(function($stateProvider){
$stateProvider.state('step', {
url: '/orders/:step',
templateUrl: 'parent.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
})
.state('step.item', {
url: '/:itemId',
templateUrl: 'child.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
});
}).controller('SimpleController', function($scope, $state){
$scope.items = [1,2,3,4,5,6];
$scope.steps = ["first", "second"];
})
I see only two ways:
add an service and inject it to both controllers
access a parent scope from a child controller and pass a parameter
But both cause me to add watchers at a parent scope.
Maybe i can accomplish it easier?
First, you need to recognize that a parent state's controller runs only once when you enter the subtree of its child states, so switching between its children would not re-run the controller.
This is to say that if you want the itemId parameter to always be up to date, you'd need a $watch (which you tried to avoid).
For the first time, you could collect the itemId like so in the parent's state:
$scope.itemId = $state.params.itemId;
If you need it to be kept up-to-date, then you'd need to watch for changes, for example:
$scope.$watch(function(){
return $state.params;
}, function(p){
$scope.itemId = p.itemId;
});
plunker
Similarly, if you only need to place it in the view, then set $state on the scope:
$scope.$state = $state;
and in the view:
<div>parent /orders/{{step}}/{{$state.params.itemId}}</div>
EDIT:
I guess another way would be to call a scope-exposed function on the parent to update the value. I'm not a fan of such an approach, since it relies on scope inheritance (which is not always the same as state inheritance) and scope inheritance in a large application is difficult to track. But it removes the need for a $watch:
In the parent controller:
$scope.updateItemId = function(itemId){
$scope.itemId = itemId;
};
and in the child controller:
if ($scope.updateItemId) $scope.updateItemId($stateParams.itemId)
I can see, that you've already found your answer, but I would like to show you different approach. And I would even name it as "the UI-Router built in approach".
It has been shown in the UI-Router example application, where if we go to child state with some ID = 42 like here we can also change the other, than the main view (the hint view in this case).
There is a working example with your scenario, showing that all in action.
What we do use, is the Multiple Named Views
The parent state, now defines root view, which is injected into unnamed ui-view="" inside of the root/index.html.
A new parent.html
<div ui-view="title"></div>
<div ui-view=""></div>
As we can see, it also contains another view target (than unnamed) - e.g. ui-view="title" which we fill with another template immediately... in parent state:
$stateProvider.state('step', {
url: '/orders/:step',
views : {
'': {
templateUrl: 'parent.html',
},
'title#step': {
templateUrl:'parent_title_view.html',
}
}
})
And child? It can continue to handle main area... but also can change the title area. Both views are independent, and belong to child.
.state('step.item', {
url: '/:itemId',
views : {
'': {
templateUrl: 'child.html',
},
'title': {
templateUrl:'child_title_view.html',
}
}
So, there are no watchers, nothing... which is not shipped with the UI-Router by default. We just adjust many places (views) with the child implementation. We can still provide their content with some defaults in parent..
Check that example here. Hope it will help to see it a bit differently...
I'm not sure if this will really solve your problem but you can do
$stateProvider.state('step', {
url: '/orders/:step/:itemId',
templateUrl: 'parent.html',
http://plnkr.co/edit/sYCino1V0NOw3qlEALNj?p=preview
This means the parent state will collect the itemID on the way through.
include $state in resolve function for parent state
.state('parent', {
url: '/parent',
controller: 'parentController',
resolve: {
params: ['$state', function ($state) {
return $state.params;
}]
}
})
then in parent controller inject the resloved vars
app.controller('parentController', ['$scope', 'params',
function ($scope, params) {
$scope.params = params;
}]);
in this way params available in both child and parent.

Angular ui.router setting ancestor state named view

As per the documentation for multiple named views (https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views) child states can set named views in ancestor states by using the # notation.
This doesn't seem to be working in my example (http://jsfiddle.net/sarvesht/rx8jLep2/)
This is how I have defined my parent and child states, I am setting the viewB view in the child state but its not reflected on the page.
.state('index', {
url: "",
views: {
"viewA": {
template: "index.viewA"
}
}
})
.state('index.sub',{
views: {
"viewB#index": {
template: "index.viewB"
}
}
})
Is it possible to achieve what I am trying to do? Or am I doing something wrong?
Regards
Sarvesh
If I do understand your aim properly, you are almost there. The only adjustment should be:
.state('index.sub',{
views: {
// instead of this
// "viewB#index": {
// we need thsi
"viewB#": {
template: "index.viewB"
}
}
})
The reason is, that I expect that your ViewA and ViewB are both defined on the root level (index.html). And that's why the first state 'index' is correctly working, because it definition is:
.state('index', {
url: "",
views: {
// this is working
"viewA": {
// and this would be as well
"viewA#": {
template: "index.viewA"
}
}
})
Other words, I expect both views are inside of root view, which name is empty string. That's why we can target both of them like:
"viewA#": {
"viewB#": {
See: View Names - Relative vs. Absolute Names small cite:
Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname#statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.
For example, the previous example could also be written as:
.state('report',{
views: {
'filters#': { },
'tabledata#': { },
'graph#': { }
}
})
Notice that the view names are now specified as absolute names, as opposed to the relative name. It is targeting the 'filters', 'tabledata', and 'graph' views located in the root unnamed template. Since it's unnamed, there is nothing following the '#'. The root unnamed template is your index.html.
There is updated plunker, with this state def:
$urlRouterProvider.otherwise("/index/sub")
$stateProvider
.state('index', {
url: "/index",
views: {
"viewA": {
template: "index.viewA"
}
}
})
.state('index.sub', {
url : "/sub",
views: {
/*
"viewB#index": {
template: "index.viewB"
},
*/
"viewB#": {
template: "index.viewB"
}
}
})
The key is to define url or at least properly navigate to sub state index.sub

Named view in angular ui-router not updating, despite being watched

I have a scoped variable $scope.foo that I am keeping a watch on. It could be updated through a text field in a form.
I have two named views A and B on a page that I am rendering using angular ui-router.
The named view A has the text form field that is being watched for changes in a controller through ng-model="foo". When the value of foo is changed by a user it changes the value of another scoped variable $scope.bar, which is an array, in the controller that is being used in the ng-repeat directive on the named view B. The changes in the $scope.bar is made using $scope.$watch method in the controller.
The issue that I am facing is that the when the foo is changed I could see the changes in bar on the named view A but not on the named view B.
Could somebody help me resolve this issue?
Edit:
Here is the plunker for this issue.
There is a plunker, which should show that your scenario is working.
The most important part of that solution is driven by:
Scope Inheritance by View Hierarchy Only (cite:)
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states.
Let me express it again: A scope inheritance goes only via the view nesting.
With that we can create this states definitions:
$stateProvider
.state('root', {
url: '/root',
templateUrl: 'tpl.root.html',
controller: 'RootCtrl', // this root scope will be parent
})
.state('root.entity', {
url: '/entity',
views:{
'A': {
templateUrl: 'tpl.a.html',
controller: 'ACtrl', // scope is inherited from Root
},
'B': {
templateUrl: 'tpl.b.html',
controller: 'ACtrl', // scope is inherited from Root
}
}
})
So the state defintion support nested views - let's profit from that and place the $scope.bar collection into the parent. All views involved will then have access to the same collection:
.controller('RootCtrl', function ($scope, $state) {
$scope.bar = ['first', 'second', 'last'];
})
.controller('ACtrl', function ($scope, $state) {
// *) note below
$scope.foo = $scope.bar[0];
$scope.$watch("foo", function(val){$scope.bar[0] = val; });
})
.controller('BCtrl', function ($scope, $state) {
})
*) note: here we do 1) set from bar 2) $watch and 3) set back to bar to follow the question description... but if the array would contain objects, we can work with them directly... without that overhead, but that's another story...
Check here how that works, and that any changes in view A are also visible in B ... because of inherited reference to the array bar declared in parent $scope.
I created the second answer, to follow also the issue in this plunker, which #skip (OP) passed me as the example fo the issue.
Firstly There is an updated working version
of that plunker, which does what we need. There are the main changes:
The original state def:
.state('home', {
url: '/',
views: {
'': { templateUrl: 'home.html' },
'A#home': {
templateUrl: 'a.html',
controller: 'MainCtrl'
},
'B#home': {
templateUrl: 'b.html',
controller: 'MainCtrl'
}
}
Was replaced with the RootCtrl defintion:
.state('home', {
url: '/',
views: {
'': {
templateUrl: 'home.html',
controller: 'RootCtrl' // here we do use parent scoping
},
'A#home': {
templateUrl: 'a.html',
controller: 'MainCtrl'
},
'B#home': {
templateUrl: 'b.html',
controller: 'MainCtrl'
}
}
And this was one controller:
app.controller('MainCtrl', function($scope) {
var fruits = [{"name": "Apple"}, {"name": "Banana"}, {"name": "Carrot"}];
$scope.bar = $scope.bar || [];
$scope.foo = 2;
$scope.$watch('foo',function(value, oldValue){
$scope.bar = [];
getBar(fruits, value);
});
function getBar(fruits, howManyFruits) {
for(var i=0; i < $scope.foo; i++) {
$scope.bar.push(fruits[i]);
}
}
});
But now we do have two (Parent and child):
app.controller('RootCtrl', function($scope) {
$scope.bar = [];
})
app.controller('MainCtrl', function($scope) {
var fruits = [{"name": "Apple"}, {"name": "Banana"}, {"name": "Carrot"}];
//$scope.bar = $scope.bar || [];
$scope.foo = 2;
$scope.$watch('foo',function(value, oldValue){
$scope.bar.length = 0;
getBar(fruits, value);
});
function getBar(fruits, howManyFruits) {
for(var i=0; i < $scope.foo; i++) {
$scope.bar.push(fruits[i]);
}
}
});
Some important parts to mention
I. The least common denominator
We have to move the shared collection (array bar) into the parent. Why?
we have to move the shared reference to the least common denominator - to the parent scope
see
How do I prevent reload on named view, when state changes? AngularJS UI-Router
II. The Reference to array must be unchanged
we have to keep the reference to the Parent $scope.bar unchanged!. This is essential. How to achieve that? see:
Short way to replace content of an array
where instead of creating new reference, we clear the array, keeping the reference to it
// wrong
$scope.bar = [];
// good
$scope.bar.length = 0;
III. Controller can have multiple instances
Also, the fact that both views A and B had the same controller (same controller name in fact), definitely did not mean, that they were the same instance.
No, they were two different instances... not sharing anything. That is I guess, the most critical confusion. see
angularjs guide - Dependency Injection
Controllers are special in that, unlike services, there can be many instances of them in the application. For example, there would be one instance for every ng-controller directive in the template.
Please, observe that all in the updated example

Named view transition using ui-router

Is it possible for a named view in ui-router to make a transition to some other named view?
For example, in the snippet below, is it possible to move from blue#bar state to orange#foo state?
.state('foo', {
url: '/',
views: {
'': { templateUrl: 'partials/a.html' },
'black#foo': {
templateUrl: 'partials/b.html',
controller: 'SuperController'
},
'green#foo': {
templateUrl: 'partials/c.html',
controller: 'SuperController'
},
'orange#foo': {
templateUrl: 'partials/d.html',
controller: 'SuperController'
}
}
})
.state('bar', {
url: '/ping/:x/:y',
views: {
'': {
templateUrl: 'partials/e.html',
controller: 'DuperController'
},
'blue#bar': {
templateUrl: 'partials/f.html',
controller: 'DuperController'
}
}
})
The ui-router transitions are there for state transitions. Other words, there are no transitions among views.
What we can do, is to move some named view anchors (ui-view="myName") into root or some parent state. Then each child (grand child) state can target any named view in its parent(s) hierarchy.
The best way how to understand that is to read this chapter from doc:
View Names - Relative vs. Absolute Names
And check this snippet (cite from the resource above):
$stateProvider
.state('contacts', {
// This will get automatically plugged into the unnamed ui-view
// of the parent state template. Since this is a top level state,
// its parent state template is index.html.
templateUrl: 'contacts.html'
})
.state('contacts.detail', {
views: {
////////////////////////////////////
// Relative Targeting //
// Targets parent state ui-view's //
////////////////////////////////////
// Relatively targets the 'detail' view in this state's parent state, 'contacts'.
// <div ui-view='detail'/> within contacts.html
"detail" : { },
// Relatively targets the unnamed view in this state's parent state, 'contacts'.
// <div ui-view/> within contacts.html
"" : { },
///////////////////////////////////////////////////////
// Absolute Targeting using '#' //
// Targets any view within this state or an ancestor //
///////////////////////////////////////////////////////
// Absolutely targets the 'info' view in this state, 'contacts.detail'.
// <div ui-view='info'/> within contacts.detail.html
"info#contacts.detail" : { }
// Absolutely targets the 'detail' view in the 'contacts' state.
// <div ui-view='detail'/> within contacts.html
"detail#contacts" : { }
// Absolutely targets the unnamed view in parent 'contacts' state.
// <div ui-view/> within contacts.html
"#contacts" : { }
// absolutely targets the 'status' view in root unnamed state.
// <div ui-view='status'/> within index.html
"status#" : { }
// absolutely targets the unnamed view in root unnamed state.
// <div ui-view/> within index.html
"#" : { }
});
Also, check this sample app (by ui-router team)
http://angular-ui.github.io/ui-router/sample/#/contacts
and its state defintion
state.js
small cite from the child state definition, targeting many different views:
///////////////////////
// Contacts > Detail //
///////////////////////
// You can have unlimited children within a state. Here is a second child
// state within the 'contacts' parent state.
.state('contacts.detail', {
// Urls can have parameters. They can be specified like :param or {param}.
// If {} is used, then you can also specify a regex pattern that the param
// must match. The regex is written after a colon (:). Note: Don't use capture
// groups in your regex patterns, because the whole regex is wrapped again
// behind the scenes. Our pattern below will only match numbers with a length
// between 1 and 4.
// Since this state is also a child of 'contacts' its url is appended as well.
// So its url will end up being '/contacts/{contactId:[0-9]{1,8}}'. When the
// url becomes something like '/contacts/42' then this state becomes active
// and the $stateParams object becomes { contactId: 42 }.
url: '/{contactId:[0-9]{1,4}}',
// If there is more than a single ui-view in the parent template, or you would
// like to target a ui-view from even higher up the state tree, you can use the
// views object to configure multiple views. Each view can get its own template,
// controller, and resolve data.
// View names can be relative or absolute. Relative view names do not use an '#'
// symbol. They always refer to views within this state's parent template.
// Absolute view names use a '#' symbol to distinguish the view and the state.
// So 'foo#bar' means the ui-view named 'foo' within the 'bar' state's template.
views: {
// So this one is targeting the unnamed view within the parent state's template.
'': {
templateUrl: 'app/contacts/contacts.detail.html',
controller: ['$scope', '$stateParams', 'utils',
function ( $scope, $stateParams, utils) {
$scope.contact = utils.findById($scope.contacts, $stateParams.contactId);
}]
},
// This one is targeting the ui-view="hint" within the unnamed root, aka index.html.
// This shows off how you could populate *any* view within *any* ancestor state.
'hint#': {
template: 'This is contacts.detail populating the "hint" ui-view'
},
// This one is targeting the ui-view="menu" within the parent state's template.
'menuTip': {
// templateProvider is the final method for supplying a template.
// There is: template, templateUrl, and templateProvider.
templateProvider: ['$stateParams',
function ( $stateParams) {
// This is just to demonstrate that $stateParams injection works for templateProvider.
// $stateParams are the parameters for the new state we're transitioning to, even
// though the global '$stateParams' has not been updated yet.
return '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';
}]
}
}
})
some other reading with more links

Categories

Resources