Angular UI router, resolve nested views - javascript

I have a single-page application built in angularjs, using UI-Router and nested views. My code looks like this:
app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider' ,'$mdDateLocaleProvider', '$locationProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, $mdDateLocaleProvider, $locationProvider) {
var rootpath = config.paths.components;
$stateProvider.state('home', {
url: '/',
views: {
index: {
templateUrl: rootpath + '_root/template.html',
controller: "RootController"
},
"header#home": {
templateUrl: rootpath + 'header/header-template.html',
controller: "HeaderController"
},
"sidebar#home": {
templateUrl: rootpath + 'sidebar/sidebar-template.html',
controller: "SidebarController"
},
"main#home": {
templateUrl: rootpath + 'main/main-template.html',
controller: "MainController"
}
},
resolve: {
authorize: ['RootService', function (RootService) {
var auth = RootService.getKey();
return RootService.getConfig(auth.key);
}]
},
});
$urlRouterProvider.otherwise('/');
}]).run(['$rootScope', '$state', '$window', function($rootScope, $state, $window){
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){
$window.location = error.status +'.html';
});
In the "home" state, I'm doing a resolve to see if the authentication key passed in is valid.
If the key is invalid or has expired, $stateChangeError event catches the error and redirects the user to an error page.
This works fine when the application starts and on refresh.
Problem is, when the key has expired (only valid for ten minutes) and the application is not reloded, the
$stateChangeError event doesn't catch the error message.
Any thoughts on how to fix this?

You can solve this problem by using the $watch provided by the angular js.
$scope.$watch(function(scope) { return scope.data.myVar },
function() {}
);
The first function is the value function and the second function is the listener function.
This example value function returns the $scope variable scope.data.myVar. If the value of this variable changes, a different value will be returned, and AngularJS will call the listener function.
I think this might solve your problem...

Related

Verify login on state change in AngularJS doesn't work well

I want to verify if the user can access a state before he gets there, if he doesn't have permissions will be redirected to another page.
The problem is that I'm doing a SPA and it verifies the permissions, but it takes a while until the server send the response and the user is redirected, so what happen is that a screen appears for 1 or 2 seconds and then is redirected successfully. Is there anyway to avoid this?
This is the code for the state change:
webApp.run(function ($rootScope, $state, StateService) {
$rootScope.$on('$stateChangeStart', function (event, toState, fromState, toParams) {
StateService.hasAccessTo(toState.name, function(data){
if (data.data != ""){
event.preventDefault();
$state.go(data.data);
}
});
});
});
and the service:
webApp.service('StateService', function($http, $rootScope){
this.hasAccessTo = function(state, callback){
$http.get("state/" + state).then(callback);
}
});
I have also tried with a promise in the $stateChangeStart, but it didn't work.
I read about interceptors, but they work if the user is in another page and access mine, if he is already on the page and type a link manually it doesn't intercepts.
Any modifications or suggestions of new ideas or improvements are welcome!
EDIT
Now I have this:
var hasAccessVerification = ['$q', 'StateService', function ($q, $state, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$urlRouterProvider.otherwise("/");
$compileProvider.debugInfoEnabled(false);
$stateProvider
.state('welcome',{
url:"/",
views: {
'form-view': {
templateUrl: '/partials/form.html',
controller: 'Controller as ctrl'
},
'#': {
templateUrl: '/partials/welcome.html'
}
},
data: {
requireLogin: false
},
resolve: {
hasAccess: hasAccessVerification
}
})
And it validates, but it doesn't load the template. It doesn't show de views. What might I be doing wrong?
EDIT 2
I forgot to add $state here:
var hasAccessVerification = ['$q', '$state', 'StateService', function ($q, $state, StateService){...}
Consider using the resolve in your state configuration instead of using $stateChangeStart event.
According to the docs:
If any of these dependencies are promises, they will be resolved and
converted to a value before the controller is instantiated and the
$stateChangeSuccess event is fired.
Example:
var hasAccessFooFunction = ['$q', 'StateService', function ($q, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl: 'views/dashboard.html',
resolve: {
hasAccessFoo: hasAccessFooFunction
}
})
.state('user', {
abstract: true,
url: '/user',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
template: '<ui-view/>'
})
.state('user.create', {
url: '/create',
templateUrl: 'views/user/create.html'
})
.state('user.list', {
url: '/list',
templateUrl: 'views/user/list.html'
})
.state('user.edit', {
url: '/:id',
templateUrl: 'views/user/edit.html'
})
.state('visitors', {
url: '/gram-panchayat',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
templateUrl: 'views/visitor/list.html'
});
And according to the docs https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-resolved-dependencies resolve are inherited:
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s),
which they can overwrite. You can then inject resolved dependencies
into the controllers and resolve functions of child states.
But, please note:
The resolve keyword MUST be on the state not the views (in case you
use multiple views).
The best practice is to have interceptor on responseError which checks the response status and acts accordingly:
webApp.config(['$httpProvider' ($httpProvider) {
var interceptor = ['$q', '$rootScope', function ($q, $rootScope) {
return {
request: function (config) {
// can also do something here
// for example, add token header
return config;
},
'responseError': function (rejection) {
if (rejection.status == 401 && rejection.config.url !== '/url/to/login') {
// If we're not on the login page
$rootScope.$broadcast('auth:loginRequired');
}
}
return $q.reject(rejection);
}
}
}];
$httpProvider.interceptors.push(interceptor);
}]);
And handle redirection in run block
webApp.run(['$rootScope', function($rootScope){
$rootScope.$on('auth:loginRequired', function () {
$state.go('loginState');
});
}]);
The good thing is that $state service does not need to deal with permission logic:
$stateProvider
.state('someState', {
url: '/some-state',
templateUrl: '/some-state.html',
resolve: {
dataFromBackend: ['dataService', function (postingService) {
// if the request fails, the user gets redirected
return dataService.getData();
}],
},
controller: function ($scope, dataFromBackend) {
}
})
Notice
With this approach, you do not need StateService, all you need to do is to return proper response statuses from backend. For example, if the user is guest, return 401 status.

ui-router onEnter "TypeError: Cannot read property 'globals' of null"

I am using the onEnter property of ui-router state to redirect to a different state depending upon the user-type.
The problem is that it gives TypeError: Cannot read property 'globals' of null at $state.transition.resolved.then.$state.transition when the 'dashboard' state (the one with the onEnter) is used in a ui-sref (the same error is thown if ng-click with $state.go('dashboard') is used.
I also created a very simple example, without resolve, showing this problem
js-fiddle DEMO.
Am I missing something?
in template
DASHBOARD
in the config
$urlRouterProvider.otherwise('/');
$stateProvider
.state('root', {
url: '',
abstract: true,
resolve: {
user: function(authService) {
return authService.requestUser();
}
},
views: {
'footer#': {
templateUrl: 'views/footer.html',
}
}
})
.state('dashboard', {
parent: 'root',
url: '/',
onEnter: ['$state', 'authService', function($state, authService){
console.log ('redirectTo');
if (authService.isStudent()) {
$state.go('student');
}
if (authService.isTeacher()) {
$state.go('teacher');
}
}]
})
.state('student', {
parent: 'root',
url: '/student',
views: {
'header#': {
templateUrl: 'views/student/studentHeader.html',
controller: 'StudentHeaderCtrl'
},
'content#': {
templateUrl: 'views/student/student.html',
controller: 'StudentCtrl'
},
},
})
Answer by Thinley Koblensky:
The problem is caused by using the onEnter property on the dashboard state together with $state.go() and ui-sref
As a workaround:
use $location.path() in the onEnter property function
and use ng-href=“/“ or $location.path(‘/‘) to navigate to the ‘dashboard’ state
Calling a $state.go inside an onEnter function causes problem because of some kind of variables not properly set up. Here is the git issue of the same problem.
https://github.com/angular-ui/ui-router/issues/326
A solution / hack that they suggest in the post is to try to use $location.path() function.
To Avoid this issue, I have tried with $timeout and Bingo. It fixes the issue for me. May be, it is just giving some breathing space to ui-router code flow.
onExit: function($timeout){
'ngInject';
$timeout(function(){
// your code
});
}
Adding to #Thinley Koblensky's answer above - I find a better workaround is to capture the $stateChangeStart event then delegate it to the individual states like so:
.run(function($rootScope, $injector) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if (!toState.data || !angular.isFunction(toState.data.stateChangeStart)) return;
$injector.invoke(toState.data.stateChangeStart, null, {
event: event,
toState: toState,
toParams: toParams,
fromState: fromState,
fromParams: fromParams
});
});
})

