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
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>
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 recently migrated from ngRoute to ui-router. I have a page that has 2 sections.
Right section displays current item details,
Left section shows similar items to current item.
Once user clicks a similar item from left list, right section will reload with clicked item id and left section will stay same.
To keep left section still on user item navigations, i defined left section as an abstract state and right section as it's child state. (you cant view similar items if you arent looking to an item).
Left section (listview) is parent and contains a ui-view in HTML to embed item details.
To load similar items on page open, i need to know which item is being loaded by my child state. But i cant define same url for both abstract state and it's child state.
i tried to resolve $stateParams in abstract state with no chance.
my state configuration is below
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('item', {
//url: '/items/:itemName/:itemID'
url: '/items',
abstract: true,
templateUrl: 'Scripts/app/item/ItemListTemplate.html',
controller: 'ItemListController as itemList'
}).state('item.itemDetails', {
url: '/:itemName/:itemID',
templateUrl: 'Scripts/app/item/ItemDetailTemplate.html',
controller: 'ItemDetailController as itemDetail'
});
how can i access itemID from my abstract state (from ItemListController)?
i solved the problem using angular scope variable. parent contains an ui-view to show child state contents. so child and parent state share same scope therefore i can notify parent about selected item id and parent can update it's view.
parent state has a scope function like this
$scope.notifySelectedItem = function(selectedItemID){};
and child state can call this function from it's scope
$scope.notifySelectedItem($stateParams.itemID);
I would model this as a single state with two named views. The subject of your state is a single item and related data, so your state should probably revolve around that. You have described two views, one which shows the item details, and another which shows other things, but the primary subject is still the one item.
$stateProvider
.state('item', {
url: '/item/:itemID',
views: {
detail: {
templateUrl: 'Scripts/app/item/ItemDetailTemplate.html',
controller: 'ItemDetailController as itemDetail'
}, related: {
templateUrl: 'Scripts/app/item/ItemListTemplate.html',
controller: 'ItemListController as itemList'
}
}
});
In your parent view, add two named ui-views, where the views defined above will plug into:
<div ui-view="related"></div>
<div ui-view="detail"></div>
Now, you can enhance this using resolves to preload the data before the controller is invoked:
$stateProvider
.state('item', {
url: '/item/:itemID',
resolve: {
item: function($http, $stateParams) {
return $http.get('/item/' + $stateParams.itemID);
},
related: function($http, $stateParams) {
return $http.get('/item/' + $stateParams.itemID + "/related");
},
},
views: {
detail: {
templateUrl: 'Scripts/app/item/ItemDetailTemplate.html',
controller: 'ItemDetailController as itemDetail'
}, related: {
templateUrl: 'Scripts/app/item/ItemListTemplate.html',
controller: 'ItemListController as itemList'
}
}
});
app.controller("ItemListController", function($scope, related) { $scope.related = related.data; });
app.controller("ItemDetailController", function($scope, item) { $scope.item = item.data; });
I have an abstract state and other states that inherit from it.
state.general (abstract)
|----------->state.general.list // It will list a collection
|
|-----------state.general.view // It will display specific element from collection
Since several views will use the same data, I was thinking in retrieving the data in the resolve function so it is injectable in both views.
The issue I'm facing is when I'm updating the collection in state.general.view. If I edit an element and go back to the state.general.list state, the collection will not show the data I have edited.
What would be the way to go? I though about init a service in the state.general resolve function and inject this in controllers. How does it sound? Is there any other solution more appropriate?
/* CODE */
.state('state.general',
{
abstract: true,
url: '/',
templateUrl: './partials/abstract.html',
resolve: {
collection: function(APIService){
return APIService.query();
}
}
}
.state('state.general.list', {
url: '/list',
templateUrl: './partials/list.html',
controller: function(collection){
// Do something with collection
}
})
....
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" : { }