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.
Related
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>
My UI includes nested tabs (using Bootstrap). The tab in question is a template with its own controller, currently specified on a view with UI-Router:
.state('editBase.edit', {
url: '/:id',
views: {
'fooTab#editBase.edit': {
templateUrl: 'foo.html',
controller: 'FooController'
},
// more views...
The template uses ng-repeat to create a list of links. Each link goes to an editor child state:
<a ui-sref="editBase.edit.subEdit({itemId: item.id})">{{item.name}}</a>
What I want to happen when the link is clicked is for the contents of the tab (the list) to be replaced with the editor. On clicking a save button, the list would reappear refreshed.
I'm not looking for an in-place editor for a list; I want to hide the entire list, but leave the surrounding templates/tabs intact. In other words, I don't want to replace the root view. How can I do this?
Note: I found this post that explains multiple nested views and view names; this helped me a lot.
In the edit state, create a view that targets the list's <ui-view>
A parent state, just for demonstration. The parent state has a ui-view that the list state plugs into
$stateProvider.state({
name: 'app',
template: '<div ui-view="list"></div>'
});
The list state targets the <ui-view="list">, which was created by the parent state
$stateProvider.state({
name: 'app.list',
url: '/objects',
views: {
list: {
controller: ...
template: `<ul><li ng-repeat="..."></li></ul>
}
}
});
The edit state targets the <ui-view="list"> that was created by the app state. The ui-view was previously filled in by the app.list state. However, the child state's view targeting that ui-view takes precedence over the parent state view.
$stateProvider.state({
name: 'app.list.edit',
url: '/edit/:id',
views: {
"list#app": {
controller: ...
template: "<a ui-sref="^">Go back...</a><form>...</form>"
}
}
}) ;
Here's a working plunker that demonstrates it: http://plnkr.co/edit/Uc39R2ZHUg2Ru3xyEkfe?p=preview
I have a layout set up for one of my pages that is then seeded with a ton of little views that I use to populate with date. My states currently looks like so:
.state('eventLayout', {
templateUrl: 'partials/layouts/event.html',
controller: 'EventCtrl',
})
.state('event', {
parent: 'eventLayout',
url: '/event/{eventUrl}',
views: {
'event.video': {
templateUrl: 'partials/views/event.video.html'
},
'event.info': {
templateUrl: 'partials/views/event.info.html'
},
'card.event': {
templateUrl: 'partials/views/card.event.html'
},
'card.clip': {
templateUrl: 'partials/views/card.clip.html'
},
'card.upcoming': {
templateUrl: 'partials/views/card.upcoming.html'
},
'card.banner': {
templateUrl: 'partials/views/card.banner.html'
},
'card.comment': {
templateUrl: 'partials/views/card.comment.html'
},
'card.notification': {
templateUrl: 'partials/views/card.notification.html'
},
'card.cube': {
templateUrl: 'partials/views/card.cube.html'
},
'card.mix': {
templateUrl: 'partials/views/card.mix.html'
},
'card.score': {
templateUrl: 'partials/views/card.score.html'
},
'card.sponsor': {
templateUrl: 'partials/views/card.sponsor.html'
},
'card.nobroadcasters': {
templateUrl: 'partials/views/card.nobroadcasters.html'
},
'card.link': {
templateUrl: 'partials/views/card.link.html'
},
'card.suggest': {
templateUrl: 'partials/views/card.suggest.html',
controller: 'SuggestblockCtrl'
},
'card.footer': {
templateUrl: 'partials/views/card.footer.html'
}
}
})
As you can see the parent layout holds my Controller for the page which is called EventCtrl . Now I would expect that all the views now have access to this controller, but that doesn't seem to be the case. Instead I have to wrap the main parent template from eventLayout in a div where I then just use the old school:
<div ng-controller="EventCtrl"></div>
I'd like to at least understand why this is happeneing and what the proper method is to make sure all views have access to the states main controller. Thanks!
EDIT:
To add more context to how im using the views in my current app I have detailed the current set-up below.
From the file partials/layouts/event.html in parent $state eventLayout
<div ng-controller="EventCtrl">
<div ui-view="event.video"></div>
<div ng-repeat="activity in activities.results">
<div ng-if="activity.card_type == 'event'" ui-view="card.event"></div>
<div ng-if="activity.card_type == 'clip'" ui-view="card.clip"></div>
<div ng-if="activity.card_type == 'upcoming'" ui-view="card.upcoming"></div>
</div>
</div>
</div>
As you can see a views are nested within the parent layout. I'm having to wrap it all with ng-controller="EventCtrl" in order to allow each view access to its scope.
The overall angular's ui-router design, is about the view / $scope inheritance - not base controller accesibility. The detailed info could be found here:
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...
Also these are good reading, which content would be hardly any better explained here:
AngularJS Inheritance Patterns by Minko Gechev
AngularJS–Part 3, Inheritance by Gabriel Schenker
So, let's summarize a bit.
1) We know, that from any template we can access only its own $scope.
2) What is available in the view/template $scope, is a job of its Controller which can extend it with some functions, objects, etc.
3) If any parent controller (from view-nesting perspective) will inject anything into its own/parent scope - we will have access to it as well (so called prototypical inheritance)
Having this, we can create an Events object in the parent $scope, managed by EventCtrl - and consume it in any a child view template:
// the reference (reference to object Events)
// to be shared accross all child $scopes
$scope.Events = {};
// objects
$scope.Events.MyModel = { FirstName: "....
// functions
$scope.Events.save = function () {....
And now in any child template we can use it like this
<div>{{Events.MyModel.FirstName}}</div>
Another technique would be to place the controller into $scope's Events object:
$scope.Events = this; // controller
And then have full access to controller's methods, properties...
I'm using the excellent ui-router module in my application. As part of this, I'm using named views to manage the 'dynamic sub-navigation' I have in the app.
Consider the following:
$urlRouterProvider.otherwise('/person/list');
$stateProvider
.state('person', {
url: '/person',
abstract: true,
})
.state('person.list', {
url: '/list',
views: {
"main#": {
templateUrl: "person.list.html",
controller: 'PersonListController'
}
}
})
.state('person.details', {
url: '/{id}',
views: {
'main#': {
templateUrl: "person.details.html",
controller: 'PersonController'
},
'nav#': {
templateUrl: "person.nav.html",
controller: 'PersonNavController'
}
}
});
When users first visit the app, they are presented with a list of people. When they click on a person, they are taken to the details page. Pretty basic stuff. Here's the markup if it helps...
<div>
<aside ui-view="nav"></aside>
<div ui-view="main"></div>
</div>
However, the PersonNavController calls a REST service to get a list of people, so when viewing a person, the user is able to navigate sibling elements. Using the method above causes the template and controller to re-render, thus causing a delay after every click, despite the content never changing.
Is there a way to keep the 'nav#' view loaded, and only refresh the 'main#' view?
The way I am using ui-router in this scenarios is: move the views to the least common denominator.
Other words: In case that ui-view="nav" is shared among all the details and is the same for all of them (because it should be loaded only once) - it should be part of the list state (parent of the detail state)
the parent state defintion would be adjusted like this:
.state('person.list', {
url: '/list',
views: {
"main#": {
templateUrl: "person.list.html",
controller: 'PersonListController'
}
// here we target the person.list.html
// and its ui-view="nav"
'nav#person.list': {
templateUrl: "person.nav.html",
controller: 'PersonNavController'
}
}
So where is the trick? In the power of the angular ui-router. We can, during each state defintion, target the current view. Now, the nav view is part of the list state definition - i.e. it will not be reloaded during the detail switching (also check here for more explanation)
We just have to use the defined naming conventions, see:
View Names - Relative vs. Absolute Names
Few cited lines from the mentioned documentation:
views: {
////////////////////////////////////
// Relative Targeting //
// Targets parent state ui-view's //
////////////////////////////////////
// Relatively targets the 'detail' view in this state's parent state, 'contacts'.
// <div ui-view='detail'/> within contacts.html
"detail" : { },
// Relatively targets the unnamed view in this state's parent state, 'contacts'.
// <div ui-view/> within contacts.html
"" : { },
///////////////////////////////////////////////////////
// Absolute Targeting using '#' //
// Targets any view within this state or an ancestor //
///////////////////////////////////////////////////////
// Absolutely targets the 'info' view in this state, 'contacts.detail'.
// <div ui-view='info'/> within contacts.detail.html
"info#contacts.detail" : { }
// Absolutely targets the 'detail' view in the 'contacts' state.
// <div ui-view='detail'/> within contacts.html
"detail#contacts" : { }
I'm using Angular's ui-router on my application to try and route to child views of a main view. Both the main and the child have their own associated IDs. Currently I can navigate to the parent, but my link to the child is not working.
In my Application.js
$stateProvider
//Working Route
.state('Project', {
url: '/Project/{projectId}',
views: {
"ContentMain" : {
templateUrl: "/Scripts/Dashboard/templates/MainContent/ProjectMainContent.html",
controller: function ($stateParams) {
console.log("Project state hit!");
}
},
...
}
})
//Non-Working Route
.state('Project.ViewResource', {
url: '/Resource/:resourceId',
parent: 'Project',
views: {
"ContentMain" : {
templateUrl: "/Scripts/Dashboard/templates/MainContent/ProjectResourceViewContent.html"
controller: function ($stateParams) {
console.log("Project.ViewResource state hit!");
}
},
...
}
});
In my HTML:
<!-- Working Link-->
<a ui-sref="Project({ projectId: 5 })"><h3> My Projects </h3></a>
<!-- Non-working Links -->
<a ui-sref="Project.ViewResource({ projectId: 5, resourceId: 3 })">View Project Resource. </a>
<a ui-sref="Project.ViewResource({ resourceId: 3})">I'm a Resource Image. </a>
The first link works, however when I click either of the "non-working" child links my browser updates to: "Home/Index/#/Project/5/Resource/3" which is the desired route, however the page content
Any idea where I'm going wrong?
Edit1: To add the lines of code in the 'views' object which should be swapping out.
Edit2: To further demonstrate the issue, I've added the controller code blocks. When I hit the first html link, my console outputs "Project state hit!" When I click either of the non-working links, there is no new output to the console. Ie, the route is likely not being hit.
Figured out what was happening. After taking a closer look at the document on multiple named views here, I realized that my child view was searching for ui-view tags within the parent template, rather than the root template. Essentially, my child was trying to nest within my parent, while my desired behavior was to replace the parent views.
So, to target ui-views within the root, my solution looked like:
.state('Project.Resource', {
url: '/Resource/{resourceId}',
parent: 'Project',
views: {
"MainControls#": { templateUrl: "/Scripts/Dashboard/templates/MainControls/MainControls.html" },
"ContentMain#": {
...
},
...
}
})