In my application, I've got an ApplicationAdapter whose ajaxError method is customized. Within that method, I'd like to be able to transition to a given route. How can I do this?
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR) {
switch(jqXHR.status) {
// [...]
case 401:
// How can I transitionTo('login') here?
}
// [...]
}
}
});
Instead of transition in the adapter, isn't a good pratice IMHO, you can return an instance of Error and handle it in the error action of the current route:
App.UnauthorizedError // create a custom Error class
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var defaultAjaxError = this._super(jqXHR);
if (jqXHR) {
switch(jqXHR.status) {
case 401:
return new App.UnauthorizedError()
}
}
return defaultAjaxError;
}
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('person');
},
actions: {
error: function(reason) {
// all errors will be propagated to here, we check the instance to handle the UnauthorizedError
if (reason instanceof App.UnauthorizedError) {
this.transitionTo('login')
}
}
}
});
If you want to use this for all routes, you can put the unauthorized transition in the ApplicationRoute. Because the ApplicationRoute is the parent of all routes, and not handled actions, or actions that return true, will bubble to the parent routes.
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(reason) {
if (reason instanceof App.UnauthorizedError) {
this.transitionTo('login')
}
}
}
});
App.BarRoute = Ember.Route.extend({
actions: {
error: function(reason) {
// handle errors of bar route
// bubble to application route
return true;
}
}
});
This is a fiddle with this sample http://jsfiddle.net/SkCH5/
Throw the error and allow the error hook on the route to catch it and transition from there. Additionally you can make a mixin with this logic and add the mixin to all of your routes.
Machty has additional information in his gist talking about the new router: https://gist.github.com/machty/5647589
App.AuthenticatedRoute = Ember.Route.extend({
beforeModel: function(transition) {
if (!authTokenPresent) {
return RSVP.reject();
// Could also just throw an error here too...
// it'll do the same thing as returning a rejecting promise.
// Note that we could put the redirecting `transitionTo`
// in here, but it's a better pattern to put this logic
// into `error` so that errors with resolving the model
// (say, the server tells us the auth token expired)
// can also get handled by the same redirect-to-login logic.
}
},
error: function(reason, transition) {
// This hook will be called for any errors / rejected promises
// from any of the other hooks or provided transitionTo promises.
// Redirect to `login` but save the attempted Transition
var loginController = this.controllerFor('login')
loginController.set('afterLoginTransition', transition);
this.transitionTo('login');
}
});
Related
I upgraded to UI Router 1.0.0, which has changed from .$on($stateChangeX to $transitions.onX( (see $transitions here).
I need to resolve the user's profile and their access BEFORE navigation to the page (e.g. the user should never see the page they're attempting to transition to). Before, I was able to use a resolve directly in the state and pass thru things I need sequentially, as such:
state('my-state', {
...
resolve : {
ProfileLoaded : ['$rootScope', function ($rootScope) {
return $rootScope.loadProfile();
}],
access: ['Access', 'ProfileLoaded', function (Access, ProfileLoaded) {
return Access.hasORRoles(['admin']); //either $q.reject(status code); or "200"
}]
}
})
Then I could easily retrieve the error type in $stateChangeError:
app.run(...
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
if (error === 401) {
$state.go('home');
}
...
With $transitions, I'm trying to do the same thing...
.state('user.my-page', {
...
data: {
loadProfile: true,
roles: [
'admin'
]
}
});
app.run(...
$transitions.onBefore({to: profile}, function(trans) {
return loadProfile().then(function (prof) {
var substate = trans.to();
return Access.hasORRoles(substate.data.roles); //either $q.reject(status code); or "200"
});
});
$transitions.onError({}, function(trans) {
var error = trans && trans._error;
if (error == 401) {
$state.go('home');
}
...
So my question is:
Does onBefore do the same thing as resolve in terms of ensuring the user can't navigate before data is checked? The page is still loading and THEN redirecting with $state.go after the page loads.
Just for people who are still landing here.
You can return a promisse to prevent the site from loading.
$transitions.onBefore({}, function(transition) {
return new Promise((resolve) => {
// Do auth..
return Access.hasORRoles(['admin']);
});
});
I have a standard HTTP interceptor as a factory:
angular
.module('app.services')
.factory('HttpInterceptorService', HttpInterceptorService);
function HttpInterceptorService($injector) {
// Callable functions
var service = {
response: response,
responseError: responseError
};
return service;
// Pass through clean response
function response(data) {
return data;
}
// Handle error response
function responseError(rejection) {
// Handle bypass requests
if (angular.isDefined(rejection.config) && rejection.config.bypassInterceptor) {
return rejection;
}
// Get $state via $injector to avoid a circular dependency
var state = $injector.get('$state');
switch (rejection.status) {
case 404:
return state.go('404');
break;
default:
return state.go('error');
}
}
}
In manual testing, I can see this works correctly by redirecting the user to the relevant 404 or error page if an HTTP call returns an error response. The basic principal of this is documented by Angular here: https://docs.angularjs.org/api/ng/service/$http#interceptors
Now I'm trying to write a unit test with Karma & Jasmine to test that the responseError function works correctly. I've checked out this SO answer to help me. My test looks like this:
describe('HttpInterceptorService', function() {
// Bindable members
var $window,
HttpInterceptorService;
// Load module
beforeEach(module('app.services'));
// Set window value
beforeEach(function () {
$window = { location: { href: null } };
module(function($provide) {
$provide.value('$window', $window);
});
});
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
// Test 404 HTTP response
describe('When HTTP response 404', function () {
beforeEach(function() {
HttpInterceptorService.responseError({ status: 404 });
});
it('Sets window location', function () {
expect($window.location.href).toBe('/404');
});
});
});
My test passes the Exists with required methods check but fails Sets window location with the following error:
Error: [$injector:unpr] Unknown provider: $stateProvider <- $state
The module doesn't seem to have ui.router module loaded, hence $state service is undefined. This is fine, because real router introduces extra moving parts and is highly undesirable in unit tests.
For functional test it is normal to treat a unit as a blackbox, provide initial conditions and test the results, asserting window.location would be appropriate.
For unit test there's no need to treat a unit as a blackbox, $state service may be stubbed:
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
And tested like:
it('...', inject(function (HttpInterceptorService, $state) {
var state404Promise = HttpInterceptorService.responseError({ status: 404 });
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
...
}))
I.e. it may be something like
describe('HttpInterceptorService', function() {
// Bindable members
var HttpInterceptorService;
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
it('...', inject(function($state) {
var state404Promise = HttpInterceptorService.responseError({
status: 404
});
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
}))
});
I have a jquery ajax call defined like this
var fetchMessages = function(){$.getJSON(<some url>).then(function(data){ return data; }};
var messages = fecthMessages();
My routes are setup like this
App.Router.map(function() {
this.resource('messages', function() {
this.resource('message', { path: ':message_id' });
});
});
I use the promise messages in my routes like this
App.MessagesRoute = Ember.Route.extend({
model : function(){
return messages;
}
});
The above route works fine.
Next I have a nested route like shown below. This however errors out when I directly try to visit #/messages/<id of the message>. Loading #/messages followed by visiting #/messages/<id of message> works fine.
App.MessageRoute = Ember.Route.extend({
model: function(params) {
message = messages.findBy("id", params.message_id);
return message;
}
});
So how do I handle the promises in nested routes?
So how do I handle the promises in nested routes?
Apparently Ember handles these for you.
This however errors out when I directly try to visit #/messages/:
App.MessageRoute = Ember.Route.extend({
model: function(params) {
message = messages.findBy("id", params.message_id);
return message;
}
});
messages is still a promise, not an array; it doesn't have a findBy method. Instead, use
return messsages.then(function(m) {
return m.findBy("id", params.message_id);
});
Ref to the question Trying to Migrate to Iron-Router from Router. I still dont understand how to migrate meteor router to iron-router.
I am using router in my meteor project. The router file is like followings:
Meteor.Router.add({
"/settings": function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
return false;
}
return 'site_settings';
},
"/new_page": function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
return false;
}
return 'new_page';
},
"/navigation": function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
return false;
}
return 'navigation';
},
"/login": function() {
return 'loginButtonsFullPage';
},
"/users": function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
return false;
}
return 'admin_users';
}
});
If someone knows how to use an iron-router to replace the return template in the right way. Much appreciate.
I meet a little bit complicated router function, and I have no idea how to solve it. the code is like:
"/": function() {
// Don't render until we have our data
if (!GroundDB.ready()) {
//if (!Offline.subscriptionLoaded('pages') || !Offline.subscriptionLoaded('settings')) {
return 'loadingpage';
} else {
var page_slug = utils.getSetting('indexPage');
var page = Pages.findOne({slug: page_slug});
if(!page) {
page = Pages.findOne();
// if pages dont have any public pages
if (!page) {
var isIndexPageInNav=Navigation.findOne({"location":"header_active","pages.slug":page_slug});
// if index page slug in navigation that means the user dont have right to view this slides or the index page not exist
if(isIndexPageInNav)
return 'loginButtonsFullPage';
else
return '404';
}
else {
page_slug = page.slug;
}
}
Session.set("page-slug", page_slug);
return page.template;
}
}
As you know the iron-router need give a template at the begining. but with router I can return dynamic templates. How does iron-router implement this idea.
Router.map(function() {
//site_settings being the name of the template
this.route('site_settings', {
path: '/settings',
action: function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
//if the conditional fails render a access_denied template
this.render('access_denied');
} else {
//else continue normally rendering, in this case the 'site_settings'
//template
this.render();
}
}
});
this.route('loginButtonsFullPage', {
path: '/login'
});
});
Note since you will be doing that if user is admin conditional a lot you can wrap that logic inside a controller and link it to all the relevant routes such as:
Router.map(function() {
this.route('site_settings', {
path: '/settings',
controller: 'AdminController'
});
this.route('new_page', {
path: '/new_page',
controller: 'AdminController'
});
this.route('navigation', {
path: '/navigation',
controller: 'AdminController'
});
//etc...
//don't need to add the controller for this one
//since all users have access
this.route('loginHuttonsFullPage', {
path: '/login'
});
});
AdminController = RouteController.extend({
action: function() {
if (!Roles.userIsInRole(Meteor.user(), ['admin'])) {
this.render('access_denied');
} else {
this.render();
}
}
});
A couple of other things you will want to check out in iron-router are layouts with {{> yield}} and waitOn which is indispensable.
The docs at https://github.com/EventedMind/iron-router will do a better job of explaining those concepts then I can here.
Here is my attempt at your more complicated route. It may not work right away because I may be misunderstanding what you are doing but the key things are to substitute the returns with this.render(template_name); waitOn instead of checking if something is ready(), adding all the required subscriptions to the waitOn and then finally adding all your logic to an action
//note: index is the name of the route, you do not actually need a template called index.
//in the previous examples where no template to render was returned then iron-router will
//look for a template with the same name as the route but in this route we will be providing
//it with a specific route name in all cases
this.route('index', {
path: '/',
//assuming GroundDB is a subscription e.g. GroundDB = Meteor.subscribe('groundDB');
//I don't know what your page and nav subscriptions are called but you should wait on them too.
//if you haven't assigned them to a variable do something like
//pageSubscription = Meteor.subscribe('pages');
waitOn: [GroundDB, pageSubscription, navigationSub],
//the template to load while the subscriptions in waitOn aren't ready.
//note: this can be defined globally if your loading template will be the same
//for all pages
loadingTemplate: 'loadingpage',
//here we evaluate the logic on which page to load assuming everything has loaded
action: function() {
var page_slug = utils.getSetting('indexPage');
var page = Pages.findOne({slug: page_slug});
if (!page) {
var isIndexPageInNav = Navigation.findOne({"location":"header_active","pages.slug":page_slug});
if(isIndexPageInNav)
this.render('loginButtonsFullPage');
else
this.render('404');
} else {
page_slug = page.slug;
}
Session.set("page-slug", page_slug);
this.render(page.template);
}
});
How do you set up Airbrake such that it gets context information from unhandled Javascript errors that occur in an Ember application?
Assuming you've included Airbrake-js you can hook on Ember's onerror handler and push errors.
Ember.onerror = function(err) { // any ember error
Airbrake.push(err);
//any other error handling
};
Ember.RSVP.configure('onerror',function(err){ // any promise error
Airbrake.push(err);
console.error(e.message);
console.error(e.stack);
//any other generic promise error handling
};
window.onerror = function(err){ // window general errors.
Airbrake.push(err);
//generic error handling that might not be Airbrake related.
};
You can see more options of different parameters of the data sent in the airbrake-js GitHub repository docs.
I don't know if this answers your question, but I hope it helps.
To handle server thrown errors you can define an "error" function in the application's route and push it in Airbrake:
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(error) {
// handle the error
Airbreak.push(error)
}
}
});
Moreover, if you catch errors somewhere else and have the same handling, you can make a mixin and pass the error:
App.ErrorHandlerMixin = Ember.Mixin.create({
handleError: function(error){
//make different stuff
Airbreak.push(error)
}
});
App.ApplicationRoute = Ember.Route.extend(App.ErrorHandlerMixin, {
actions: {
error: function(error, transition) {
this.handleError(error);
}
}
});
App.ApplicationController = Ember.ObjectController.extend((App.ErrorHandlerMixin, {
someFunction: function () {
this.handleError(randomError);
}
});
This way you have all the error handling in a single place.