Ember CLI + Ember Data: How to Handle Error Messages - ember.js

I am trying to provide useful feedback for the user based on the response from the server when errors occur. By default, Ember CLI provides a template for error.hbs, which sets its model to the promise returned from the Ember Data GET call. I have configured error conditions on my template and would like to set those conditions in a controller:
{{#if pageNotFound}}
<p>Sorry. The page you requested does not exist.</p>
{{elseif internalServerError}}
<p>An internal server error occured. Please try again.</p>
{{elseif notAuthorized}}
<p>It looks like you are not authorized to view this page. Have you registered and logged in?</p>
{{/if}}
I have tried setting the values via my ApplicationRoute with no success, as suggested here:
ApplicationRoute = Ember.Route.extend(
actions:
error: (error, transition) ->
errorController = #controllerFor "error"
switch error.status
when 404 then errorController.set("pageNotFound", true)
when 500 then errorController.set("internalServerError", true)
when 403 then errorController.set("notAuthorized", true)
else errorController.set("generic error", true)
#transitionTo "error"
)
But I see no transition, though the actions function triggers when I put a console.log in.
How can I display conditional error messages based on the promise status value on a generic ApplicationError route and template?

Related

Ember & Cypress | Integration test failing likely due to lack of store context

I am working with Ember.js on the frontend and using Cypress.io for integration testing. I have a "Order Status" button that upon click is supposed to use data within the store to access the specific order and navigate to the Order Status page.
However within the test, upon click from the modal, the Order Status page cannot load and my test throws this error:
TypeError: Cannot read property ‘_internalModel’ of null
...in the console, which I think likely has to do with that route's model not finding the order from within the store and loading.
In the Cypress test, I've created the necessary routes to pass in session data, but am struggling to understand if the lack of store data is throwing this error, and how to fix it. It is important to note that this whole flow works 100% on its own - I'm just trying to get test coverage.
I'll attach snippets from the relevant files / functions below -- please do let me know if more information is needed. I really suspect that Cypress cannot access the Ember store, but after much research and experimenting, I'm not exactly sure what the issue is.
order-status.js - model function from route
async model({ order_id: orderId }) {
let cachedOrder = this.get('store').peekRecord('order', orderId);
return cachedOrder
? cachedOrder
: await this.get('store').findRecord('order', orderId);
},
modal.hbs - where we navigate from current page to order-status route
<fieldset>
<span class="modal-text">See order status</span>
<div class="inline-button-wrap">
<button
{{action 'decline'}}
class="close-btn button-hollow-green">
Close
</button>
<button
{{action 'accept'}}
class="order-status-btn">
Order Status
</button>
</div>
</fieldset>
test.js - test that simulates clicking order status button above
it('order status btn navigates to order status page', () => {
cy.server();
cy.route('GET', '/api/session', sessionResponse);
cy.route('GET', `/api/orders/*`, order);
cy.visit('/');
cy.get('.delivery-card .button-cta').click(); //opens modal
cy.get('#modal .order-status-btn').click(); //navigates to order-status
});
Thanks so much for your help.
Whether stubbing API requests or not, Ember's store should behave as expected when used in conjunction with Cypress. The error mentioned is a good clue to the primary problem – an undefined model. It's not fully clear how your application is working while the Cypress test is failing and throwing an error, but a couple of changes (some in Ember, some in Cypress) should clear things up. Here's a breakdown for resolving this issue (some reverse engineering / assumptions made, from the code examples provided)...
Send along the order id via the button action that opens the modal (in the delivery-card HTML mark-up):
<button class="button-cta" {{action 'openModal' order.id}}>
Open Order Status Modal
</button>
The openModal action passes along the order id as a param for the model on the subsequent modal route, when transitioning. In the delivery-card.js controller:
actions: {
openModal(id) {
this.transitionToRoute('modal', id); // <-- id
}
}
Then, the modal's accept action passes along the id to the order status page as well, when transitioning. In the modal.js controller:
actions: {
accept() {
this.transitionToRoute('order-status', this.get('model.id')); // <-- here
},
decline() {
...
Further, in order-status.js, the argument passed to the model hook can be simplified as params:
async model(params) { // <-- just use params
const id = params.id; // <-- id
const cachedOrder = this.get('store').peekRecord('order', id); // <-- id
return cachedOrder
? cachedOrder
: await this.get('store').findRecord('order', id); // <-- id
}
Finally, some changes to test.js:
it('order status btn navigates to order status page', () => {
cy.server();
cy.route('GET', `/api/session`, 'fixture:session.json'); // <-- here
cy.route('GET', `/api/orders`, 'fixture:orders.json'); // <-- here
cy.visit('/');
cy.get('.delivery-card .button-cta').click(); // opens modal
cy.get('#modal .order-status-btn').click(); // navigates to order-status
// here, verify that an order status is displayed
cy.get('.current-status')
.should(($el) => {
const statusTextValue = $el[0].innerText;
expect(statusTextValue).to.not.be.empty;
});
});
The API GET requests are stubbed with fixtures using standard Cypress conventions.
Finally, here's a screenshot of the Cypress runner, showing only the initial XHR STUB request when GET-ting orders. Notice that Ember's store methods, peekRecord and findRecord, used in the order-status.js route file, do not generate any further XHR requests, and, result in no errors being thrown. Ember's store is functioning as anticipated.

Ember.js showing ember-data validation errors on page

I'm having issues getting the default JSONAPIAdapter to propagate model validation errors to the login page. I've validated that the API server is sending the response correctly by catching the error and using console.log to validate the adapter and model are processing the data returned correctly, but for some reason, i am unable to get it to show up on the page itself:
JSON response from API server:
{ errors: [
{
detail: 'Display name already exists',
source: {
pointer : 'data/attributes/display-name'
}
},
{
detail: 'Email already exists',
source: {
pointer : 'data/attributes/email'
}
}
]}
This also returns a status of 422 which shows in the console log:
Failed to load resource: the server responded with a status of 422 (Unprocessable Entity)
If i do not .catch() the error, it console.logs the following, which from what i have read, and seen in the code is standard:
ember.debug.js:19746 Error: The adapter rejected the commit because it was invalid
at ErrorClass.EmberError (ember.debug.js:19681)
at ErrorClass.AdapterError (errors.js:23)
at ErrorClass (errors.js:49)
at Class.handleResponse (rest.js:820)
at Class.handleResponse (data-adapter-mixin.js:100)
at Class.superWrapper [as handleResponse] (ember.debug.js:25367)
at ajaxError (rest.js:1341)
at Class.hash.error (rest.js:915)
at fire (jquery.js:3187)
at Object.fireWith [as rejectWith] (jquery.js:3317)
The controller code is the following:
user.save().then(() => {
this.set('valid', 'Success! Log in!');
}).catch(() => {
console.log(user.get('errors.email')[0].message);
console.log(user.get('errors.displayName')[0].message);
});
The above catches and console.logs the error messages correctly, which means the adapter is successfully catching the error and serializing it for use in model. The problem comes in with the template:
<div class="col-sm-6">
<h2>ERRORS</h2>
{{#each user.errors.displayName as |error|}}
<h4>{{error.message}}</h4>
{{/each}}
{{#each user.errors.email as |error|}}
<h4>{{error.message}}</h4>
{{/each}}
<h2>END ERRORS</h2>
</div>
According to DS.ERRORS Class this should be the appropriate way to set this up, but I am unable to get the errors to show properly on my front end between the ERRORS and END ERRORS headers.

How to client side authentication with Emberjs

First of all I don't use Ruby nor Devise :) (all my searches led me to plugins that kind of rely on Devise)
I want to do pretty simple authentication with Ember, I have a REST backend that blocks requests without a proper cookie(user-pass) and i want Ember to watch when it gets 403 forbidden (won't let you to transition into protected URLs) and then pop up a user-login dialog.
So when a user tries to send a new message for example(lets say i've built a forum) Ember will fire the request and if it gets 403 it will block the transition and popup a login form and will retry the transition after the login have completed
Also is there a way to get the errors from ember-data and respond to them? (if a user tries to change an attribute he can't access i would like to inform him about it[Access denied or something like that])
I want to use custom errors that my server will send to ember data not just error numbers but words like "Sorry you can't change this before 12 PM"
You can simply listen to the response of your server and transition to your LOGIN (or whatever you call it) route. In my apps I happen to keep two types of routes (LOGIN and AUTHENTICATED). When they access the authenticated routes without logging in, they get a 401 unauthorized error and get transitioned to the LOGIN route.
// AuthenticatedRoute.js
redirectToLogin: function(transition) {
// alert('You must log in!');
var loginController = this.controllerFor('login');
loginController.set('attemptedTransition', transition);
this.transitionTo('login');
},
events: {
error: function(reason, transition) {
if (reason.status === 401) {
this.redirectToLogin(transition);
} else {
console.log(reason);
window.alert('Something went wrong');
}
}
},
model: function () {
return this.store.find('post');
},
So now when the user requests for post he gets a 401 and gets transitioned to LOGIN controller.
// LoginController.js
login: function() {
var self = this, data = this.getProperties('username', 'password');
// Clear out any error messages.
this.set('errorMessage', null);
$.post('/login', data).then(function(response) {
self.set('errorMessage', response.message);
if (response.success) {
alert('Login succeeded!');
// Redirecting to the actual route the user tried to access
var attemptedTransition = self.get('attemptedTransition');
if (attemptedTransition) {
attemptedTransition.retry();
self.set('attemptedTransition', null);
} else {
// Redirect to 'defaultRoute' by default.
self.transitionToRoute('defaultRoute');
}
}
});
}
The basic answer you need is capturing the events in the route and transitioning accordingly. I just happened to include the code for attempted transition as it comes in handy at times.

TypeError: jsonErrors is not an object

I recently upgraded to a canary build of Ember.js 1.0. After the upgrade, Ember can no longer parse validation errors from the server. I handle a form submit action like this:
submit: (event, view) ->
#get('model').save().then ((response) =>
#transitionToRoute('organization.timeline', #content)
), (response) =>
#set "errors", response.responseText
Before updating to the canary build, this worked as expected. Now, I get this error when validation fails:
TypeError: jsonErrors is not an object
This happens during the call to save, so that even if I reduce the code to:
submit: (event, view) ->
#get('model').save()
I still get the error.
I heard in a talk by Tom Dale that Ember is rolling out a new system for handling validation errors, and I'm assuming that's what's causing the conflict. I can't find any documentation (even a pull request) for this new approach. If anyone can point me in the right direction, it'd be greatly appreciated.
Looks like I needed to change the way I was sending the error. I'm using a Rail backend. The response code looked like this:
def create
organization = Organization::Master.find(params[:id])
if organization.update_attributes(organization_params)
render json: organization, status: 201
else
render json: organization.errors, status: 422
end
end
Ember now supports a simpler approach:
def create
organization = Organization::Master.find(params[:id])
organization.update_attributes(organization_params)
respond_with organization
end
With that, Ember is able to parse the errors.

emberjs handle 401 not authorized

I am building an ember.js application and am hung up on authentication. The json rest backend is rails. Every request is authenticated using a session cookie (warden).
When a user first navigates to the application root rails redirects to a login page. Once the session is authorized the ember.js app is loaded. Once loaded the ember.js app makes requests to the backend using ember-data RESTadapter and the session for authorization.
The problem is the session will expire after a predetermined amount of time. Many times when this happens the ember.js app is still loaded. So all requests to the backend return a 401 {not autorized} response.
To fix this problem I am thinking the ember.js app needs to notify the user with a login modal every time a 401 {not autorized} response is returned from the server.
Does anyone know how to listen for a 401 {not autorized} response and allow the user to re-login without losing any changes or state.
I have seen other approaches such as token authorization but I am concerned with the security implications.
Anybody have a working solution to this problem?
As of the current version of Ember Data (1.0 beta) you can override the ajaxError method of DS.RESTAdapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR && jqXHR.status === 401) {
#handle the 401 error
}
return error;
}
});
Note that you should call #_super, especially if you are overriding one of the more complex adapters like DS.ActiveModelAdapter, which handles 422 Unprocessable Entity.
AFAIK this is not addressed by the current implementation of ember-data and the ember-data README states that "Handle error states" is on the Roadmap.
For the time being, you can implement your own error handling adapter. Take a look at the implementation of the DS.RestAdapter . By using that as a starter, it should not be too difficult to add error handling in there (e.g simply add an error function to the the data hash that is passed to the jQuery.ajax call).
For those willing to accept a solution that does lose changes and state you can register a jQuery ajaxError handler to redirect to a login page.
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) {
// You should include additional conditions to the if statement so that this
// only triggers when you're absolutely certain it should
if (jqXHR.status === 401) {
document.location.href = '/users/sign_in';
}
});
This code will get triggered anytime any jQuery ajax request completes with an error.
Of course you would never actually use such a solution as it creates an incredibly poor user experience. The user is yanked away from what they're doing and they lose all state. What you'd really do is render a LoginView, probably inside of a modal.
An additional nicety of this solution is that it works even if you occasionally make requests to your server outside of ember-data. The danger is if jQuery is being used to load data from other sources or if you already have some 401 error handling built-in elsewhere. You'll want to add appropriate conditions to the if statement above to ensure things are triggered only when you're absolutely certain they should.
It's not addressed by ember-data (and probably won't be), but you can reopen the DS class and extend the ajax method.
It looks like this:
ajax: function(url, type, hash) {
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.contentType = 'application/json; charset=utf-8';
hash.context = this;
if (hash.data && type !== 'GET') {
hash.data = JSON.stringify(hash.data);
}
jQuery.ajax(hash);
},
You can rewrite it with something like this (disclaimer: untested, probably won't work):
DS.reopen({
ajax: function(url, type, hash) {
var originalError = hash.error;
hash.error = function(xhr) {
if (xhr.status == 401) {
var payload = JSON.parse(xhr.responseText);
//Check for your API's errorCode, if applicable, or just remove this conditional entirely
if (payload.errorCode === 'USER_LOGIN_REQUIRED') {
//Show your login modal here
App.YourModal.create({
//Your modal's callback will process the original call
callback: function() {
hash.error = originalError;
DS.ajax(url, type, hash);
}
}).show();
return;
}
}
originalError.call(hash.context, xhr);
};
//Let ember-data's ajax method handle the call
this._super(url, type, hash);
}
});
What we're doing here is essentially deferring the call that received the 401 and are preserving the request to be called again when login is complete. The modal's ajax call with have the original error applied to it from the original ajax call's hash, so the original error would still work as long as it's defined :-)
This is a modified implementation of something we're using with our own data-persistence library, so your implementation might vary a bit, but the same concept should work for ember-data.