It's important to make sure your server-side app is secure when using a client-side application like Ember, since any user can alter your javascript at runtime to make it do whatever you want. Therefore you want to make sure you still use server-side validations to make sure that your persistant records are valid.
However, Ember doesn't include any built-in way of displaying these errors on a form like you might be used to coming from a Rails world. This is how I do it.
Let's take this login form as an example (using twitter bootstrap css):
<form class="form-horizontal" {{action sendRegistration on="submit"}}>
<div class="control-group">
<label class="control-label" for="email">Email</label>
<div class="controls">
{{input type="text" id="email" placeholder="Email" value=email}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">Password</label>
<div class="controls">
{{input type="password" id="password" placeholder="Password" value=password}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="password_confirmation">Password Confirmation</label>
<div class="controls">
{{input type="password" id="password_confirmation" placeholder="Password Confirmation" value=passwordConfirmation}}
</div>
</div>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-inverse">Sign Up</button>
</div>
</div>
</form>
And let's assume we have the following JSON that comes back from the server when a validation error occurs:
{
errors:
{
email: ["can't be blank"],
password: ["doesn't match confirmation"]
}
}
And we need properties for the errors arrays:
App.RegistrationController = Ember.Controller.extend
email: null
password: null
passwordConfirmation: null
emailErrors: []
passwordErrors: []
We need to do three basic things to display the errors:
- Bind the
control-group
divs to a computed property to add theerror
css class to them when errors are present - Add some markup and bind content for displaying the actual errors
- When the post request fails, we need to populate the Errors arrays on the controller
For one, we just need to use a bindAttr
binding:
<div {{bindAttr class=":control-group hasEmailErrors:error"}}>
<label class="control-label" for="email">Email</label>
<div class="controls">
{{input type="text" id="email" placeholder="Email" value=email}}
</div>
</div>
:control-group
in this case is a static class that will always be present. hasEmailErrors:error
binds the error
class to the truty or falsiness of the hasEmailErrors
property.
We can use a Computed Property Macro for the computer property in this case. So in our controller we just add:
hasEmailErrors: Ember.computed.notEmpty('emailErrors')
So now, whenever there is something in our emailErrors
array, our form fields will be automatically styled as having an error.
Now for #2. Add this after the email input field:
<span class="help-inline">{{emailErrorOutput}}</span>
`emailErrorOutput will be another computed property, albeit the classical kind
emailErrorOutput: (->
output = "Email "
for error in @emailErrors
output = "#{output} #{error}"
).property('emailErrors')
Now we just need to set the errors property after our request fails:
sendRegistration: () ->
self = @
self.set('emailErrors', null)
self.set('passwordErrors', null)
$.post('/users',
user:
email: @email,
password: @password,
password_confirmation: @passwordConfirmation)
.done (response) ->
App.Auth.createSession(response)
.fail (response) ->
errors = response.responseJSON.errors
if errors.email
self.set('emailErrors', errors.email)
if errors.password
self.set('passwordErrors', errors.password)
This function will set the errors arrays as necessary, and the bindings will take care of the rest.
The complete template and controller:
App.RegistrationController = Ember.Controller.extend
email: null
password: null
passwordConfirmation: null
emailErrors: []
passwordErrors: []
hasEmailErrors: Ember.computed.notEmpty('emailErrors')
hasPasswordErrors: Ember.computed.notEmpty('passwordErrors')
emailErrorOutput: (->
output = "Email "
for error in @emailErrors
output = "#{output} #{error}"
).property('emailErrors')
passwordErrorOutput: (->
output = "Password "
for error in @passwordErrors
output = "#{output} #{error}"
).property('passwordErrors')
sendRegistration: () ->
self = @
self.set('emailErrors', null)
self.set('passwordErrors', null)
$.post('/users',
user:
email: @email,
password: @password,
password_confirmation: @passwordConfirmation)
.done (response) ->
App.Auth.createSession(response)
.fail (response) ->
errors = response.responseJSON.errors
if errors.email
self.set('emailErrors', errors.email)
if errors.password
self.set('passwordErrors', errors.password)
<div class="row">
<div class="span2"></div>
<div class="span8">
<div class="page-header">
<h1>Sign Up</h1>
</div>
<form class="form-horizontal" {{action sendRegistration on="submit"}}>
<div {{bindAttr class=":control-group hasEmailErrors:error"}}>
<label class="control-label" for="email">Email</label>
<div class="controls">
{{input type="text" id="email" placeholder="Email" value=email}}
<span class="help-inline">{{emailErrorOutput}}</span>
</div>
</div>
<div {{bindAttr class=":control-group hasPasswordErrors:error"}}>
<label class="control-label" for="password">Password</label>
<div class="controls">
{{input type="password" id="password" placeholder="Password" value=password}}
<span class="help-inline">{{passwordErrors}}</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password_confirmation">Password Confirmation</label>
<div class="controls">
{{input type="password" id="password_confirmation" placeholder="Password Confirmation" value=passwordConfirmation}}
<span class="help-inline">{{passwordConfirmationErrors}}</span>
</div>
</div>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-inverse">Sign Up</button>
</div>
</div>
</form>
</div>
<div class="span2"></div>
</div>
Thanks! -DVG