Using UI-Router, but I'm not sure that it matters.
What I'm trying to do is extract away my current resolve logic into a factory/service, such that it doesn't pollute my $stateProvider definition. This is a short example:
Here's my current config: ($kinvey is a service that returns a promise)
.state('home', {
url: '/',
views: {
'header': headerView,
'content': {
templateUrl: 'views/main.html',
controller: 'MainCtrl',
},
'footer': footerView
},
resolve: {
user: ['$kinvey', function ($kinvey) {
return $kinvey.User.me();
}]
}
})
And what I'd like it to be:
resolve: {
'user': 'DataResolverService.userResolve'
}
DataResolverService
.factory('DataResolverService', function DataResolverService($kinvey) {
var userResolve = {
user: ['$kinvey', function ($kinvey) {
return $kinvey.User.me();
}]
};
// public API
return {
userResolve: userResolve
};
});
I seem to be getting a Unknown provider: DataResolverService.userResolveProvider <- DataResolverService.userResolve. Is this because factories/services can't be run like this inside the config() block?
This is because you are using a service in the config block. You should be able to inject it like this:
resolve: {
'user': ['DataResolverService', function(DataResolverService){
return DataResolverService.userResolve;
}]
}
An alternative approach would be to hide the injection mess in a constant (don't forget to include $injector as a dependency):
resolve: $injector.get('ResolveMap')
app.constant('ResolveMap',{
'user': ['DataResolverService', function(DataResolverService){
return DataResolverService.userResolve;
}]
});
There was a discussion around this here: https://groups.google.com/forum/#!topic/angular/QtO8QoxSjYw
Related
I have various states that use the same resolve to load a timeLog into my controller $scope before the controller loads. I would like to not reproduce this code but share it between these views. I'm fairly new to JS frameworks, and especially Angular JS.
I'm having a hard time googling this and not finding any decent information. Maybe I'm searching incorrectly or not thinking about this correctly. Any Suggestions?
.config(function($stateProvider) {
$stateProvider
.state('tab.edit-log-summary', {
url: '/logs/edit-log-summary/:timeLogId',
views: {
'tab-logs': {
templateUrl: 'templates/logs/edit-log-summary.html',
controller: 'EditLogSummaryCtrl'
}
},
resolve: {
timeLog: function(config, $stateParams, DailyLog) {
return DailyLog.get({id: $stateParams.timeLogId});
},
}
})
.state('tab.edit-time-log', {
url: '/logs/edit-time-log/:timeLogId',
views: {
'tab-logs': {
templateUrl: 'templates/logs/edit-time-log.html',
controller: 'EditTimeLogCtrl'
}
},
resolve: {
timeLog: function(config, $stateParams, DailyLog) {
return DailyLog.get({id: $stateParams.timeLogId});
},
}
})
})
This really goes down to vanilla Javascript. The resolves are objects. Just define them as a single object somewhere above and pass it to the resolve property each time.
.config(['$stateProvider',function($stateProvider) {
var timeLogResolve = {
timeLog: ['config','$stateParams','DailyLog',function(config, $stateParams, DailyLog) {
return DailyLog.get({id: $stateParams.timeLogId});
}]
};
$stateProvider
.state('tab.edit-log-summary', {
url: '/logs/edit-log-summary/:timeLogId',
views: {
'tab-logs': {
templateUrl: 'templates/logs/edit-log-summary.html',
controller: 'EditLogSummaryCtrl'
}
},
resolve: timeLogResolve,
}
})
.state('tab.edit-time-log', {
url: '/logs/edit-time-log/:timeLogId',
views: {
'tab-logs': {
templateUrl: 'templates/logs/edit-time-log.html',
controller: 'EditTimeLogCtrl'
}
},
resolve: timeLogResolve,
}
})
}])
One suggestion- use inline array notation for providing dependencies. This helps protect your code from breaking if you minify it. I did that myself in the demo above, but I leave it to your discretion to keep it.
I have some routes defined like this :
$stateProvider
.state('app', {
url: '/',
abstract: true,
views: {
'menuContent': {
templateUrl: 'templates/home.html'
}
}
})
.state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.restricted.pending', {
url: '/pending',
views: {
'tabsView': {
templateUrl: 'templates/restricted/restricted-manage-pending.html',
controller: 'RestrictedPendingController as vm'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.restricted.devices', {
url: '/devices',
views: {
'tabsView': {
templateUrl: 'templates/trusted/restricted-manage-devices.html',
controller: 'RestrictedDevicesController as vm'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.grant', {
url: '/grant-access',
views: {
'content': {
templateUrl: 'templates/grant-access.html',
controller: 'GrantAccessController as vm'
}
}
})
;
In these routes I have a restricted area and a grant access page to grant access to the restricted area.
When the isGranted resolve provider is rejected I redirect to the app.grant route.
This is the code doing this :
$rootScope.$on(AngularEvents.STATE_CHANGE_ERROR, _onStateChangeError);
function _onStateChangeError(event, toState, toParams, fromState, fromParams, error){
switch (error) {
case 'accessRejected':
$state.go('app.grant');
break;
}
}
Here is the code of my isGranted provider :
(function() {
'use strict';
angular.module('app')
.provider('isGranted', isGrantedProvider);
isGrantedProvider.$inject = [];
function isGrantedProvider() {
this.$get = isGranted;
isGranted.$inject = ['$q', '$log', 'grantService'];
function isGranted($q, $log, grantService){
$log.log('isGrantedProvider');
if (grantService.isGranted()) {
return $q.when(true);
} else {
return $q.reject('accessRejected');
}
}
}
})();
(grantService.isGranted() just returns a boolean value)
The first time I go to the app.restricted route with $state.go('app.restricted') the provider is executed.
The route is rejected because the access is not granted and we are redirected to the app.grant route.
In this page, the user can log in and have access to the restricted area. Once the user is logged in we redirect him to the app.restricted.pending route but the resolve is not called and the route is rejected and we are redirected to the app.grant route again, whereas the access was granted.
Why is the resolve not called?
Is there a way to force it?
EDIT
I have new information after some testing.
I saw that the resolve is not called the second time only when it is a service:
This resolve is always executed when we enter the state:
state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: ['$log', function($log) {
$log.log('RESOLVE');
}]
}
})
But this resolve is only executed once even when I enter again to the state:
state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: 'isGranted'
}
})
angular.module('app')
.provider('isGranted', isGrantedP);
isGrantedP.$inject = [];
function isGrantedP() {
this.$get = isGranted;
isGranted.$inject = ['$q', '$log'];
function isGranted($q, $log){
$log.log('RESOLVE');
}
}
Why isn't this service called each time? Is it because a service is a singleton? How should I proceed?
After a lot of investigations and testing I found the solution!
First, let's see why it is not working
As mentioned in the docs (http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider), if the resolve is a string, then it corresponds to a service
factory - {string|function}: If string then it is alias for service.
Otherwise if function, it is injected and return value it treated as
dependency. If result is a promise, it is resolved before its value is
injected into controller.
And as mentioned in the angularjs docs (https://docs.angularjs.org/guide/providers), all services are singletons, meaning that it will be instantiated only once
Note: All services in Angular are singletons. That means that the
injector uses each recipe at most once to create the object. The
injector then caches the reference for all future needs.
Why is it important?
Because resolves do not call a function inside our service. They just use the return value of the instantiated service. BUT because our service will be instantiated only once, the return value will always be the same! (because our service initialization is only called once)
What can we do?
From my tests I could see that a resolve defined like this:
resolve: {
myResolve: ['$log', function($log) {
$log.log('My Resolve!');
}]
}
is always executed, so we can write them this way to make it work correctly.
But how can I do if I want to use my service?
The best working solution I found to be able to use my service and have a syntax that looks similar to this one: myResolve: 'myResolveService' is to declare my resolve like this:
resolve: {
myResolve: ['myResolveService', function(MyResolveService) {
myResolveService.log();
}]
}
And my service like this:
angular.module('app')
.factory('myResolve', myResolve);
myResolve.$inject = ['$log'];
function myResolve($log) {
function service(){
this.log = log;
function log() {
$log.log('My resolve!');
}
}
return new service();
}
This code can also be adapted for resolves that return a promise:
Resolve:
resolve: {
myResolve: ['myResolveService', function(MyResolveService) {
return myResolveService.check();
}]
}
Service:
angular.module('app')
.factory('myResolve', myResolve);
myResolve.$inject = ['$q', 'myService'];
function myResolve($q, myService) {
function service(){
this.check = check;
function check() {
var defer = $q.defer();
if (myService.check()) {
defer.resolve(true);
} else {
defer.reject('rejected');
}
return defer.promise;
}
}
return new service();
}
I am new to angular,i need know how to call function next to the roles instead of [] need to call function , in the below example:-
state('signin', {
parent: 'site',
url: '/signin',
data: {
roles: [] //Here i need to call function Becuase i need
assign values like this roles: ['dev','Admin']
(need to know how to call function here)
},
views: {
'': { templateUrl: '/signin.html' },
'content#': {
templateUrl: '/index.html',
controller: 'SigninCtrl'
}
}
})
If , i am trying with resoleve like below example:-
.state('signin', {
parent: 'site',
url: '/signin',
views: {
'': { templateUrl: '/signin.html' },
'content#': {
templateUrl: '/index.html',
controller: 'SigninCtrl'
}
},
resolve: {
TrackerApp: function ($q) {
var defer = $q.defer();
roles: Loadrole()
defer.resolve();
return defer.promise();
}
}
})
Getting Error like:-
TypeError: Cannot read property 'roles' of undefined
if am putting roles inside resolve getting above error
Your resolve method must be like this:
resolve: {
"trackerApp" : "TrackerApp",
"roles" : trackerApp.getRoles();
}
Move the data fetching part to service method like getRoles(). Assign it to roles property which will be available in your controller scope.
I am trying to load a get service JSON function in the main state resolve function so I can store the data to a scope variable.
The account JSON information is relevant because all sub pages are essentially dependent on the information.
--
The below code is partially working. The account resolve function is being successfully called and even the $http returns a promise (state === 0 though). The issue is when the account function resolves the state.controller is never being called.
$stateProvider
.state('app',{
url: '/',
views: {
'header': {
templateUrl: '../views/templates/partials/header.html',
},
'content': {
templateUrl: '../views/templates/partials/content.html'
},
'footer': {
templateUrl: '../views/templates/partials/footer.html',
}
},
resolve: {
account: function($timeout, accountFactory){
//Comment
return $http({method: 'GET', url: '/account.json'});
}
},
controller: ['$scope', 'account', function($scope, account){
// You can be sure that promiseObj is ready to use!
$scope.data = account;
console.log('SCOPE!!!!!');
}],
})
.state('app.accessory', {
url: 'accessory',
views: {
'content#': {
templateUrl: '../views/accessory/listing.html',
controller: 'accessoryListingCtrl',
controllerAs: 'vm'
}
}
})
}]);
Your parent state config is not correct. When using multiple named views A controller does not belong to a state but to a view, so you should move your controller statement to the specific view declaration, or all of them if you need it everywhere.
See here: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views
$stateProvider
.state('report',{
views: {
'filters': {
templateUrl: 'report-filters.html',
controller: function($scope){ ... controller stuff just for filters view ... }
},
'tabledata': {
templateUrl: 'report-table.html',
controller: function($scope){ ... controller stuff just for tabledata view ... }
},
'graph': {
templateUrl: 'report-graph.html',
controller: function($scope){ ... controller stuff just for graph view ... }
},
}
})
I don't know why the controller does not get called. But you can start by making sure that resolve always return data.
resolve: {
account: function($timeout, accountFactory){
//Comment
return $http({method: 'GET', url: '/account.json'})
.$promise.then(
function(data) { return data; },
function(error) { return error; });
}
}
I think this is just basically a question of syntax, but I have this routing set up:
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('addAlbum', {
url: "/albums/create/:artistId",
templateUrl: "public/albums/views/album.tpl.html",
controller: "createAlbumController",
data: {
pageTitle: 'Create Album'
},
resolve: {
albums: function (publicArtistServices) {
return publicArtistServices.getAlbums(1);
}
}
});
}]);
And I need that getAlbums() method to use the artistId passed in on the URL:
return publicArtistServices.getAlbums({need artistId from URL here});
I tried guessing at it by using return publicArtistServices.getAlbums($state.params[0]); but that just threw a "$state is not defined" error.
What's the right syntax here?
$stateParams is what you want to use for this.
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('addAlbum', {
url: "/albums/create/:artistId",
templateUrl: "public/albums/views/album.tpl.html",
controller: "createAlbumController",
data: {
pageTitle: 'Create Album'
},
resolve: {
albums: function ($stateParams, publicArtistServices) {
return publicArtistServices.getAlbums($stateParams.artistId);
}
}
});
}]);
You need to include the $state parameter (to "inject" it into your function):
albums: function ($state, publicArtistServices) {
return publicArtistServices.getAlbums($state.params[0]);
}
If that doesn't work, use $route.current.params.artistId as shown here:
https://stackoverflow.com/a/13433335/584846