Angular routing and state handling problems - angularjs

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.

Related

Create a non abstract parent with a URL

I have a simple form that can be used to initiate a forecast request. I created this as a parent state requests (Initiate Forecast).
Desired behavior
When a request is submitted, that immediate request is shown in a child state (View Status) as most recent request. This View Status state will also hold a grid of all past requests, meaning I will be refreshing the grid with data every time this state is invoked.
Both parent and child states are navigable from a sidebar menu.
So, if a user clicks on parent (Initiate Forecast), he should be able to see only the form to submit a request. If a user directly clicks on the 'View Status'(child), then he should be able to see both the form and the grid of requests.
app.js
function statesCallback($stateProvider) {
$stateProvider
.state('requests', {
url: '',
views: {
'header': {
templateUrl: 'initiateforecasting.html',
controller: 'requestsInitiateController'
},
'content': {
template: '<div ui-view></div>'
}
},
params: {
fcId: null,
fcIndex: null
}
})
.state('requests.viewStatus', {
url: '/ViewStatus',
templateUrl: 'viewstatus.html',
controller: 'requestsStatusController'
});
}
var requestsApp = angular.module('requestsApp', ['ui.router']);
requestsApp.config(['$stateProvider', statesCallback]);
requestsApp.run(['$state', function($state) {
$state.go('requests');
}]);
Plunker of my attempts so far.
This is all working for me as shown in the plunker, but I am doing it by not setting a URL to the parent. Having no URL for the parent state is allowing me to see both states. If I add a URL to parent state, then clicking on View Status link is not going anywhere (it stays on Initiate).
How do I change this so that I can have a URL for parent state and still retain the behaviour I need from the parent and child states as described above?
Note: I am fine without the URL for parent state in standalone sample code, but when I integrate this piece with backend code, having no URL fragment on the parent state is making an unnecessary request to the server. This is visible when I navigate to the child state and then go to the parent state. It effectively gives the impression of reloading the page which I think is unnecessary and can be avoided if a URL can be set to the parent state.
You shall not directly write url when using ui.router, try like this:
<a ui-sref="requests.viewStatus">View Status</a>
You are writing state name in ui-sref directive and it automatically resolves url. It's very comfortable because you can change urls any time and it will not break navigation.

How to reuse a ui-router state in different states?

I have a ui-router state that I use to display some info about a movie in an overlay. The data it displays is always the same, and the data it recieves is also always the same. But I'm using that state multiple times in my ui-router controller, but with different state parameters.
I have 4 states from where I call this trailer state,
home.container-big.trailer
home.suggestions.container-big.trailer
friendprofile.container-big.trailer
home.activities.movie-info.trailer
These are the 4 states I call the trailer state.
So for each click I have a different state although everything except the state name is different.
So I was thinking this,
I've create a stateProvider
$stateProvider
.state('trailer',{
params: {
trailer_link: null
},
url: '',
views: {
"youtube_trailer":{
templateUrl: '../assets/angular-app/templates/_container-trailer.html',
controller: function($scope, $stateParams, $state) {
$scope.trailer_link = $stateParams.trailer_link;
}
}
}
})
And I want to call it inside the home.container-big state like so,
%a{"ui-sref" => ".trailer({trailer_link: '{{ trailer.link }}'})"} Trailer {{$index+1}}
The click produces this error
Could not resolve '.trailer' from state 'home.container-big'
Which makes sence. So I'm thinking I need to pass the current state in the click to the name of the ui-state. Is this possible? Or is there another way I can reuse the same state in different states?

Using $state.params in resolve does not work

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 :)

Am I using states correctly?

I have an angular app with a homepage that shows a list of things. Each thing has a type. In the nav, there are selectors corresponding to each thing type. Clicking one of these selectors causes the home controller to filter the things shown to those of the selected type. As such, I see the selectors as corresponding to states of the home page.
Now, I'd like to map each of these states to a url route: myapp.com/home loads the home page in default (unfilitered) state, myapp.com/home/foo opens the home page with the foo-type selector activated, and switching from there to myapp.com/home/bar switches to the bar-filtered state without reloading the page.
It's that last bit - triggering "state" changes without reloading the page, that's been particularly tricky to figure out. There are numerous SO/forum questions on this topic but none have quite hit the spot, so I'm wondering if I'm thinking about this in the wrong way: Should I be thinking of these "states" as states at all? Is there a simpler approach?
Also, I'm open to using either ngRoute or ui.router - is there anything about one or the other that might make it simpler to implement this?
Using ui-router, you can approach it like this:
$stateProvider
.state('home', {
url: "/home",
controller: "HomeController",
templateUrl: "home.html"
// .. other options if required
})
.state('home.filtered', {
url: "/{filter}",
controller: "HomeController",
templateUrl: "home.html"
// .. other options if required
})
This creates a filtered state as a child of the home state and means that you can think of the URL to the filtered state as /home/{filter}. Where filter is a state parameter that can then be accessed using $stateParams.
Since you don't want to switch views, you inject $stateParams into your controller, watch $stateParams.filter, and react to it how you wish.
$scope.$watch(function () { return $stateParams.filter }, function (newVal, oldVal) {
// handle it
});

ui-router navigation is occurring twice under certain conditions

I have a UI-Router site with the following states:
$stateProvider
.state('order', {
url: '/order/:serviceId',
abstract:true,
controller: 'OrderController'
})
.state('order.index', {
url:'',
controller: 'order-IndexController'
})
.state('order.settings', {
url:'',
controller: 'order-SettingsController'
})
Where my two states do NOT have a url set, meaning they should only be reachable through interaction with the application. However the order.index state is automatically loaded by default because of the order in which I have defined the states.
I am noticing that trying to do a ui-sref="^.order.settings" or $state.go("^.order.settings") then ui-router first navigates to the order.settings state and then it immediately navigates to the order.index state (default state). I think this is happening because the url changing is causing a second navigation to occur and since the state.url == '' for both, it automatically defaults to the order.index state...
I tested this out by setting the {location: false} object in the $state.go('^order.settings', null, {location:false}). This caused the correct state to load (order.settings), but the url did not change. Therefore I think the url changing is triggering a second navigation.
I'd understand, can imagine, that you do not like my answer, but:
DO NOT use two non-abstract states with same url defintion
This is not expected, therefore hardly supported:
.state('order.index', {
url:'', // the same url ...
...
})
.state('order.settings', {
url:'', // .. used twice
...
})
And while the UI-Router is really not requiring url definition at all (see How not to change url when show 404 error page with ui-router), that does not imply, that 2 url could be defined same. In such case, the first will always be evaluated, unless some special actions are used...
I would strongly sugest, provide one of (non default) state with some special url
.state('order.settings', {
url:'/settings', // ALWAYS clearly defined
...
})

Resources