Trouble passing parameter to child state in ui-router - javascript

I have two states, one is a child of the other. One represents a list of people (people) and one for when you click on an individual person to view more details (people.detail).
My first state works as intended, and has several parameters which represent all the various server side filters and paging you could apply. The child state is a modal window, which popups as expected but my only paramater personID never makes it into $stateParams. I wonder if it's something to do the combination of the RESTful style URL and the query string style?
It is perhaps worth noting that $stateParams is populated with everything you'd expect from the parent state.
EDIT: Plunker to show what I mean - http://plnkr.co/edit/eNMIEt?p=info (note that the ID is undefined)
app.js
$urlRouterProvider.otherwise('/people');
$stateProvider
.state('people', {
url: '/people?pageNumber&pageSize&sortField&sortDirection&search&countryID&jobFunctionIDs&firmTypeIDs',
templateUrl: 'Static/js/angular/views/people-list.html',
controller: 'PeopleListController',
resolve: {
api: "api",
people: function (api, $stateParams) {
//Code ommitted
},
countries: function (api) {
//Code ommitted
},
jobFunctions: function (api) {
//Code ommitted
},
firmTypes: function (api) {
//Code ommitted
}
}
});
modalStateProvider.state('people.detail', {
url: "/{personID}",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});
The modalStateProvider looks like:
angular.module('myApp')
.provider('modalState', function ($stateProvider) {
var provider = this;
this.$get = function () {
return provider;
}
this.state = function (stateName, options) {
var modalInstance;
$stateProvider.state(stateName, {
url: options.url,
onEnter: function ($modal, $state) {
modalInstance = $modal.open(options);
modalInstance.result['finally'](function () {
modalInstance = null;
if ($state.$current.name === stateName) {
$state.go('^');
}
});
},
onExit: function () {
if (modalInstance) {
modalInstance.close();
}
}
});
};
})
And finally my function in my controller to transition to the people.detail state:
$scope.transitionToPersonDetail = function (personID) {
$state.transitionTo('.detail', { personID: personID }, { location: true, inherit: true, relative: $state.$current, notify: false });
};

After a lot more inspection I'm still not entirely sure why this was happening, I think it had something to do with the modalStateProvider's scope with $stateParams and the fact that the state wasn't "ready". All of this is purely speculation however.
I fixed it with this code:
$stateProvider.state('people.detail', {
url: '/{personID:int}',
onEnter: ['$stateParams', '$state', '$modal', 'api', function($stateParams, $state, $modal, api) {
$modal.open({
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function(person) {
console.log(person);
},
resolve: {
person: function() {
return api.people.getDetail($stateParams.personID);
}
}
}).result['finally'](function(result) {
$state.transitionTo('people');
});
}]
});

As far as I remember the value from 'resolve' is directly available in controller, although I think it's worth checking if controller for your child view is triggered at all
modalStateProvider.state('people.detail', {
url: "/:personID",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
console.log(person)
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});

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.

Angular UI Router - State params reset for parent when sharing controller in child state

