Using Angular-ui-router, is there a possibility to define a child-state that has multiple parent states:
$stateProvider
.state("parent1", {
url: '/',
templateUrl: 'parent1.html'
})
.state('parent2', {
url: '/parent2',
templateUrl: 'parent2.html'
})
//Make accessible from every state parent-state
.state("child", {
url: 'child',
//TODO parents: ['parent1', 'parent2']
onEnter: function() {
//do something;
}
})
Example:
Your Angular app has a directive that is used multiple times in different states in your app. The directive itself includes a link which redirects to a different state. Depending on where the directive is used, it should append its child-state to the current active parent-state.
It just doesn't seem right to define states for each permutation like state1.child, state2.child etc.. There needs to be a better way.
This kind of hierarchy would go against the DOM tree structure, which by definition doesn't allow multiple parents of same element.
Moreover, it is error (and headache) prone and could easily result in the multiple inheritance diamond problem, as child state do inherit from parent state in some cases.
It sounds like a directive, and not a state, would be the better solution for what you're looking for.
EDIT:
Just saw that there's a closed issue on this, which is closed because he reached the same conclusion (that a directive is the better way)
Related
I was trying to implement a secure area with an abstract state that is then implemented by concrete states.
According to this blog post, I thought i could have the abstract state resolve the authentication and have the concrete states rely on it, because as stated in ui-router docs
Child states will inherit resolved dependencies from parent state(s)
But later on, the same ui-router doc mentions that
The resolve keys MUST be injected into the child states if you want to wait for the promises to be resolved before instantiating the children.
and indeed, if i comment out the resolveline on the concrete state, it doesnt work anymore.
So i am really missing something. If ChildState inherit resolved dependencies, why should they be re-injected?
my code below :
router.config(function($stateProvider, $urlRouterProvider) {
var authentication = ["Auth", function(Auth) {
return Auth.$requireAuth();
}];
.state('secure', {
abstract: true,
template: '<navbar></navbar><ui-view/>',
resolve: authentication
})
.state('secure.concrete', {
url: '/concrete',
templateUrl: 'views/concrete.html',
controller: 'concreteCtrl',
resolve: authentication // <--- if i comment this out, it wont work
})
UPDATE :
After hours of tears, it looks like i'm going the wrong path. The problem doesnt come from inherited resolved dependencies, but from the fact that once the promisse is resolved, it's not "resetted".
Is there anyway to force the promise to be re-setted / re-evaluated on each state change ?
The problems may result from incorrect resolve syntax, it should be an object:
resolve: {
authentication: authentication
}
'MUST be injected into the child states' means that authentication should be explicitly stated as child state controller dependency. Duplicate resolve on child state isn't an injection; it will just make authentication called twice.
this did the trick for me but i'm not exactly sure why :
instead of calling state.go() or ui-sref normally, i call them with state.go(state, {reload:true}) or
that way the resolve seems to be re-evaluated.
How it is
I'm working on an application which has a little but more complex form than usual. I've splitted it up by having different views and controllers, but still keeping them inside a form element.
It has different sections but the payload of the request should contains all the information when I do the POST request. So basically that's why it has one main form (well it's not about having it inside a form or not, I could even have it without a form and then collecting models and submitting it by including all model's data into one main).
Question
My question is:
Does ui-router has a kind of required field to set? Just in order to have the required controller's functions and scope.
Does anyone knows a better solution?
Code
The code is just an example, this is not literally the code I have.
View (sectionMain.html)
<form>
<div ui-view="section1"></div>
<div ui-view="section2"></div>
<div ui-view="section3"></div>
<button type="submit">Submit</button>
</form>
State definition
$stateProvider.state('myState', {
url: '/mstate',
parent: 'stateForAuthenticatedUsers',
views: {
//this will be loaded what I have above. Just a part of the page.
'content': {
templateUrl: 'modules/myModule/views/sectionMain.html',
controller: 'Section1Ctrl as vm'
},
'section1#myState': {
templateUrl: 'modules/myModule/views/section1.html',
controller: 'Section1Ctrl as vmState1'
},
'section2#myState': {
templateUrl: 'modules/myModule/views/section2.html',
controller: 'Section2Ctrl as vmState2'
},
'section3#myState': {
templateUrl: 'modules/myModule/views/section3.html',
controller: 'Section3Ctrl as vmState3'
},
}
});
Controller (for example State2Ctrl)
function State2Ctrl($scope) {
//stuffs here. Need to refer parent's scope somehow
$scope.vm.someVariableOrObject
}
My concern
My only concern is that trusting a value vm which has been defined in the state's configuration doesn't give me the feeling that it's a well composed structure. I'd really need something I can require to, to have it explicitly instead of blindly trusting in a value vm.
Also my views contains the vm.someVariable in order to refer the parent's scope. But I don't like it.
Why not simply use your $rootScope? An easy way to achieve what you are looking for, without passing in a $scope every time.
Here is my route config
$stateProvider.state('layout', {
abstract: true,
controller: "MenuCtrl",
templateUrl: "views/layout/MainLayout.html"
}).
state('layout.home', {
url: '/',
templateUrl: 'views/Main.html',
controller: 'MainCtrl'
}).state('layout.tag.add', {
url: '/addTag',
templateUrl: 'views/AddTag.html',
controller: 'AddTagCtrl'
})
Later I have in my code function:
var goToAddTagPage = function(){
$state.go('layout.tag.add');
};
When I call this function I get Could not resolve 'layout.tag.add' from state 'layout'. If I rename this to layout.addTag it works correctly. Nested tag causes issue. How I can correctly nest states like that?
EDIT: I have added empty state
state('layout.tag',{
}).
Now exception is gone. However the view is now not rendered. I get empty screen. I try to add abstract : true for state but it didn't helped.This state need some configuration?
In your state hierarchy, there really must be all three states.
state('layout', {...}
state('layout.tag', {...}
state('layout.tag.add', {...}
Because a '.' (dot) in the state name simply represents hierarchy (parents, grand parents)
But once we add new parent between grand-parent and child, we need to be sure, that
parent contains a view "target" for a child.
child explicitly uses absolute view naming to target grand-parent
So, this would work (and I would prefer that, because we gain inheritance parent-child)
state('layout.tag', {
template: '<div ui-view ></div>'
...
}
So now, there is the element <div ui-view ></div> injected into grand-parent, and also serves as an anchor/target for child.
Check the doc:
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.
The second approach is to use absolute view naming and skip grand parent
.state('layout.tag.add', {
url: '/addTag',
views: {
'#layout': { // target unnamed view anchor in grand-parent
templateUrl: 'views/AddTag.html',
controller: 'AddTagCtrl'
}
}
})
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.
You may(dont quote me on this, but try) need to introduce an intermediate layout.tag state if you want to use this hierarchy.
UI router could be failing on 'dot-notation' based nesting because youre skipping a state essentially.
Update: Based on the regex in this snippet from the source
var compositeName = /^(.+)\.[^.]+$/.exec(state.name);
It does look for a parent state named 'layout.tag'.
So you will either need to accept the 'layout.addTag' hierarchy or introduce an intermediate 'layout.tag' state
As you can notice in the following code, I would like to have a view for the parent "colors" state (which will show a table with list of colors), and then each color should have it's own view, not inherited from "colors".
So the hierarchy should only apply to URL's, not to views.
Any idea ?
.state('colors', {
url: "/colors",
templateUrl: 'views/colors/colors.html'
})
.state('colors.yellow', {
url: "/yellow",
templateUrl: 'views/colors/yellow.html'
})
I understand that you've found your answer. But let me append other approach and extend your solution with some more dynamic stuff. It'll a bit overcome your question, but could help you to see the magic around UI-Router
I created an example here
Firstly, we can have this kind of parent template (colors.html)
<div ui-view="">
// the content of the parent template, e.g. list of colors
</div>
So, because the ui-view="" is defined on the parent root element, child will in fact replace it. And what's more, we would gain $scope inheritance:
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 now even more. Let's imagine that we would have more colors, not only yellow. That could lead to change in the approach, and color could become parameter:
.state('colors', {
url: "/colors",
...
})
.state('colors.color', {
url: "/:color",
...
That's a big change, because now we can have url like /colors/yellow or /colors/red and all will be managed by stat colors.color
Let's continue, using the solution from here: Trying to Dynamically set a templateUrl in controller based on constant - and we can even have many templates, different by each color name.
We can then define them as constants inside of the angular module:
.value('myTemplates', {
"yellow" : "views/colors/yellow.html",
"red" : "views/colors/red.html",
"default" : "views/colors/default.html",
}
)
And our child state could use them in the run-time, based on the parameter. This would be the call:
<a ui-sref="colors.color({color: 'yellow'})">
<a ui-sref="colors.color({color: 'red'})">
<a ui-sref="colors.color({color: 'white'})">
And this will be the adjusted child state:
.state('colors.color', {
url: "/:color",
templateProvider: ['$templateRequest', '$stateParams', 'myTemplates',
function($templateRequest, $stateParams, myTemplates) {
var templateName = myTemplates[$stateParams.color]
|| myTemplates["default"];
return $templateRequest(templateName);
}],
})
Check that all in action here
I'm trying to integrate Rails with Angular to turn part of my app into a one-page-app using Angular. I have a main module with the following (coffeescript) code for routing:
MainApp.config ($routeProvider) ->
$routeProvider
.when '/',
{
templateUrl: 'post_archive.html'
}
.when '/new',
{
templateUrl: 'new_post.html'
}
.when '/:postSlug',
{
templateUrl: 'show_post.html'
}
.when '/:postSlug/edit',
{
templateUrl: 'edit_post.html'
}
.otherwise
redirectTo: '/'
The main view for this section of the site starts with this haml
%div{ ng_controller: 'PostCtrl', ng_init: 'init()', ng_cloak: true }
And the PostCtrl has this init function:
$s.init = ->
$s.getPost().then ->
$s.getPostList() unless $s.postList
$s.getPreviousPost()
$s.getNextPost()
The idea is, the current post, as well as the next and previous, need to be recalculated, but the postList should remain the same, so it doesn't need to be re-fetched on every page load.
However, it is. It seems that the scope is getting dumped on every page load, which means that it's not really behaving at all like a one-page app, and there's a flicker while the postList reloads, whenever a link is followed within the app.
The links' rendered HTML looks like this, for example:
<a id="link_name" ng_href="#/post-name" class="ng-binding" href="#/post-name">
Post Name
</a>
Any idea what I'm doing wrong here? (Does this have to do with all the pound signs that angular seems to be inserting before the final URL slash?)
This is the expected behavior. If scope.init() is being called on init, it should be called on page load, as the scope will be bound (and initialized) each time its controller route will be accessed.
To avoid that behavior, simply call init() on demand, or — better yet — escalate postList to a higher level in the scope hierarchy (above ng-view, where the route change takes place and re-binds scopes to views), e.g. in the $rootScope. That way its initialization / evaluation won't be tied to the $s scope's init.
To illustrate this [1]:
You can define postList in the topmost scope, — it will be prototypically (is that even a word?) inherited:
$rootScope.postList = [];
It's also sufficient to save it in a parent controller — so long as it's higher in the hierarchy than the router's scope (where ng-view resides), as same rules apply for inheritance with controller's scopes. Something along these lines [2]:
// in the view
%div { ng_contoller: 'ParentCtrl' }
%div { ng_view }
%div{ ng_controller: 'PostCtrl', ng_init: 'init()', ng_cloak: true }
// in ParentCtrl
$s.postList = [];
[1] Caution! not tested!
[2] Risk of bogus code — I don't really know HAML!