I have two states:
.state('a.b', {
url: "",
views: {
"b": {
template: require('./ab.html'),
controller: ABController
}
})
.state('a.b.c', {
url: "/:someid",
views: {
"c": {
template: require('./abc.html'),
controller: ABCController
}
})
with ab.html:
<div ui-view="c"></div>
When I navigate from state a.b to state a.b.c, I see the ABController being reconstructed while when I navigate from state a.b.c to a.b, ABContoller is not reconstructed.
What I really want to achieve is to not reconstruct ABContoller when navigating from a.b to a.b.c or from a.b.c to a.b. Is there a way to do this ?
So, the answer is that this is indeed possible and it is what happens by default because, as I was expecting it, ui-router does track carefully what states need to be destroyed/created based on the from and to states.
In this case, what triggered the extra construction is the fact that two of my states used the same parameter variable which forced ui-router to consider the common elements of the state path to be different.
Basically, the fix involved removing an unused/buggy "params" attribute from a state definition.
Related
Edit: Here is the complete code at Plunker. Though I can not c anything in execution but same code working at local. However gives a console error though
It all works perfect. But due to :id in /news/:id/, i am getting jquery/angular errors in console which can not be tracked anywhere in my code
I can not c What i am doing wrong.
Edit: Solved plunker https://plnkr.co/edit/FWcuBgGpVdMj3CroFrYJ
First of all you are trying to use ui-router but you're including ngRoute script in your plunker. Change it to
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.3.1/angular-ui-router.min.js"></script>
Then everything should work fine!
I suggest you a few changes...
1. Use ui-sref instead of href because it's much easier to define
ui-sref="post({id:1})" which turns into href="#/news/1"
If you would like to change url some day, then you will have to just change your route file, not each href.
$stateProvider
.state('post', {
url: "news/:id"
or
$stateProvider
.state('post', {
url: "archive/:id"
or
$stateProvider
.state('post', {
url: "whatever/:id"
2. Use abstract state
In your example it's a way better to define abstract state which holds header, content and footer - it's a typical use case.
ui-router
Abstract States
An abstract state can have child states but can not get activated
itself. An 'abstract' state is simply a state that can't be
transitioned to. It is activated implicitly when one of its
descendants are activated.
Some examples of how you might use an abstract state are:
To prepend a url to all child state urls. To insert a template with
its own ui-view(s) that its child states will populate. Optionally
assign a controller to the template. The controller must pair to a
template. Additionally, inherit $scope objects down to children, just
understand that this happens via the view hierarchy, not the state
hierarchy. To provide resolved dependencies via resolve for use by
child states. To provide inherited custom data via data for use by
child states or an event listener. To run an onEnter or onExit
function that may modify the application in someway. Any combination
of the above. Remember: Abstract states still need their own
for their children to plug into. So if you are using an
abstract state just to prepend a url, set resolves/data, or run an
onEnter/Exit function, then you'll additionally need to set template:
"".
Here's a plunker which shows how I would do it.
https://plnkr.co/edit/5FvJaelyxdl5MuALt5VY?p=preview
Hope it helps.
Look at the documentation for ui router named views,
You can use following syntax for using multiple views
$stateProvider
.state('state',{
url: '',
views: {
'header': {
templateUrl: 'views/header.html',
controller: 'headerCtrl'
},
'content': {
template: '<div ui-view=" "></div>', //<-- child templates loaded to here
},
'footer': {
templateUrl: 'views/footer.html',
controller: 'footerCtrl'
}
}
})
.state('state.post', {
url: 'news/:id/:KeyWords'
templateUrl: 'views/post.html' //<-- This goes into content's ui-view
});
I'm guessing you want to keep the header and footer and change content views.
You can achieve this by making this state as parent to all other states
suppose
.state('main',{
abstract: true,
views: {
'header': ... ,
'content': {
template: '<ui-view></ui-view>',
}
'footer': ...
}
})
then all the child views will load their views in the ,
ex: in main.child etc, your template will load in the content's <ui-view></ui-view> tag
If you need to use a custom template depending on keywords you can do the following:
.config(['$routeProvider',
function($routeProvider, $routeParams) {
$routeProvider
.when('/news/:id/:keyWords', {
template: '<div ng-include="url"></div>',
controller: "exampleController"
})
then in the exampleController
function($routeParams, $scope) {
$scope.url = $routeParams.keyWords;
}
I'm currently working on a small application that shows a 'drill down' type of menu. Unlike a TreeView my component only displays the current level.
There are only 3 levels: main, submenu, item. Drilling deeper (on the item) will result in a new state that corresponds to a detail view of the selected item.
I have only 2 states, the first is valid for any of the three levels. The second is the detail state:
$stateProvider.state('menu', {
url: '/menu/:submenu/:item',
templateUrl: 'src/menu/views/menu.html',
controller: 'MenuController',
controllerAs: 'MenuController',
resolve: {
data: function($stateParams, MenuService){
return MenuService.loadUri($stateParams);
}
}
}).state('menu.details', {
url: '/:selectedItem',
templateUrl: 'src/menu/views/menu-details.html',
controller: 'MenuDetailsController',
controllerAs: 'MenuDetailsController',
resolve: {
data: function($stateParams, MenuService){
return MenuService.loadUri($stateParams);
}
}
});
This solutions works correctly, however I'm a bit annoyed by the fact I need to define the resolvemethod twice. I am required to do this as $stateParams contains only the params of that exact state (excluding child states) and I need all parameters to form a valid URL to fetch the corresponding resource via the MenuService.
I know that state.params can be used to get all state params, however it doesn't seem to work in the resolve method: it contains the parameters of the previous state.
Does anybody know how I could fix this? Having a child state that should simply trigger a new view, whilst using the resolve method of the parent state with all state parameters?
If you think I'm completely misunderstanding how states work, then please go ahead and tell me :)
I have 3 nested states:
$stateProvider
.state('A', {
url: '/a',
templateProvider: function ($templateCache) {
return $templateCache.get('pages/a.html');
},
controller: 'aController'
})
.state('A.B', {
url: '/b',
templateProvider: function ($templateCache) {
return $templateCache.get('pages/b.html');
}
controller: 'bController'
})
.state('A.B.C', {
url: '/c',
templateProvider: function ($templateCache) {
return $templateCache.get('pages/c.html');
},
controller: 'cController'
})
Let's say that A is the initial state of the app. Now, when a link to state B is clicked in state A, $state.go('.B', ...) is called, state B has to get parameters from A so it can call some service with that parameters and render the data.
Likewise, when a link to state C is clicked on state B, C has to get parameters from B so it can do it's work and display data properly.
First question:
What is the best way to pass those parameters down the line? Is it wrong to nest controllers so parent scope is visible in child scope? Or should they be passed as params?
Second question (kind of dependent on the answer to the first one):
How should the html templates be structured (especially with regards to the 'ui-view' directive), so when the browser back button is clicked on state C after coming from B, the controller for template B doesnt get triggered, so that state B displays the same data as before going to state C without reloading. The same goes for clicking back on state B after coming to it from A.
Third question:
If the user has made the following transitions: A->B->C, and then navigates to some unrelated state D (by clicking the link on the main menu for example), and then in state D presses the back button, how do I prevent controller C from falling apart since it has no input params?
Fourth question (related to third):
Premise: The only 'right' way to get to state B is by clicking on state A, likewise to get to C the user has to pass A->B first.
How, then, do I handle when the user manually enters for example URL B, from some unrelated state D? Again, everything falls apart because B has no input params.
Thank you.
The 1st, 3rd, and 4th questions can be solved by using parameters on the url of your states. For instance, the URL of state A.B can be /b?x&y&z instead of just /b. Then to transition from A to A.B, you would use:
$state.go('A.B', {x: "val1", y: "val2", z: "val3"});
This solves the first problem because you're passing parameters from A to B. This solves the third problem because state A.B.C would not fall apart after returning from another state, since the URL history contains the parameters. And it's very apparent how this approach will solve the fourth problem of user entering the URL directly into the address bar.
Question 2 presents a problem slightly trickier to solve. But what you can do is introduce an intermediate state between A and B whose sole purpose is to prevent the re-loading of state A's controller when the back button is pressed.
.state('A', {
abstract: true,
url: '/a',
templateProvider: function ($templateCache) {
return $templateCache.get('pages/a.html');
},
controller: 'aController'
})
.state('A.int', {
url: '/a/view',
template: <div></div>,
controller: 'aIntermediateController'
})
.state('A.int.B', {
url: '/b',
templateProvider: function ($templateCache) {
return $templateCache.get('pages/b.html');
}
controller: 'bController'
})
Notice, I made state A abstract so that you can't navigate to it directly. Instead, you navigate to A.int directly. When you initially load state A.int, aController will execute once, before aIntermediateController executes. Subsequently, any state changes to A.int will only execute aIntermediateController. And that's how you can prevent aController from running multiple times.
Similarly, you can add an intermediate view between state B and C the same way.
I am using Angular UI-Router and need to have a single parent state that several other states can inherit the results of a resolve from. I have tried every solution found here on SO for this but NOTHING is working. My states look like this:
.state('transaction', {
abstract:true,
url: '/register/pos/:screen/:document_sid',
template:'<div ui-view></div>',
resolve: {
Document: //the thing I need to have all the children access
}
})
.state('transaction.view', {
url:'',
views:{
'':{
templateUrl: '/views/docs/pos-transaction-outer-partial.htm',
controller:'posdocumentController'
}
}
})
.state('transaction.edit', {
url: '/:mode',
views:{
'':{
templateUrl: '/views/docs/pos-transaction-outer-partial.htm',
controller:'posdocumentController'
}
}
})
.state('transaction.returns', {
url: '/returns',
views:{
'':{
templateUrl: '/views/pos-itemreturn-partial.htm',
controller: 'posItemReturnsController'
}
}
})
When I use $state.go('transaction.edit') it works perfectly. Inside the 'posdocumentController' I have a function that calls $state.go('transaction.returns') and I can see the route change and the partial is requested, but the view never changes. According to the docs for multiple named views the specified views should load in the parents ui-view that is contained within the template. Obviously, for me this is not happening and for the life of me I have not been able to figure out why. I've tried this both with and without the views option and get the same results. Can someone explain why and how I fix this?
In details is hidden... in very small detail, in this case.
I. Why not working?
Here is a plunker with broken navigation (exactly as described in the question above)
These would be the calls to the above states:
ui-sref
transaction.view ({ screen: 1 ,document_sid:2 ,mode:5 })
transaction.edit ({ screen: 1 ,document_sid:2 ,mode:5 })
transaction.returns ({ screen: 1 ,document_sid:2 ,mode:5 })
href
#/register/pos/1/2
#/register/pos/1/2/5
#/register/pos/1/2/returns
so, while these could seem as proper state calls, the last two, edit and returns, could be observed as:
// edit link result
params
{
"screen": "1",
"document_sid": "2",
"mode": 5
}
state
{
"url": "/:mode",
...
// returns link result
params
{
"screen": "1",
"document_sid": "2",
"mode": "returns"
}
state
{
"url": "/:mode",
Now it should be clear. The third link (to state returns) is in fact navigating to edit again. Why? because of this states definition:
.state('transaction.edit', {
url: '/:mode', // here we say :mode is param
...
.state('transaction.returns', {
url: '/returns', // and 'returns' here is already mapped... above as :mode
Check the not working solution here
II. How to make it working
There is a link to working plunker. The change made is:
firstly define returns and then edit state
.state('transaction.returns', {
url: '/returns', // 'returns' would be evaulated first, and used
...
.state('transaction.edit', {
url: '/:mode', // only other then returns values will be used here
Working solution is here
Other solution, maybe better, would be to use some regex... to narrow :mode values: Regex Parameters
'/user/{id:[^/]*}' - Same as '/user/{id}' from the previous example.
'/user/{id:[0-9a-fA-F]{1,8}}' - Similar to the previous example, but only matches if the id parameter consists of 1 to 8 hex digits.
'/files/{path:.*}' - Matches any URL starting with '/files/' and captures the rest of the path into the parameter 'path'.
'/files/*path' - Ditto. Special syntax for catch all.
Considering the following states taken from the ui-router documentation:
.state('state1', {
url: '/state1',
templateUrl: 'partials/state1.html'
controller: 'State1Ctrl'
})
.state('state1.list', {
url: '/list',
templateUrl: 'partials/state1.list.html',
})
And the controller for "partials/state1.html" for state "state1":
.controller('State1Ctrl', function () {
});
Is there any built-in feature to determine from within the controller or within the template, what state the controller/template is associated with?
For example:
.controller('State1Ctrl', function ($state) {
console.log($state.this); // state1
});
And if there is no built-in solution, how would you "decorate" $state or $stateParams, to contain this information?
The only solution I've come up with is to use $state.get() and then find the state with the controller or template value. This seems incredibly messy, though.
You can access the current state configuratin object like this:
$state.current
For further information take a look at the $state documentation.
You can do it as follow,
$state.current.name //Return the name of current state
Couldn't find this documented anywhere, so I looked in the source code.
There is a data field named $uiView attached to the ui-view element, it contains the view name and the associated state. You can get this state like this:
elem.closest('[ui-view]').data('$uiView').state
or even
elem.inheritedData('$uiView').state
We can see what is defined for current state, using the $state.current, check this example showing:
state1
{
"url": "/state1",
"template": "<div>state1 <pre>{{current | json }}</pre><div ui-view=\"\"></div> </div>",
"controller": "State1Ctrl",
"name": "state1"
}
list
{
"url": "/list",
"template": "<div>list <pre>{{current | json }}</pre></div>",
"controller": "State1Ctrl",
"name": "state1.list"
}
the controller:
.controller('State1Ctrl', function($scope, $state) {
$scope.current = $state.current
});
check that here
EXTEND: The above example will always return current state - i.e. if there is hierarchy of three states and we access the last state ('state1.list.detail') directly:
<a ui-sref="state1.list.detail({detailId:1})">....
Each controller will be provided with the same state: $state("state1.list.detail").
Why? beause this state has enough information what are all the views (hierarchically nested) and their controllers needed. We can observe that all in the
$state.$current // not .current
Quickly discussed here cite:
In addition, users can attach custom decorators, which will generate new properties within the state's internal definition. There is currently no clear use-case for this beyond accessing internal states (i.e. $state.$current), however, expect this to become increasingly relevant as we introduce additional meta-programming features.
BUT: there is NO way, how to get information from current controller instance, to which $state it belongs! Even any iterations, searching through some $state.get('stateName') will be unreliable, because simply there is no kept relation that way. One controller Class could be used for many views as different Instances. And also, from my experience, I do not remember any case, when I needed to know such information... wish now it is a bit more clear
This is useful if you are looking for Current state name, $state.current.name
Not sure it is the same version, but on 0.3.1 you can use this to get the current state:
$state.$current.name
And to perform a check:
$state.is('contact.details.item');
Documentation:
https://ui-router.github.io/ng1/docs/0.3.1/index.html#/api/ui.router.state.$state
A working "out of the box" Controller from your code, which shows state:
.controller('State1Ctrl', function ($state) {
console.log("Current State: ", $state.current.name);
});
If you want to check the current state
console.log("state", $state.current)
If you want to check the name of the current state name
console.log("statename", $state.current.name)