I have ONE controller that is used by TWO child states (detail.Controller).
The parent states (QUIZ1 & QUIZ3) have different controllers that push different data to the parent scopes (quiz1.Controller & quiz3.Controller)
routes.js:
{
name: 'QUIZ1',
abstract: true,
url: '/quiz1',
templateUrl: 'app/html-partials/quiz.html',
controller: require('./app/controllers/quiz1.Controller').inject(angular.module('app', ['ui.router', require('angular-sanitize')]))
},
{
name: 'QUIZ1.detail',
url: '/:page',
templateUrl: 'app/html-partials/quiz.detail.html',
params: {
page: {
value: '0',
squash: true
},
score: {
value: '0',
squash: true
},
timer: {
value: false,
squash: true
}
},
controller: require('./app/controllers/detail.Controller').inject(angular.module('app', ['ui.router', require('angular-sanitize')]))
},
{
name: 'QUIZ2',
// some other state stuff
},
{
name: 'QUIZ3',
abstract: true,
url: '/quiz3',
templateUrl: 'app/html-partials/quiz.html',
controller: require('./app/controllers/quiz3.Controller').inject(angular.module('app', ['ui.router', require('angular-sanitize')]))
},
{
name: 'QUIZ3.detail',
url: '/:page',
templateUrl: 'app/html-partials/quiz.detail.html',
params: {
page: {
value: '0',
squash: true
},
score: {
value: '0',
squash: true
},
timer: {
value: false,
squash: true
}
},
controller: require('./app/controllers/detail.Controller').inject(angular.module('app', ['ui.router', require('angular-sanitize')]))
},
quiz1.Controller.js: (parent controller that builds data)
function buildData(resp) {
$scope.slides = [];
for (var item in resp.quiz_slides) {
if (resp.quiz_slides) {
$scope.slides.push(resp.quiz_slides[item]);
}
}
}
detail.Controller.js: (partial where breakpoint is erroring):
$scope.slide = $scope.slides[$stateParams.page];
THE PROBLEM:
When i navigate out of QUIZ1.detail with a $state.go('QUIZ2') and eventually into a page of QUIZ3.detail I get:
angular.js:14328 TypeError: Cannot read property '0' of undefined
This is relating to the $stateParams.page (used for navigation) I think as it's not correctly resetting because the old data might still be in the $scope.
I KNOW THIS because navigating IMMEDIATELY to QUIZ3 will parse the data and no errors.
Has anyone had any experience with this?
Ok, if any one is wondering how I got around this, after putting it here and there and airing it to colleagues, I thought: "$rootScope for the slides"!
NOTE: naming conventions differ slightly from answer, but you get the idea.
I introduced an init state in between the states so that it would properly parse the json stuff and throw the state into a new child called .detail:
init-state.js:
exports.inject = function (app) {
app.controller('sevenIntAFFECTinit.Controller', exports.controller);
exports.controller.$inject = ['$scope', '$state', '$stateParams', 'jsonPartialService', '$log', '$rootScope'];
return exports.controller;
};
exports.controller = function sevenIntAFFECTinitCtrl($scope, $state, $stateParams, jsonPartialService, $log, $rootScope) {
// Do the json in here. Jokes
$rootScope.slides = [];
jsonPartialService
.getJSON('part-1/7-INT-QUIZ-AFFECT')
.then(function (response) {
$scope.prevState = response.prev_state;
$scope.nextState = response.next_state;
$scope.quizTitle = response.quiz_title;
buildData(response);
});
function buildData(resp) {
for (var item in resp.quiz_slides) {
if (resp.quiz_slides) {
$rootScope.slides.push(resp.quiz_slides[item]);
}
}
$log.debug('after the slides have been pushed in init -->', $rootScope.slides);
$state.go('7-INT-QUIZ-AFFECT.detail', $stateParams, {reload: true, inherit: false});
}
};

Angular Authentication : Avoid multiple resolve for different routes

