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.
Related
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.
I would like to display an error message when the server responses with record not found.
The model in the route handler:
model: function(userLoginToken) {
var userLoginToken= this.store.createRecord('userLoginToken');
return userLoginToken;
},
The action:
actions: {
sendOTP: function(userLoginToken) {
var thisObject = this;
var model=this.currentModel;
this.store.findRecord('user-login-token', userLoginToken.get('mobileNumber')).then(function(response) {
//thisObject.get('controller').set('model', response);
},
function(error) {
//thisObject.get('controller').set('model', error);
//alert("model======== "+model.get('errors'));
});
},
The template is not displaying any error message.
The template:
{{#each model.errors.messages as |message|}}
<div class="errors">
{{message}}
</div>
{{/each}}
Unfortunately, the error message doesn't appear.
Ember depends on an DS.error object, in order to get errors from your models the response has to fulfill the requirements. In order to get Ember to recognize an valid error, in Ember 2.x the error code MUST be 422 and has to follow jsonapi http://jsonapi.org/format/#errors-processing
If you want to catch the errors from the backend response you have to use the catch method:
this.store.findRecord('user-login-token', userLoginToken.get('mobileNumber'))
.then(success => {
// Do whatever you need when the response success
})
.catch(failure => {
// Do whatever you need when the response fails
})
},
For catching the errors automatically as you are doing in your template, your backend needs to response in the right way. I would suggest you to read the answer for this SO question.
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?
I use the RESTadpater to persist data. When a validation error occurs, I want to return a 422 response and then log the errors and show an indication next to each incorrect field.
My REST response status code is as follows:
Status Code:422 Unprocessable Entity
My REST response body is as follows:
{
"message": "Validation failed",
"errors": [
{
"name": "duplicate"
}
]
}
In my controller, the becameInvalid fires correctly.
App.AuthorsNewController = Ember.ObjectController.extend({
startEditing: function () {
//Create a new record on a local transaction
this.transaction = this.get('store').transaction();
this.set('model', this.transaction.createRecord(App.Author, {}));
},
save: function (author) {
//Local commit - author record goes in Flight state
author.get('transaction').commit();
//If response is success: didCreate fires
//Transition to edit of the new record
author.one('didCreate', this, function () {
this.transitionToRoute('author.edit', author);
});
//If response is 422 (validation problem at server side): becameError fires
author.one('becameInvalid', this, function () {
console.log "Validation problem"
});
}
...
2 QUESTIONS:
I want to log below the 'console.log "Validation problem"', the complete list of errors returned by the server. How can I do that ?
In my hbs template, I want to indicate an error next to the relevant field. How can I do this ?
I am not sure that the data returned via REST adapter is correct. So problem might be at the REST side or at the Ember side ...
Solution:
In controller save function:
author.one('becameInvalid', this, function () {
console.log "Validation problem"
this.set('errors', this.get('content.errors'));
});
In hbs template:
{{view Ember.TextField valueBinding='name'}}
{{#if errors.name}}{{errors.name}}{{/if}}
Here is how I do it, may not be the best practice but it works for me:
instead of using commit(), I use save(), and if you wonder what's the difference, here is the link. I haven't tried your approach of using transaction, but basically I create the record using record = App.Model.createRecord(...), and here is the code of my apiAddShop function inside the AddShopController:
apiAddShop: function() {
//console.log("add shop");
newShop = App.Shop.createRecord({name:this.get('name'), currentUserRole:"owner"});
//this.get('store').commit(); // Use record.save() instead, then() is not defined for commit()
var self = this;
newShop.save().then(function(response){
self.transitionToRoute('shops');
}, function(response){
// if there is error:
// server should respond with JSON that has a root "errors"
// and with status code: 422
// otherwise the response could not be parsed.
var errors = response.errors;
for(var attr in errors){
if (self.hasOwnProperty(attr)) {
self.set(attr+"Error", true);
self.set(attr+"Message", Ember.String.classify(attr)+" "+errors[attr]);
}
console.log(attr + ': ' + errors[attr]);
}
console.log(response.errors.name[0]);
});
},
the above code assume there is a attrError(boolean) and attrMessage(string) for each of the attributes in your form. Then in your template, you could bind the class of your field to these error attributes, such as <div {{bindAttr class=":control-group nameError:error:"}}>, and the error message could be easily display next to the form field such as: <span {{bindAttr class=":help-inline nameError::hidden"}} id="new_shop_error">{{nameMessage}}</span> Here is my example handlebar gist (have to use gist here, since SO is escaping my html inputs).
What is the best way to handle 403 errors from ember-data?
I want to know the best way to handle non validation errors, for example I have a bit of code that might return an http 403 if the user is not allowed to perform the action. For example, I have the following code:
contact.set 'something', 'something'
transaction = #get('store').transaction()
transaction.add(contact)
contact.one 'becameInvalid', (result) =>
#display some error message
contact.one 'becameError', (result) =>
# code does not get here for some reason
transaction.rollback()
#display some error message
transaction.commit()
If a 403 error happens then the above becameError handler does not get invoked and the object's state machine is left in the rootState.error state and any subsequent attempts to set a property on the object will fail because of the now infamous and dreaded:
uncaught Error: Attempted to handle event `willSetProperty` on <Radium.Contact:ember2286:17> while in state rootState.error. Called with {reference: [object Object], store: <Radium.Store:ember3219>, name: name}
My thinking is that I can override the didError method:
didError: function(store, type, record, xhr) {
if (xhr.status === 422) {
var json = JSON.parse(xhr.responseText),
serializer = get(this, 'serializer'),
errors = serializer.extractValidationErrors(type, json);
store.recordWasInvalid(record, errors);
} else {
this._super.apply(this, arguments);
}
},
My question is how do I get an object back into a usable state after the state machine has transitioned to rootState.error.