DRYing up validations in EmberJS

Ember Validations, written by the totally awesome Brian Cardarella, is a pretty cool piece of code that adds Rails style validations to EmberJS.

App.CoolModel = DS.Model.extend(Ember.Validations.Mixin,  
  email:      DS.attr 'string'

      presence: true
      format: { with: /^([\w.-\+]+)@([\w.-]+)\.([a-zA-Z.]{2,6})$/i, allowBlank: false, message: 'Invalid email address' }

I decided on skipping Ember EasyForms which hooks with Ember Validations pretty well and ended up writing a component for reusability that aids me in form validations. It is imporant to note that validations kick in when the user either focuses out of an input field or a form submit has been issued.

Component in Action

Before looking at the actual code of the component, lets have a look of how the component is used in templates.

{{#form-input-error-handler name="Email" formController=controller}}
  {{view Ember.TextField valueBinding='email' class='form-control' name='email' placeholder='Enter email'}}

The current controller as well as the name of the input field are passed to the component as parameters, where as the actual input field is passed as a block.

Note to self: Perhaps there is a way in which the controller doesn't need to be passed to the component? I'll need to look up on that.

Component explained

Apart from one cool thing, implementing the component was pretty easy.

Portal.FormInputErrorHandlerComponent = Ember.Component.extend  
  hasError: false

  focusOut: (event) ->
    @set 'hasError', !Ember.isEmpty(@get('error'))

  displayError: ( ->
    @get('hasError') || @get('formController.showValidationFields')
  ).property('hasError', 'formController.showValidationFields')

  instantiate: ( ->
    @set 'inputId', this.$('input').attr('id')

    Ember.defineProperty this, 'error', Ember.computed( ->

A brief run down of the code: The component has a property hasError defaulted to false. Once a user focuses out from the component, hasError will be assigned true if the validations for that particular field fail.

Now time for the cool part. The instantiate method is called whenever the component is inserted into the DOM(consider it as a constructor). It carries out two operations:

  • It stores the DOM id of the input as a property. My usecase involved the use of this id in the label as for the "for" tag. This statement can be skipped.
  • It defines a computed property, error on the particular attribute(stored in name, which was received as a paramenter to the component) of the errors object in the controller. The reason why we're doing it in such a fashion is because the dependency of the computed property is inferred in a dynamic fashion("formController.errors.#{@get('name').toLowerCase()}")

Finally displayError is a computed property that is set to true if either an error kicks in when the user focuses out from the component or the form submit action has been invoked and the controller identifies an error and sets the showValidationFields property to true.

The template for this particular component is pretty simple and would most probably change as per requirements.

<div class="form-group" {{bind-attr class="displayError:has-error"}}>  
  <label {{bind-attr for=inputId}}>{{name}}</label>
  {{#if displayError}}
  <b class="text-danger">{{error}}</b>

Finally lets have a look at the controller.

App.RandomController = Ember.ObjectController.extend  

    save: ->

      if @get('model').get('isValid')
        # Save the record
        @set('showValidationFields', true)

Nothing special here. If the model has no errors, go ahead and save it or else assign showValidationFields a value of true.

I'm still not sure whether I should create a mixin solely for declaring the showValidationFields attribute and include it in the various controllers utilizing validations. Need to look into this further.

Final thoughts

Before implementing forms and validations, Ember EasyForms certainly deserves to be looked at. For people coming from Rails, it is the Ember equivalent of SimpleForm.

comments powered by Disqus