How do I prevent a state change for a specific "to" state in ui-router (is it using onEnter?) assuming I have this route:
.state('auth.confirm', {
url: '/confirm/:resetToken',
template: '<confirm-page></confirm-page>',
data: { pageTitle: 'Confirm Reset', specialClass: 'gray-bg' }
})
and this service with this promise-based function:
validateResetToken: function(resetToken) {
var self = this;
var deferred = $q.defer();
$http.post(AppConstants.hostRootUrl + '/auth/reset/validate', { resetToken: resetToken })
.then(function(response) {
if(response.data && response.data.success) {
// if we got a 200 return and it indicates success in the response, resolve
self.message = 'Success';
deferred.resolve(self.message);
}
else if (response.data && !response.data.success && response.data.error) {
// if we got a 200 return, but success is falsey and there's an error message, reject with that message
self.message = response.data.error;
deferred.reject(self.message);
}
else {
// error with generic message
self.message = 'Unknown response. Contact administrator.';
deferred.reject(self.message);
}
}, function(errPost) {
if (errPost.data && errPost.data.error) {
self.message = errPost.data.error;
deferred.reject(self.message);
}
else {
self.message = 'Could not connect.';
deferred.reject(self.message);
}
});
return deferred.promise;
},
For posterity (and Googlers) sake, Alon Eitan made me take a second look at my resolve approach, and I realized that my addition of the catch() was causing the rejected promise to not percolate up. This final code works:
.state('auth.confirm', {
url: '/confirm/:resetToken',
template: '<confirm-page></confirm-page>',
data: { pageTitle: 'Confirm Reset', specialClass: 'gray-bg' },
resolve: {
validated: function($q, $stateParams, AuthService, toastr) {
//$log.log('auth.confirm resolve $stateParams',$stateParams);
return AuthService.validateResetToken($stateParams.resetToken).catch(function(validateErr) {
toastr.error(validateErr, 'Blocked', {closeButton: true});
return $q.reject(validateErr);
});
}
}
})
You can create a rule as in https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-create-rules-to-prevent-access-to-a-state
Adapt their example:
app.config(function($stateProvider) {
$stateProvider.state('privatePage', {
data: {
rule: function(token) {
return validateResetToken(token)
}
});
});
app.run(function($rootScope, $state, $stateParams) {
$rootScope.$on('$stateChangeStart', function(e, to) {
if (!angular.isFunction(to.data.rule)) return;
var result = to.data.rule($stateParams.resetToken);
if (result && result.to) {
e.preventDefault();
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(result.to, result.params, {notify: false});
}
});
});
Related
In my code, I define the following two routes:
$routeProvider.when('/problem/report', {
templateUrl: '/app/management/problem/reportAProblem.html',
controller: 'reportAProblemCtrl',
resolve: {
authorized: function($http, $location) {
var path = $location.path();
return $http.get('/svc/authorize/view?urlPath=' + path).then(function(response) {
var data = response.data;
if (response.data.result === 'NOT_AUTHORIZED') {
throw "NOT_AUTHORIZED";
}
return data;
})
}
}
});
$routeProvider.when('/problem', {
templateUrl: '/app/management/problem/problem.tmpl.html',
controller: 'problemCtrl',
resolve: {
authorized: ['$authorization', function($authorization) {
$authorization.authorize();
}]
}
});
The first case seems to work. However, the second case has had the function refactored into a Service, and AngularJS does not seem to be waiting for the Promise to resolve before displaying the page.
The refactored code looks like the following:
angular.module('authorization', [])
.factory('$authorization', ['$http', '$location',function($http, $location) {
var $authorization = {};
$authorization.authorize = function() {
var path = $location.path();
return $http.get('/svc/authorize/view?urlPath=' + path).then(function(response) {
var data = response.data;
if (response.data.result === 'NOT_AUTHORIZED') {
throw "NOT_AUTHORIZED";
}
return data;
});
}
return $authorization;
}]);
Can anyone tell me why the second case above doesn't wait for the promise to resolve before displaying the page?
Add return:
authorized: ['$authorization', function($authorization) { **return** $authorization.authorize(); }]
In my code, I define the following two routes:
$routeProvider.when('/problem/report', {
templateUrl: '/app/management/problem/reportAProblem.html',
controller: 'reportAProblemCtrl',
resolve: {
authorized: function($http, $location) {
var path = $location.path();
return $http.get('/svc/authorize/view?urlPath=' + path).then(function(response) {
var data = response.data;
if (response.data.result === 'NOT_AUTHORIZED') {
throw "NOT_AUTHORIZED";
}
return data;
})
}
}
});
$routeProvider.when('/problem', {
templateUrl: '/app/management/problem/problem.tmpl.html',
controller: 'problemCtrl',
resolve: {
authorized: ['$authorization', function($authorization) {
$authorization.authorize();
}]
}
});
The first case seems to work. However, the second case has had the function refactored into a Service, and AngularJS does not seem to be waiting for the Promise to resolve before displaying the page.
The refactored code looks like the following:
angular.module('authorization', [])
.factory('$authorization', ['$http', '$location',function($http, $location) {
var $authorization = {};
$authorization.authorize = function() {
var path = $location.path();
return $http.get('/svc/authorize/view?urlPath=' + path).then(function(response) {
var data = response.data;
if (response.data.result === 'NOT_AUTHORIZED') {
throw "NOT_AUTHORIZED";
}
return data;
});
}
return $authorization;
}]);
Can anyone tell me why the second case above doesn't wait for the promise to resolve before displaying the page?
Add return:
authorized: ['$authorization', function($authorization) { **return** $authorization.authorize(); }]
//the controller that creates the datatable
app.controller('AdminListCtrl', function ($scope, $compile, DTOptionsBuilder, DTColumnBuilder, adminService) {
var vm = this;
function stateChange(iColumn, bVisible) {
console.log('The column', iColumn, ' has changed its status to', bVisible);
}
//vm.dtOptions = DTOptionsBuilder.fromSource('http://localhost/api-v1/admin')
vm.dtOptions = DTOptionsBuilder.fromFnPromise(function() {
return adminService.loadAdmin();
})
.withPaginationType('full_numbers')
.withOption('createdRow', createdRow)
// Add Bootstrap compatibility
.withBootstrap()
// Active ColVis plugin
.withColVis()
// Add a state change function
.withColVisStateChange(stateChange)
// Exclude the last column from the list
.withColVisOption('aiExclude', [2])
// Add Table tools compatibility
.withTableTools('scripts/vendor/datatables/TableTools/swf/copy_csv_xls_pdf.swf')
.withTableToolsButtons([
'copy',
'print', {
'sExtends': 'collection',
'sButtonText': 'Save',
'aButtons': ['csv', 'xls', 'pdf']
}
]);
//adminService to request for all administrators
app.factory('adminService', ['ApiService', function (ApiService) {
return {
loadAdmin: function () {
ApiService.get("admin").then(function (response) {
if (response) {
if (response.success === true) {
return response;
}else{
console.log(response);
}
}else {
console.log('error request ');
}
});
}
};
}]);
//apiservice to interact with api
app.factory('ApiService', function ($http, $q, $localStorage) {
return {
get: function (apiresource) {
var returnData = $q.defer();
$http({
url: api + apiresource,
method: 'GET',
headers: {"Auth-Token": $localStorage.user_data.auth_token}
})
.success(function (data) {
returnData.resolve(data);
})
.error(function (error) {
returnData.resolve();
});
return returnData.promise;
}};
});`enter code here`
When ever I am in that view it throws this errorCannot read property 'then' of undefined. I am following examples from these two sources
http://www.revillweb.com/angularjs-by-example/4-sharing-data-with-angularjs-services/
http://l-lin.github.io/angular-datatables/#/withPromise
You need to return promise object (result of ApiService.get("admin") call) from loadAdmin method.
Also make sure you don't "swallow" rejections inside of the then (in console.log branches) - what happens when you unintentionally handle errors by not passing it further. For this return rejected promise or simply throw error, so that rejection will propagate further down the promise chain:
app.factory('adminService', ['ApiService', function (ApiService) {
return {
loadAdmin: function () {
return ApiService.get("admin").then(function (response) {
if (response) {
if (response.success === true) {
return response;
} else{
console.log(response);
throw response;
// or custom error object: throw {message: 'Error loadAdmin', response}
}
} else {
console.log('error request ');
throw new Error('error request');
}
});
}
};
}]);
Below is my code snippet where i'm trying to use uimodal to display the user detail along with additional detail.
I'm failing to bind the response data to uimodal, Kindly help to resolve this.
$scope.selectedUserData = '';
$scope.edituser = function (user) {
usereditService.resp(size.userid, function (response) {
if (response != false) {
console.log(response[0]);//Specific user details object from API
selectedUserData = response[0];
}
else {
console.log('no user found');
}
});
$scope.modalInstance = $uibModal.open({
animation: false,
backdrop: 'static',
templateUrl: '/_views/_editUser.html',
controller: 'userController',
size: size,
resolve: {
selectedData: function () {
return $scope.selectedUserData;
}
},
controller: function($scope, selectedData) {
$scope.editobj = selectedData;
}
});
modalInstance.result.then(function (response) {
$scope.selected = response;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
The data is coming from async response, so can't get that ajax response data as soon as your requested for it.
You need to follow promise pattern in that case, and return the data in promise chain patter from your resolve's selectedData function.
//assuming usereditService.resp is returning promise object
//it it doesn't returning promise object, you need to create it by custom promise using $q
var userDataPromise = usereditService.resp(size.userid).then(function (response) {
if (response != false) {
console.log(response[0]);//Specific user details object from API
selectedUserData = response[0];
return selectedUserData;
}
else {
console.log('no user found');
return 'no user found';
}
}, function(error){
console.log();
return 'no user found';
});
$scope.modalInstance = $uibModal.open({
animation: false,
backdrop: 'static',
templateUrl: '/_views/_editUser.html',
controller: 'userController',
size: size,
resolve: {
selectedData: function () {
//returning response object here as a promise.
return userDataPromise;
}
},
controller: function($scope, selectedData) {
$scope.editobj = selectedData;
}
});
May be I miss something but
if I set a state like this:
.state('session_register', {
url: '/privatearea',
resolve: {
isLogged: function(Session){
return Session.isLogged();
}
},
templateUrl: 'private/user.html',
controller:'UserController'
})
if isLogged return a 401 status (resolve fails)
even if I don't really see the user.html in the browser
the partial is just loaded (firebug)
So I'm wondering
Is it the wanted behaviour ?
Is there a way to not load partial
when a resolve fails ?
I've got also an httpInterceptor
.factory('httpInterceptor', ['$q', '$location',
function($q, $location) {
return {
'response': function(response) {
if (response.status === 401) {
$location.path('/auth/login');
return $q.reject(response);
}
return response || $q.when(response);
},
'responseError': function(rejection) {
if (rejection.status === 401) {
$location.url('/auth/login');
return $q.reject(rejection);
}
return $q.reject(rejection);
}
};
}
])
//Http Interceptor to check auth failures for XHR requests
.config(['$httpProvider',
function($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
}
]);
END UP
.factory('httpInterceptor', ['$q','$location',function ($q,$location) {
var canceller = $q.defer();
return {
'request': function(config) {
// promise that should abort the request when resolved.
config.timeout = canceller.promise;
return config;
},
'response': function(response) {
return response;
},
'responseError': function(rejection) {
if (rejection.status === 401) {
canceller.resolve('Unauthorized');
$location.url('/user/signin');
return $q.reject(rejection);
}
if (rejection.status === 403) {
canceller.resolve('Forbidden');
$location.url('/');
return $q.reject(rejection);
}
return $q.reject(rejection);
}
};
}
])
//Http Intercpetor to check auth failures for xhr requests
.config(['$httpProvider',function($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
}]);
it works :)
I think it is by design. Apart from an extra bandwidth, there is no need to block the template loading, otherwise the view will complete slower.
And there is no way to change this behavior too. From the source code:
https://github.com/angular-ui/ui-router/blob/0.2.10/src/state.js#L1158
dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
var promises = [ dst.resolve.then(function (globals) {
dst.globals = globals;
}) ];
if (inherited) promises.push(inherited);
// Resolve template and dependencies for all views.
forEach(state.views, function (view, name) {
var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {});
injectables.$template = [ function () {
return $view.load(name, { view: view, locals: locals, params: $stateParams, notify: false }) || '';
}];
promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) {
// References to the controller (only instantiated at link time)
if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) {
var injectLocals = angular.extend({}, injectables, locals);
result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals);
} else {
result.$$controller = view.controller;
}
// Provide access to the state itself for internal use
result.$$state = state;
result.$$controllerAs = view.controllerAs;
dst[name] = result;
}));
});
// Wait for all the promises and then return the activation object
return $q.all(promises).then(function (values) {
return dst;
});
A promise from state.resolve:
$resolve.resolve(state.resolve, locals, dst.resolve, state);
and $view.load (which make a http request for the templateUrl):
$view.load(name, { view: view, locals: locals, params: $stateParams, notify: false })
not wait for each other to finished first, it happens in pararell, and used in the return statement:
return $q.all(promises).then(function (values) {
Hope this clear things up.