How to manage state with the ui-router with multiple modules - angularjs

I have an app page with 3 columns. The middle column is the main activity and is always displayed. The two side columns are widget lists, that have their own controller and states, and can be either hidden or unhidden, and have multiple views within them as well. Ideally, I'd imagine url routes like the following:
/app - main activity is shown, both panels hidden
/app/1234 - main activity is shown, but shows info for 1234 entity
/app/1234/leftpanel - main activity is shown with 1234 entity, and leftpanel is open
/app/1234/leftpanel/list - main activity is shown with 1234 entity, and leftpanel is showing the list view
/app/leftpanel/list - main activity is showing default state, leftpanel is still showing the list
Is this possible to setup with ui-router? I've seen this example:
https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions
which shows how to use the $stateProvider between multiple modules, but I'm still not seeing how to make this scenario work-

I did resolve the issue. I also posted about this on the angular-ui github, and their response was basically, "well, that case isn't really what the router has been designed for, so if you want "fancier" state management, put the data in parameters and look at them yourself and implement whatever logic you need". I kinda felt that this was what the ui-router was designed for, so I extended it a bit (no source code changes) to accomplish this. The solution is a combination of abstract states, a fake "off" state, parameters in the router urls, and extending the $urlRouterProvider service.
First extend the $urlRouterProvider:
urlRouterProvider.when(/^((?!leftpanel).)*$/, ['$state', '$location', function ($state, $location) {
//we've got just /app or /app/3434, nothing that contains /leftpanel, so turn off
$state.transitionTo("off");
}]);
Then add that "off" state:
$stateProvider.state('off',{
//url: //there is no url
views:{
container:{
template: 'blank',
controller:['$scope', '$stateParams', function($scope, $stateParams){
console.log("off yay"); //just for sanity, not necessary
}]
}
}});
Then setup the rest of the app routing:
appModule.constant('LEFT_PANEL_STATES', function() {
var leftPanelRoot = {
name: 'root.leftpanel', //mandatory
template: '',
url: "/app/:primaryId/leftpanel",
views:{
'container#': {
templateUrl: "partials/leftpanel_container_partial.html",
controller:"LeftPanelRootCtrl",
resolve: {
//etc
}
}
},
"abstract":true //makes this view only viewable from one of its child states
};
var leftPanelItemsList = {
name: 'root.leftpanel.itemslist', //mandatory
parent: leftPanelRoot, //mandatory
url: "/items-list",
views:{
'childview#root.leftpanel': {
templateUrl: "partials/leftpanel_items_list.html",
controller:"LeftPanelItemsListCtrl",
resolve: {
//etc
}
}
}};
var leftPanelListDetail = {
name:"root.leftpanel.itemslist.detail",
parent:leftPanelItemsList,
url:"/:id/detail",
views:{
"detail":{
templateUrl:"partials/leftpanel_item_detail.html",
controller:"LeftPanelItemListDetailCtrl"
}
}};
var leftPanelExtendedDetailList = {
name:"root.leftpanel.itemslist.extendedlist",
parent:leftPanelItemsList,
url:"/:id/extendedDetail/list",
views:{
"extendeddetaillist":{
templateUrl:"partials/leftpanel_extended_detail_list.html",
controller:"LeftPanelExtendedDetailListCtrl"
}
}};

Related

ui-router parent state optional parameter

I am trying to manage CRUD for lists and list elements.
I want to have one state, one template and one controller per element for each case(create/edit).
I create the state:
state('list', {
parent: 'root',
url: '/list?list_id',
views: {
'content#root': {
templateUrl: 'list.tmpl.html',
controller: 'ListController',
controllerAs: 'vm',
}
}
})
Which is perfect, in the controller i can check for a list_id and toggle betweend create/edit.
The problem occurs when the state above becomes a parent.
When the child is introduced:
state('list-element', {
parent: 'list',
url: '/element?list_id&element_id',
views: {
'content#root': {
templateUrl: 'element.tmpl.html',
controller: 'ElementController',
controllerAs: 'vm',
}
}
})
I can no longer have the uncertainty i need.
To put it more simple, i want the url structure to look like this:
/list?list_id - if list_id toggle edit
/list/element?list_id&element_id - if element_id toggle edit
Note that when the list element is created, the parent state does not have a parameter.
Now, i can work around this by creating two states for the list:
/list (parent) create
?list_id (child for 1) edit
/element?list_id&element_id (child for 1) create or edit
but this would break the "one state, one template and one controller" that i want.
Anyway that i can achieve to do it the way i want?
I know you're looking for a specific implementation, but I think part of the reason you're hitting trouble is because that approach may not be the best for what you're trying to do, since it seems like you're confusing the role of a route versus a DOM element.
Components (and directives) are designed to help you bundle together controllers and templates explicitly for elements on the page. Routing is just what gets you there, and tells you which ones to load. Here's how you might handle this using a more conventional approach:
stateHelperProvider.state({
name: 'list',
url: '/list'
// This is possible with https://github.com/marklagendijk/ui-router.stateHelper
children: {
name: 'element',
// Example: /list/123
url: '/{list_id:int}?element_id',
resolve: {
item: ['$stateParams', 'FooService', ($stateParams, FooService) => {
// Get whatever info you need, based on the ID.
return FooService.getItem($stateParams.list_id);
}
},
views: {
'content#root': {
// This will use the resolved `item` and pass it into the component.
template: '<list-element item="$resolve.item"></list-element>'
}
}
}
});
// The component
(function () {
'use strict';
angular.module('yourModule')
.component('listElement', {
templateUrl: 'element.tmpl.html',
bindings: {
item: '<'
}
});
})();
// Sample component template
<h2 ng-bind="$ctrl.item.name"></h2>
<div ng-bind="$ctrl.item.description"></div>
For a different route, you may want to check out nested named views in UI Router. This will allow you to specify a different controller, template, and state each. Gist:
<!-- Parent template -->
<div ui-view="editContents"></div>
<div ui-view="previewContents"></div>

Angular Ui router multiple named views and nested states

I have a question about the Angular UI router when I am using multiple named views in nested states. Basically I have an abstract state with a template that points to two named views. Those two named views are defined in both the sub states. I want to keep the URL fixed to /test.
When transitioning to either of the sub states, I see the view corresponding to the first sub state only. Why is that? I really hope someone can clarify the concept for me so I can learn
JSFiddle is here: https://jsfiddle.net/adeopura/e2c5n14o/16/
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.subs1', {
url: '',
views: {
'view1': {
template: "Im 1View1"
},
'view2': {
template: "Im 1View2"
}
}
})
.state('test.subs2', {
url: '',
views: {
'view1': {
template: "Im 2View1"
},
'view2': {
template: "Im 2View2"
}
}
});
}])
.run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$state.transitionTo('test.subs1');//I see the data corresponding to the test.subs1
// Assume here that there are lots of different state transitions in between unrelated to the test1 state
$state.transitionTo('test.subs2');//I still see the data corresponding to the test.subs1, why is that?
}]);
It's actually the order by which you've defined them. Swap the order so that you define test.sub2 before test.sub1 and you'll see it'll start # test.sub2.
I think it's because you don't have a url assigned to either of the sub-states. I would try giving them their own url paths, and seeing if you can then reference the states that way.
One of my colleagues explained what is happening in this case. Explained as below:
state.transitionTo('test.subs2') causes the application to go to state 'test.subs2'
Once in state 'test.subs2', the URL is changed to /test
This causes the Angular UI Router to fire a change to the appropriate state.
The appropriate state is the first one that matches the URL /test, which would be the state 'test.subs1'
Hope this helps someone

