ui-router parent state optional parameter - angularjs

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>

Related

Angular UI-Router Multiple Named Views

Desired Behavior
I'm using AngularJS and the Angular UI-Router.
I want to allow two child states to share a parent state.
I want the child states to fill a ui-view in the parent state's view with their own view.
I want one of the two child states to have three ui-view's in their view, each filled with views.
Attempt at a diagram:
Parent: <ui-view>
filled by
Child: <ui-view> <ui-view> <ui-view>
filled filled filled
Specific Details for My Situation
I have a state called category-details. Inside the view for this abstract state I have an unnamed ui-view. In one of the two child states (category-details.selected) I want to use multiple named views.
Current WORKING Strategy
Here is the abstract state. Very basic, but included for your reference.
.state('category-details', {
abstract: true,
data: {
pageTitle: 'Category Details'
},
templateUrl: "views/category-details.html",
})
In the category-details.selected state (the state which will have multiple named views), I set the unnamed ui-view of category-details.html to category-details-selected.html:
.state('category-details.selected', {
views: {
'': {
templateUrl: 'views/category-details-selected.html',
controller: 'CategoryDetailsSelectedCtrl'
}
}
})
Inside of the category-details-selected.html view I have three named ui-views:
<div ui-view="firstNamedView"></div>
<div ui-view="secondNamedView"></div>
<div ui-view="thirdNamedView"></div>
Finally, I define a state for setting these three ui-view's to meet the third part of my desired behavior:
.state('category-details.selected.aspect', {
url:"/category-details/:selectedCategory",
views: {
'firstNamedView': {
templateUrl: 'views/first-named-view.html',
controller: 'FirstNamedViewCtrl'
},
'secondNamedView': {
templateUrl: 'views/second-named-view.html',
controller: 'SecondNamedViewCtrl'
},
'thirdNamedView': {
templateUrl: 'views/third-named-view.html',
controller: 'ThirdNamedViewCtrl'
}
}
});
Why My Solution is Awkward and Suboptimal
Adding the category-details.selected.aspect state to set constant elements (the three ui-view's) of the category-details-selected view is unnecessary. It forces creating an extra state every time I want multiple named views.
What I've Tried
I believe I should be able to move the url and views of the category-details.selected.aspect state into the views component of its parent state (category-details.selected). This would look like:
.state('category-details.selected', {
url:"/category-details/:selectedCategory",
views: {
'': {
templateUrl: 'views/category-details-selected.html',
controller: 'CategoryDetailsSelectedCtrl'
},
'firstNamedView': {
templateUrl: 'views/first-named-view.html',
controller: 'FirstNamedViewCtrl'
},
'secondNamedView': {
templateUrl: 'views/second-named-view.html',
controller: 'SecondNamedViewCtrl'
},
'thirdNamedView': {
templateUrl: 'views/third-named-view.html',
controller: 'ThirdNamedViewCtrl'
}
}
})
This resulted in the unnamed ui-view being set correctly, but the three named ui-view's were not filled.
Since selecting the three named ui-view's was the problem, I then attempted to select them with absolute selectors described here instead. This did not fix the problem. I tried:
firstNamedView
firstNamedView#
firstNamedView#category-details.selected
(others of course)
Closing Remarks
Is what I'm imagining possible, is another way better, or is my current method the best? It boils down to assigning the child ui-views of a parent ui-view being set at the same time. I thought the last sentence was too confusing alone, so I included the entire example.
Please let me know if I can provide any more clarification such as versions. Thank you.
Abstract states need their own <ui-view/> for their children to plug into.
Parent state category-details is abstract state. Child state will need some reference ui-view to plug that state into. In your I believe the view /category-details.html does not any ui-view (as you have mentioned that category-details-selected.html contains the ui-view).
Try this:
.state('category-details', {
abstract: true,
data: {
pageTitle: 'Category Details'
},
templateUrl: "views/category-details-selected.html",
})
.state('category-details.selected', {
url:"/category-details/:selectedCategory",
views: {
'firstNamedView': {
templateUrl: 'views/first-named-view.html',
controller: 'FirstNamedViewCtrl'
},
'secondNamedView': {
templateUrl: 'views/second-named-view.html',
controller: 'SecondNamedViewCtrl'
},
'thirdNamedView': {
templateUrl: 'views/third-named-view.html',
controller: 'ThirdNamedViewCtrl'
}
}
})
Here, we are giving abstract view a template, which has ui-view in it, for child to populate.
Have a look at documentation of ui-router: Abstract State for more information.
EDIT: I had assumed that views/category-details.html does not contain any ui-view. However, it was then pointed out that, views/views/category-details.html does have ui-view
This is what works for me:
category-details.html:
<div ui-view=""></div>
category-details-selected.html:
<div ui-view="firstNamedView"></div>
<div ui-view="secondNamedView"></div>
<div ui-view="thirdNamedView"></div>
router:
.state('category-details', {
abstract: true,
data: {
pageTitle: 'Category Details'
},
templateUrl: "../app/atest/category-details.html",
})
.state('category-details.selected', {
url: "/atest",
views: {
'': {
templateUrl: "../app/atest/category-details-selected.html",
// controller: 'approvalsCtrl as vm',
},
'firstNamedView#category-details.selected': {
templateUrl: '../app/atest/first.html',
// controller: 'approvalsCtrl as vm',
},
'secondNamedView#category-details.selected': {
templateUrl: '../app/atest/second.html',
// controller: 'approvalsCtrl as vm',
},
'thirdNamedView#category-details.selected': {
templateUrl: '../app/atest/third.html',
// controller: 'approvalsCtrl as vm',
}
}
})
I could see that you have mentioned you tried out using firstNamedView#category-details.selected, but it didn't worked for you. The above example is working for me. Check if you category-details.html and category-details-selected.html view contain proper ui-view.

Using ui-router and controllerAs can I simplify the adding of services?

I have this code:
var home = {
name: 'home',
template: '<div data-ui-view></div>',
url: '/',
templateUrl: 'app/access/partials/home.html',
controller: ['accessService', function (accessService: IAccessService) {
this.ac = accessService;
}],
controllerAs: 'home'
};
var homeAccess = {
name: 'home.access',
url: 'Access',
templateUrl: 'app/access/partials/webapi.html',
controller: ['accessService', function (accessService: IAccessService) {
this.ac = accessService;
}],
controllerAs: 'homeAccess',
resolve: {
abc: ['accessService', function (accessService) {
return accessService.getAbc();
}],
def: ['accessService', function (accessService) {
return accessService.getDef();
}]
}
};
Now that I am using controllerAs is there a way that I can simplify this code so as to eliminate adding the accessService into both of the controllers and into the two parts of the resolve? Also if I did this then how could I get to the access service inside the home.html and also the webapi.html?
There are probably a few different ways, just thinking out loud here.
Your home state is the parent of home.access and it uses a ui-view to show the child state. As such the template for the child state can reference the controller in the parent state. This is just the regular inheritance of $scope in Angular views, although it's much cleaner b/c you are using the controllerAs syntax.
For example, your views might end up looking like this:
<home-template>
<p>{{home.someValue}}</p>
<!-- included by the ui-view -->
<home-access-template>
<p>{{homeAccess.anotherValue}}
<!-- this works b/c home is on the parent scope -->
<p>{{home.someOtherValue}}</p>
</home-access-template>
<home-template>
So if it makes sense in your scenario, you only need to inject your accessService into the parent controller. The child views will use the service through methods of the parent controller.
A similar thing can also be done with the resolves: by declaring them on the parent state, they are available to the child states. This is more useful when there are many child states for a given parent.

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

Nested parameterized routes with ui-router in AngularJS

I believe I'm on the right track with these terms, "nested parameterized routes", but haven't found what I'm looking for yet.
My objective is to create intuitive routes for my API, something like the following example:
/api/v1/project/list
/api/v1/project/1/item/list
/api/v1/project/1/item/1/edit
/api/v1/project/2/item/3/delete
It's relatively easy and clear how to setup project states, but not the item states within each project.
{
state: 'project'
config: {
url:'/project'
}
},
{
state: 'project.list'
config: {
url: '/list'
}
},
{
state: 'project.detail'
config: {
url: '/:project_id'
}
}
It's not clear to me where to go from there so that items are relative or nested within projects.
I'll assume you have a REST api (based on your example containing /api/v1) which you want to expose/parallel as a UI. I'll assume you want to allow the user to drill down some hierarchical data model.
Choices!
There are many ways you could organize your states, for this drill-down list/details pattern. None is the "correct" way, but some are probably better than others. I will highlight two approaches that I've used:
Sibling states for list and details
One approach is to keep the "item list" states and "item details" states as siblings. This is what you did with project.list and project.details. This approach can be seen in the UI-Router Extras Demos source code.
When taking this approach
you must take care to move the user from the list state to the detail state when drilling down.
This approach has the benefit of easy-to-understand nesting of UI-Views. The ui-view for the detail view replaces the ui-view for the list view, when drilling down, because you are navigating to a sibling state.
Your choice whether or not the detail for an entity also retrieves the list of sub-entities (does the detail for a project also show the items list for that product?)
States:
projectlist // template plugs into parent ui-view
projectdetail // template plugs into parent ui-view, replacing projectlist
projectdetail.itemslist // template plugs into parent ui-view (#projectdetail)
projectdetail.itemdetail // template plugs into parent ui-view (#projectdetail), replacing itemslist
Details state as a substate of List state
Another approach is to make the detail state a child of the list state. This is organized similar to your REST routes.
When taking this approach
States hierarchy closely resembles the REST routes being exposed
Drilling down is simple and intuitive
You must manage the visual display of list/detail.
When drilling down from list state to the details substate, you probably want to hide the list.
We use named views, and absolute naming in order to replace the parent list state's template with the template for the the detail state. This is called "view targetting".
States:
top // theoretical parent state
top.projects // lists projects. Plugs into parent ui-view (#top)
top.projects.project // details for project. Its named view targets the grandparent ui-view (#top), replacing the template from top.projects list state.
top.projects.project.items // lists items. Plugs into parent ui-view (#top.projects.project)
top.projects.project.items.item // details for item. Its named view targets the grandparent ui-view (#top.projects.project), replacing the template from top.projects.project.items list state.
Here's an example of using named view targeting to accomplish the second approach:
$stateProvider.state('top', {
url: '/',
template: '<ui-view/>',
});
$stateProvider.state('top.projects', {
url: '/projects',
resolve: {
projects: function(ProjectsRoute) {
return ProjectsRoute.getProjects();
}
},
controller: function($scope, projects) { $scope.projects = projects; },
template: '<li ng-repeat="project in projects"> <ui-view/>'
});
$stateProvider.state('top.projects.project', {
url: '/:projectid',
resolve: {
project: function(ProjectsRoute, $stateParams) {
return ProjectsRoute.getProject($stateParams.projectid);
}
}
views: {
'#top': {
controller: function($scope, project) { $scope.project = project; },
template: 'Project details: {{ project.name }} <a ui-sref=".items">view items</a> <ui-view/>'
}
});
$stateProvider.state('top.projects.project.items', {
url: '/projects',
resolve: {
items: function(ItemsRoute, project) {
return ItemsRoute.getItemsForProject(project.id);
}
},
controller: function($scope, items) { $scope.items = items; },
template: '<li ng-repeat="item in items"> <ui-view/>'
});
$stateProvider.state('top.projects.project.items.item', {
url: '/:itemid',
resolve: {
item: (ItemsRoute, $stateParams) {
return ItemsRoute.getItem($stateParams.itemid);
}
},
views: {
'#top.projects.project': {
controller: function($scope, item) { $scope.item = item; },
template: 'Item details: {{ item.name }}'
}
});
I checked the Github Wiki, the Abstract States is enough.

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