AngularJS accessing $stateParams in run function

I would like to be able to access the url parameters in my run function and make actions based on those params. However, I was pretty surprised to see that ui.router's $stateParams object is empty in my run function.
angular.module('app', ['ui.router'])
.run(['$rootScope', '$state', '$stateParams', '$location', function($rootScope, $state, $stateParams, $location) {
$rootScope.$state = $state;
$rootScope.location = $location;
$rootScope.$stateParams = $stateParams;
$rootScope.$on('$stateChangeStart', function(event, toState) {
if (toState.name === 'main') {
event.preventDefault();
$state.go('main.child', {
param: 1
})
}
});
console.log('$stateParams is empty!');
console.log($stateParams);
console.log($rootScope.$stateParams);
}])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', function($stateProvider, $urlRouterProvider, $locationProvider) {
$stateProvider
.state('main', {
url: '',
template: '<div>hey</div><div ui-view></div>'
})
.state('main.child', {
url: '/:param',
template: 'child state template'
});
}]);
Is it possible to access the params accessed from $stateParams in the run function? Or are they only available in the config function? Why are they not accessible in the run function?
Plnkr: http://plnkr.co/edit/1YXKRmR48LyxbGXWq66Y?p=preview
Thanks for any help.
It looks like you are using StateChangeStart event and you can access parameters in this event:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//you code goes here
console.log(toParams);
}
I am using it to add some custom logic to parametrs (authentication redirects)

