Server Side Validations with Ember Data and DS.Errors
Validation errors are an unfortunate fact of life that you will have to deal with in almost every non trivial application. Often you will want to do client-side validations, and for that I recommend having a look at Ember Validations.
However, client side validations can be complex to implement, and you will always need to do server side validations anyway, both because you can't trust anything done on the client side, and some validations like uniqueness can't be implemented correctly without the transactional semantics only available on the server side.
It's a lot easier to start out doing server-side only validations, and this post shows how to take advantage of Ember's built in error handling.
Shameless plug: This post explains some techniques that are used in Emberkit, my Ember/Rails SaaS kit. If you find this post useful, have a look, as you'll find many more useful techniques demonstrated there:
Starting out with a form
I'm going to start out with a basic form, using the form components I build in my previous post - if there's anything in this post that's not making sense you might want to read that one first.
A you can see, nothing works yet. If you click on the Save button, you will get an error in the console:
Error: The backend rejected the commit because it was invalid: {name: must be present, email: Is not an email,Is blank, base: This person is a generally unsavoury character and is not allowed to sign up}
I'm going to be using mockjax to emulate the response needed from your server, which should have a response code of 422 Unprocessable Entity
, and a body that looks like this:
{
"errors": {
"field_one": ["error 1", "error 2"],
"field_two": ["error 1"]
}
}
Intro to DS.Errors
So how do we get ember to handle the errors more gracefully? It turns out that all we need to do is to handle the save()
promise rejection, by providing a function as the second argument to the then
handler:
model.save().then ->
alert 'saved'
, ->
#empty function
Just by providing that empty function, the error is considered handled and no console error is thrown. Your model also gets it's errors
property populated with an instance of DS.Errors
, which you can find out more about in the API docs.
For now, we're just going to use the length
property to see if there are any validation errors and display a message to the user:
Using form components to display errors
So now we have an error message, but it's quite useless because we're not telling the user what errors actually occurred. To improve this a bit, I'm going to extend the form components from my previous post to show the errors on each field, along with in the alert above the form:
The key parts here are:
- Extend App.FormFieldComponent to know what errors are on each field so inline messages can be shown:
hasError: (->
@get('object.errors')?.has @get('for')
).property 'object.errors.[]'
errors: (->
return Em.A() unless @get('object.errors')
@get('object.errors').errorsFor(@get('for')).mapBy('message').join(', ')
).property 'object.errors.[]'
- Create a new
error-messages
component to display the error messages nicely - Add a
titleize
helper to convert"form_field"
to"Form field"
, so that you can display the error messages in human-readable form. - Use the new component in the template to display the errors.
Handling arbitrary errors
If you've been paying very close attention, you may have noticed that we have a problem - not all error messages we returned are displayed!
The reason for this is that DS.Errors is currently only populated for attributes that you have defined on your model with DS.attr
(or DS.belongsTo
/ DS.hasMany
). But we have returned an error for base
in our (fake) json response. Currently it is just ignored. It's quite easy to hack around this, by changing how errors are applied to your models:
DS.Model.reopen
adapterDidInvalidate: (errors) ->
recordErrors = @get 'errors'
for own key, errorValue of errors
recordErrors.add key, errorValue
In this example, you can return arbitrary errors and they will be displayed in the alert above the form. I also add special handling for an error called base
, allowing you to have errors that don't apply to a specific field but the whole record in general:
I currently have a pull request open in ember data to make this the default behaviour - hopefully it will be merged and you won't have to worry about this in the future.
Handling Errors with the RESTAdapter
All the previous examples use DS.ActiveModelAdapter
. However many ember applications use DS.RESTAdapter
, which doesn't include this error handling by default. If you are using the RESTAdapter
, you can add support for automatic error handling like this:
App.ApplicationAdapter = DS.RESTAdapter.extend
ajaxError: (jqXHR) ->
error = @_super(jqXHR)
if jqXHR and jqXHR.status is 422
response = Ember.$.parseJSON(jqXHR.responseText)
errors = {}
if response.errors?
jsonErrors = response.errors
Ember.keys(jsonErrors).forEach (key) ->
errors[Ember.String.camelize(key)] = jsonErrors[key]
new DS.InvalidError(errors)
else
error
Bonus: Generating the correct responses in Rails
The format of the json required is quite simple, you shouldn't have any problems generating it in any backend language. However, if you happen to be using Rails on the backend, it's really easy to generate this automatically. If you already have validations on your model, you can just render the errors object to get the exact format you need:
def create
user = User.new sanitized_user_params
if user.save
render json: user
else
render json: {errors: user.errors}, status: 422
end
end
In fact, it can be even easier than this, because the respond_with helper will do just that automatically:
def create
user = User.create sanitized_user_params
respond_with user
end
Now you have implemented server-side validations, have a look at Ember Validations to see how you can add client side validations that can validate your form on the client side in real-time, for a better UX for your visitors.