Initialize data only once with ui-router and md-tabs

I am using ui-router with Material Design's tabs in order to load tab content dynamically in an ngGrid.
Every view has its separate controller with a service which requests the data. Any ideas on how I can configure ui-router to initialize the data only once for each tab?
Currently, every time I click on a tab a new request is being fired and ngGrid is reinitialized.
ng-router config file, just in case:
$stateProvider
.state('tab1', {
url: '/tab1',
views: {
'tab1': {
templateUrl: 'tab1.html',
controller: 'tab1Controller'
}
}
})
.state('tab2', {
url: '/tab2',
views: {
'tab2': {
templateUrl: 'tab2.html',
controller: 'tab2Controller'
}
}
})
.state('tab3', {
url: '/tab3',
views: {
'tab3': {
templateUrl: 'tab3.html',
controller: 'tab3Controller'
}
}
});
UPDATED
In case, that data are different for each tab, we have to options.
use inheritance (view inheritance),
use Services
The first case will mean, that all states do have same parent. This parent declares Model = {}. Every sub-state can later access this Model and add or use the data
var data = Model.DataForTab1;
if(!data){
data = loadData();
Model.DataForTab1 = data;
}
next time, we come to this tab, we can reuse existing, in parent $scope referenced data Model
The second approach is to use caching inside of a special services for each data load. Such service would have almost the same logic (return loaded cached data or load them)
Difference? Services will have longer life time (through whole application). The parent of tabs will keep the data JUST until we go to another parent state.
ORIGINAL part
If data are the same across all the states, the most suitable way (I'd say) with UI-Router, is to
introduce one common parent state,
use resolve in that parent (will be executed just once)
inherit from it, all children can get such data:
Check this Q & A
// virtual parent 'root'
$stateProvider
.state('root', {
abstract: true,
template: '<div ui-view></div>',
resolve: {objectX : function() { return {x : 'x', y : 'y'};}},
controller: 'rootController',
})
// each state will declare parent state
// but its name and url is not effected
.state('tab1', {
parent: "root",
url: '/tab1',
Check more here and the working plunker

Controlling order of operations with services and controllers

I have two services - one to store user details and the other to make a call to retrieve those details:
userService stores user details to be used across the entire app (i.e. injected in controllers, services, etc.)
function userService($log) {
var id = '';
var username = '';
var isAuthenticated = false;
var service = {
id: id,
username: username,
isAuthenticated: isAuthenticated
};
return service;
}
authService is used (hopefully just once) to retrieve the user details from a Web API controller:
function authService($log, $http, userService) {
$log.info(serviceId + ': Inside authService method');
var service = {
getUserDetails: getUserDetails
};
return service;
function getUserDetails() {
$log.info(serviceId + ': Inside getUserDetails method');
return $http.get('api/authentication', { cache: true });
}
}
Initially, I had the call to the authService fire in a .run block like so:
.run(['$log', 'authService', 'userService', function ($log, authService, userService) {
authService.getUserDetails()
.then(querySucceeded);
function querySucceeded(result) {
userService.id = result.data.Id;
userService.username = result.data.username;
}
}]);
But the problem was that the getUserDetails-returned promise did not resolve until after I my controllers fired and, thus, too late for me. The user data was not ready.
I then looked at the resolve option in the $stateProvider (for UI-Router):
.state('dashboard', {
url: '/dashboard',
views: {
header: {
templateUrl: 'app/partials/dashboard/header.template.html',
controller: 'DashboardHeaderController',
controllerAs: 'dashboardHeaderVM',
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
}
}
})
The assumption is that the view won't be rendered until the promise in the resolve section is, well, resolved. That seems to work fine.
Here's the (relevant part of the) controller where I use the returned user property:
function DashboardHeaderController($log, user) {
var vm = this;
// Bindable members
vm.firstName = user.data.firstName;
}
However, I have two routes (more to come) and a user can navigate to either one. Do I need to have a resolve property in each state section for the authService? I want to fire the call to authService.getUserDetails just once no matter which route is served and have it available after that for any route, controller, etc.
Is there a better (best practice) way to do this?
Not sure about better or best practice, but here is a plunker with my way.
The point is to move resolve into some parent root state. The one who is ancestor of all states in the application:
$stateProvider
.state('root', {
abstract : true,
// see controller def below
controller : 'RootCtrl',
// this is template, discussed below - very important
template: '<div ui-view></div>',
// resolve used only once, but for available for all child states
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
})
This is a root state with resolve. The only state with resolve. Here is an example of its first child (any other would be defined similar way:
$stateProvider
.state('index', {
url: '/',
parent : 'root',
...
This approach will work out of the box. I just would like to mention that if the 'RootCtrl' is defined like this:
.controller('RootCtrl', function($scope,user){
$scope.user = user;
})
we should understand the UI-Router inheritance. See:
Scope Inheritance by View Hierarchy Only
small 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...
More explanation could be found in this Q & A
So, what does it mean?
Our root view can pass the resolved stuff into child state only - if their views are nested.
For example, the $scope.user will be inherited in child states/views/$scopes only if they are nested like this
.state('index', {
url: '/',
parent : 'root',
views: {
'' : { // the root view and its scope is now the ancestor
// so $scope.user is available in every child view
templateUrl: 'layout.html',
controller: 'IndexCtrl'
},
'top#index' : { templateUrl: 'tpl.top.html',},
'left#index' : { templateUrl: 'tpl.left.html',},
'main#index' : { templateUrl: 'tpl.main.html',},
},
Check it here
If I correctly understand you want that on page load you would have user info before any controller or service request it.
I had similar task in my current project.
To solve the problem I manually requested current user info before app bootstapping & store it in localStorage.
Then after app bootstrapping all controllers/services have accesss to current user info.
TIP: to get user info before app bootstrap you can still use $http service by manually injecting it:
angular.injector(['ng']).get('$http');

Update single view where there are many in AngularJs and UI-Router

I am a n00b to Angular, and trying to get my head around how ui-routing works. I see the how the concept of multiple routes works, and how nested routes work. How would i do both? Or is this not good practice?
In the example bellow I have app/wh working fine, however when some goes to app/wh/toc/123 I want only the middle view to swap.
.state('app.wh', {
url: "/wh",
views: {
'left' : {
templateUrl: "static/partials/leftPane.html"
},
'middle': {
templateUrl: "static/partials/start.html"
},
'right' : {
templateUrl: "static/partials/rightPane.html",
controller: 'AsideCtrl'
}
}
})
.state('app.wh.toc', {
url: "/toc/:id",
views: {
'middle' : {
templateUrl: "static/partials/toc.html",
controller: function($scope, $stateParams, $state){
$scope.title = $stateParams.id;
}
}
}
})
I am guessing that you want your left and right panels to stay static while the middle content changes through the users actions. Personally I am not a big fan of using multiple routes, and instead simply use ng-include if I want to separate portions of a single view into their own files.
If you do this, then you only need to define your parent, and child state:
.state('app.wh', url: '/wh', abstract: true, controller: ...
Note that if you want separate controllers for left and right, you can use ng-controller in your views. Also If this state is common to multiple middle contents, then you might want to make it an abstract state, so that the user cannot actually load it (it only acts as a parent placeholder).
.state('app.wh.toc', url: /toc/:id ...
Then in your view:
<div ng-include="someUrlRefLeft"></div>
<div ui-view=""></div>
<div ng-include="someUrlRefRight"></div>

Resources