I'm developing an Angular application. In this, I'm authenticating the user before going to dashboard. To achieve this I have wrote the signIn function as
Sign-In Function
this.signIn = function(credentials) {
console.info('AccountController[signIn] Called');
AuthService
.login(credentials)
.then(function(authenticatedUser) {
$scope.globals['currentUser'] = authenticatedUser;
AuthService.setCurrentUser(authenticatedUser);
$scope.globals['isAuthenticated'] = true;
$location.path('/dashboard');
}).catch(function(error) {
console.warn('AccountController[signIn] :: ', error);
Flash.Error(error);
$scope.credentials.password = '';
});
};
I also want to restrict the user from accessing the routes, if they are not logged in. To achieve that I came up with this dirty code.
Routes
$stateProvider
.state('signIn', {
url: '/signIn',
templateUrl: 'partials/signIn/signIn.html',
data: {
pageTitle: 'SignIn'
},
controller: 'AccountController',
controllerAs: 'ac',
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var userInfo = AuthService.isAuthenticated();
console.info('SignIn Route[isAuthenticated] :: ', userInfo);
if (!userInfo) {
return $q.when(userInfo);
} else {
return $q.reject({
isAuthenticated: true
});
}
}]
}
})
.state('dashboard', {
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
.state('manageStudent', {
url: '/manageStudent',
templateUrl: 'partials/manageStudent.html',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
});
App.run(['$rootScope', 'settings', '$state', 'AuthService', '$location', function($rootScope, settings, $state, AuthService, $location) {
$rootScope.$state = $state; // state to be accessed from view
$rootScope.$settings = settings; // state to be accessed from view
$rootScope.$on('$stateChangeStart', function(event, next,nextParams,prev,prevParams) {
// If the user is logged in don't allow him to land on the Login Page
if (next.access !== undefined) {
if (next.access.requiredLogin && !AuthService.isAuthenticated()) {
$location.path('/signIn');
}
}
});
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
if (!error.isAuthenticated) {
console.warn("I'm not Authenticated.Going to Sign-in");
return $location.path('/signIn');
} else {
console.info("I'm Authenticated");
$location.path('/dashboard');
}
});
}]);
Reason I said the above code DIRTY is because, If I have 10 routes which I want to protect from Unauthenticated user, I have to copy the same resolve function in all the routes.
So my question is , what should I do to get rid of multiple resolve function and being able to write DRY code?
Since auth should be resolved on each route change, it is insufficient to just wrap it into separate factory (which is a singleton and will run only once). To get round this limitation it should be a function
app.factory('authResolver', function ($q, AuthService) {
return function () {
// ...
};
});
which runs on every route resolve
...
resolve: {
auth: function (authResolver) {
return authResolver();
}
}
Still not that DRY, but that's the recommended humidity level.
More radical approach that may save the one from boilerplate resolve and save a few lines of code will be similar to that:
app.run(function ($rootScope, authResolver) {
$rootScope.$on('$stateChangeStart', function (e, to) {
if (to.doAuthPlease)
to.resolve.auth = authResolver();
});
});
and
...
doAuthPlease: true,
resolve: {}
The obvious difference with ngRoute in the mentioned answer is that in UI Router you need to have resolve object defined to be able to add new resolvers to the state dynamically. It can be treated like that or leaved as is.
You're on the right track so far. You have what looks like a custom data member access: { requiredLogin: true} on your state objects.
The next step is to use this with the State Change Events that ui-router provides:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!AuthService.isAuthenticated()) {
event.preventDefault();
// redirect to signIn?
}
}
});
This would be placed in your .run block somewhere which means AuthService needs to be injected there as well. This should remove the need for the resolve block on every route.
Hope that helps.
Update:
if your AuthService.isAuthenticated() function returns a promise, it could be potentially dangerous to rely on the promise to resolve within the event handler (it may move on before the promise resolves). Its probably better that you run the AuthService function before the block (as the application starts) and then store it in a variable:
var isAuth;
AuthService.isAuthenticated().then(function (result) { isAuth = result });
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!isAuth) {
event.preventDefault();
// redirect to signIn?
}
}
});
var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
var unrestricted = ['signIn'];
if (unrestricted.indexOf(name) === -1) {
definition.resolve = angular.extend({}, definition.resolve, {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
});
}
return $delegate.apply(this, arguments);
};
Here I'm dynamically adding the resolve to the routes which I want to restrict.
Because you're using ui.router states (and assuming you're using v0.2.0 or greater), you can use state inheritance to solve this with the resolve and not have to duplicate it all over your various states.
What Do Child States Inherit From Parent States?
Child states DO inherit the following from parent states:
Resolved dependencies via resolve
Custom data properties
Nothing else is inherited (no controllers, templates, url, etc).
Inherited Resolved Dependencies
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.
src - https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states
I accomplish this by using an abstract base state that will defined essentially the same thing you're doing, checking to see if the user is allowed to proceed. Since all of my UI states inherit from the abstract parent state, the authentication dependency is resolved for each of them.
abstract base state
.state('baseState', {
url: '',
abstract: true,
template: '<ui-view></ui-view>'
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
other states
.state('dashboard', {
parent: 'baseState'
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
...
})

$state.go in resolve not working

So I have these routes set up:
.state('createOrder', {
url: '/customer-services/orders/create',
templateUrl: 'tpl/customerServices/orders/save.html',
controller: 'SaveOrderController',
controllerAs: 'controller',
resolve: {
order: ['SaveOrderService', function (shared) {
shared.order = { forDelivery: true };
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order'
}
}).state('createOrder.lines', {
url: '/lines',
views: {
'#': {
templateUrl: 'tpl/customerServices/orders/save/line.html',
controller: 'SaveOrderLinesController',
controllerAs: 'controller'
}
},
resolve: {
validate: ['$state', 'SaveOrderService', function ($state, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
console.log('redirecting');
// Redirect to the create order view
$state.go('createOrder');
}
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order : Lines'
}
})
But the state does not change. I thought that there might be an error somewhere, so I subscribed the the state events like this:
// On state change
$rootScope.$on('$stateChangeStart', function (event, toState) {
var data = toState.data; // Get our state data
var requireLogin = typeof data === 'undefined' ? false : data.requireLogin; // Check to see if we have any data and if so, check to see if we need login rights
var user = service.get(); // Get our current user
console.log(toState);
$rootScope.currentUser = user; // Set our current user on the rootScope
// If we require login rights and we are not authenticated
if (requireLogin && !user.authenticated) {
event.preventDefault(); // Stop processing
$state.transitionTo('login'); // And redirect to the login page
}
});
$rootScope.$on('$stateNotFound', function () {
console.log('state not found');
});
$rootScope.$on('$stateChangeError', function () {
console.log('state errored');
});
$rootScope.$on('$stateChangeSuccess', function () {
console.log('state changed');
});
and when I refresh my lines view, the console outputs this:
Object { url: "/lines", views: Object, resolve: Object, data: Object, name: "createOrder.lines" } app.js:42:9
redirecting app.js:2436:21
Object { url: "/customer-services", templateUrl: "tpl/customerServices/index.html", controller: "CustomerServicesController", controllerAs: "controller", data: Object, name: "customerServices" } app.js:42:9
state changed app.js:63:9
Object { order: Object, account: null, collectionPoint: null }
As you can see, the states think they have changed, but I still see the createOrder.lines view.
Does anyone have any idea why?
I think you'll need to wrap the $state change in a function that will trigger a digest cycle whilst also rejecting the promise in the resolve method...
$timeout(function() { $state.go("createOrder") });
return $q.reject("Rejection message!");
Remember to inject $timeout and $q into your resolve function! =)
Should also add that rejecting the resolve will fire stateChangeError.
So, it turns out you don't need the promise. Just adding the timeout works.
I found another post which suggests that the timeout is needed to avoid digest issues (which I am guessing is what is causing my states to not change).
Here is the final code:
validate: ['$state', '$timeout', 'SaveOrderService', function ($state, $timeout, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
// Timeout to avoid digest issues
$timeout(function () {
// Redirect to the create order view
$state.go('createOrder');
});
}
}]

pass simple parameter to angular ui-bootstrap modal?

I see many posts about posting many complicated things to ui-bootstrap modals, but I just want to pass a simple parameter so that I might use it to display different content in the dialog.
For example, I have the main document which has MyDocumentController as mydoc
In MyDocumentController is a function such as
_this.modals = {
inviteUser: function() {
$modal.open({
animation: true,
templateUrl: 'modules/templates/modals/modal-invite-user.html',
controller: 'UserInviteModalController as invite',
size: 'lg'
});
}, ...
And I call it in the main document like so, here is where I want to pass the param from:
<a href="" ng-click="users.modals.inviteUser(returningUser)">
And here is the controller for the modal:
(function() {
'use strict';
angular.module('mymodule')
.controller('UserInviteModalController', function($log, $modalInstance, toastr) {
var _this = this;
_this.someVar = HERE'S WHERE I WANT TO GET THE VALUE returningUser FROM THE NG-CLICK
_this.inviteDialog = {
close: function() {
$modalInstance.close();
},
resendInvite: function() {
toastr.success('A new invite has been sent.');
$modalInstance.close();
}
};
});
})();
What is the process for passing the simple param to the dialog's controller?
Try using resolve:
_this.modals = {
inviteUser: function(returningUser) {
$modal.open({
animation: true,
templateUrl: 'modules/templates/modals/modal-invite-user.html',
controller: 'UserInviteModalController as invite',
size: 'lg',
resolve: {
returningUser: function() {
return returningUser;
}
}
});
}, ...
And then inject it into your controller:
angular.module('mymodule')
.controller('UserInviteModalController', function($log, $modalInstance, toastr, returningUser) {
var _this = this;
_this.someVar = returningUser;

Categories