$state.go() not routing to other state, #/null in url

Using angular ui-router I'm trying to use $state.go() to change to the blogEdit state after creating a new entry with blogCreate to continue editing after saving. When I click to save and trigger addPost() method, it doesnt redirect correctly and I see /#/null as the route in the address bar instead of the expected /blog/post/:postId/edit.
blogModule.controller('PostCreateController', ['$scope', '$state', '$stateParams', 'PostResource',
function ($scope, $state, $stateParams, PostResource) {
$scope.post = new PostResource();
$scope.addPost = function () {
$scope.post.$save(function () {
$state.go('blogEdit', {postId: $stateParams.postId}); // THIS SHOULD REDIRECT TO CONTINUE EDITING POST
});
}
}
]);
blogModule.controller('PostEditController', ['$scope', '$stateParams', 'PostResource',
function ($scope, $stateParams, PostResource) {
$scope.post = PostResource.get({postId: $stateParams.postId});
$scope.updatePost = function () {
$scope.post.$update({postId: $stateParams.postId});
}
}
]);
State route configuration:
var app = angular.module('app', [
'ui.router',
'blogModule'
]);
app.config(['$stateProvider', function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('blog', {
url: '/blog',
templateUrl: 'app/blog/view/blog-list.html',
controller: 'PostListController'
})
.state('blogView', {
url: '/blog/post/{postId:[0-9]}',
templateUrl: 'app/blog/view/blog-detail.html',
controller: 'PostViewController'
})
.state('blogCreate', {
url: '/blog/post/new',
templateUrl: 'app/blog/view/blog-create.html',
controller: 'PostCreateController'
})
.state('blogEdit', {
url: '/blog/post/{postId:[0-9]}/edit',
templateUrl: 'app/blog/view/blog-edit.html',
controller: 'PostEditController'
});
}]);
It seems to do this regardless of what state I try to change to.
I suppose you are saving your post on backend. When you perform save (PUT) operation your backend should return you some response. The response should be like HTTP 201 Entity created and there should be location attribute set (f.e. http://example.com/blog/post/1). Then you can get the id from location header like this:
$scope.post.$save(function (createdPost, headers) {
var postId = headers.location.split("/").pop();
$state.go('blogEdit', {postId: postId});
});
Another way is to just ignore headers and return json response from your backend. F.e. {"postId": 1, "title": "New post", ...}. Then you can do something like:
$scope.post.$save(function (createdPost) {
$state.go('blogEdit', {postId: createdPost.postId});
});
The most important is to know API of your backend (what "it returns").

Incorrect context within $on in AngularJS

I'm attempting to handle http errors within AngularJS (using ui-router), but am losing the context of my run function in the following code.
bugtracker.run(['$rootScope', '$state', function($rootScope, $state) {
//at this point $state and $rootScope are defined
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
//at this point $state and $rootScope are undefined
});
}]);
The code that causes $stateChangeError to trigger is as follows.
//main.js
bugtracker.config(['$stateProvider', '$httpProvider', '$compileProvider', function($stateProvider, $httpProvider, $compileProvider) {
//...
$stateProvider.state('projects.show', {
url: '/{projectId:[0-9]{1,8}}',
templateUrl: '/assets/projects/show.html',
controller: 'ProjectShowCtrl',
resolve: bugtracker.controller('ProjectShowCtrl').resolve
});
//...
}]);
//ProjectShowCtrl.js
projectShowCtrl.resolve = {
project: function(Project, $q, $stateParams, $state) {
var deferred = $q.defer();
Project.findById($stateParams.projectId, function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(errorData); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
};
I would like $state to be defined within the anonymous function called by the $on function so that I could redirect the user to a 401, 403, etc. page, but I'm unsure why it is not.
In other examples I have seen (https://github.com/angular-ui/ui-router/issues/898) it is implied that $state is defined within the context of the anonymous function.
If anyone could explain why $state is not defined or what I can change to make it defined I would greatly appreciate it!
There's no way these would be undefined, unless they were already undefined in the run block. However I've seen cases where the debugging tool thinks some variables are undefined when they actually aren't. That happened to me in FireBug with an old FF version.

Categories