AngularJS - Updating data from resolve function (routeProvider) - angularjs

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
}
})
....

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>

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

Angular UI-router and using dynamic templates

I'm trying to load a template file using a rootscope value as for it's name.
I have a init controller which sets the $rootScope.template to "whatever.html", then I have my route like this:
$stateProvider.state('/', {
url: '/',
access: 'public',
views: {
page: {
controller: 'HomeCtrl',
templateProvider: function($templateFactory, $rootScope) {
return $templateFactory.fromUrl('/templates/' + $rootScope.template);
}
}
}
});
But this doesn't work. It actually freezes the whole chrome so that I have to kill the process in order to stop it... I've also tried this with templateUrl but with no results.
So how could I use my dynamic template file with UI-router?
Similiar to your other question (in order I found them): Angular and UI-Router, how to set a dynamic templateUrl, I also created a working plunker to show how to. How it would work?
So, if this would be state call:
#/parent/child/1
#/parent/child/2
And these would be states:
$stateProvider
.state('parent', {
url: '/parent',
//abstract: true,
templateUrl: 'views.parentview.html',
controller: function($scope) {},
});
$stateProvider
.state('parent.child', {
url: '/child/:someSwitch',
views: {
// see more below
Then we can use this templateProvider definiton:
templateProvider: function($http, $stateParams, GetName) {
// async service to get template name from DB
return GetName
.get($stateParams.someSwitch)
// now we have a name
.then(function(obj){
return $http
// let's ask for a template
.get(obj.templateName)
.then(function(tpl){
// haleluja... return template
return tpl.data;
});
})
},
What we can see is chaining of async results:
// first return of promise
return asyncstuff
.then(function(x){
// second return of a promise once done first
return asyncstuff
.then(function(y){
// again
return asyncstuff
.then(function(z){
return ... it
}
}
}
And that's what the magical templateProvider can do for us... wait until all promises are resolved and continue execution with known template name and even its content. Check the example here. More about template provider: Angular UI Router: decide child state template on the basis of parent resolved object

AngularJS ui-router abstract state

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; });

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');

Resources