Ember CP Validations is a Ruby on Rails inspired model validation framework that is completely and utterly computed property based. Ember has a ton of great options when it comes to validations. So why did we choose this library?

  1. Ruby on Rails inspired validators.
  2. The use of computed properties instead of observers.
  3. Custom validators.
  4. No need for controllers.

I am going to quickly go over a simple implementation of user creation/update validations as well as address the struggles and solutions found through the process of using this library.

NOTE: All code examples use the --pod structure. Actions are handled within routes.

Step 1: Installation

ember install ember-cp-validations

Step 2: Build Your Validations

// models/user.js

import Ember from 'ember';
import DS from 'ember-data';
import { validator, buildValidations } from 'ember-cp-validations';

const Validations = buildValidations({
  email: {
    validators: [
      validator('presence', true),
      validator('format', { type: email }), 
      validator('length', {
        max: 200,
      })
    ]
  },
  firstName: {
    validators: [
      validator('presence', true),
      validator('length', {
        max: 30
      })
    ]
  },
  lastName: {
    validators: [
      validator('presence', true),
      validator('length', {
        max: 30
      })
    ]
  }
});

export default DS.Model.extend(Validations, {
  'email': DS.attr('string'),
  'firsName': DS.attr('string'),
  'lastName': DS.attr('string'),
});

Let’s begin with the basics! Ember CP Validations Basic Usage Docs explains how to implement validator rules within the model. In our case we add validations for email, firstName, and lastName within the user model. Building these rules will then generate a mixin that we will include in our model. Default messages are included for pre-existing validator classes such as ‘presence’, ‘length’ and ‘format’. Advanced options are easily implemented for such validators if defaults aren’t your thing. Custom validators are also very useful and work well for validating unique records and circumstances. Step two complete.

Step 3: Showing Inline Errors

  • Ember g component validated-input
  • This will generate 2 files, component.js & template.hbs
//components/validated-input/component.js

import Ember from 'ember';

const {
computed,
defineProperty,
} = Ember;

export default Ember.Component.extend({
classNames: ['validated-input'],
classNameBindings: ['showErrorClass:has-error', 'isValid:has-success'],
model: null,
value: null,
type: 'text',
valuePath: '',
placeholder: '',
validation: null,
isTyping: false,

init() {
  this._super(...arguments);
  var valuePath = this.get('valuePath');
  defineProperty(this, 'validation', computed.oneWay(`model.validations.attrs.${valuePath}`));
  defineProperty(this, 'value', computed.alias(`model.${valuePath}`));
},

notValidating: computed.not('validation.isValidating'),
didValidate: computed.oneWay('targetObject.didValidate'),
hasContent: computed.notEmpty('value'),
isValid: computed.and('hasContent', 'validation.isValid', 'notValidating'),
isInvalid: computed.oneWay('validation.isInvalid'),
showErrorClass: computed.and('notValidating', 'showMessage', 'hasContent', 'validation'),
showMessage: computed('validation.isDirty', 'isInvalid', 'didValidate', function() {
  return (this.get('validation.isDirty') || this.get('didValidate')) && this.get('isInvalid');
})
});
<!-- components/validated-input/template.hbs -->

<div class="form-group">
  {{input type=type value=value placeholder=placeholder 
    class="form-control" name=valuePath }}
  {{#if isValid}}
    <span class="valid-input fa fa-check" style="color: green"></span>
  {{/if}}

  <div class="input-error">
      {{#if showMessage}}
        <div class="error">
          <p style="color: red; font-size:12px; margin:0;">{{v-get model 
            valuePath 'message'}}</p>
        </div>
      {{/if}}
  </div>
</div>

Step 4: Utilizing The Component

Okay getting closer! To use our component we simply have to replace our previous input with validate-input in our new template and edit template forms. Like any component, our validate-input.js can be reused for input validations throughout our app. In unison with our component we also utilized the v-get helper which allows access to global model properties such as isValid:

<!-- new/template.hbs -->
<form>
  {{validated-input model=model valuePath='firstName' placeholder='First Name'}}
  {{validated-input model=model valuePath='lastName' placeholder='Last Name'}}
  {{validated-input model=model valuePath='email' placeholder='Email'}}
  <button class="create-user" disabled={{v-get model 'isInvalid'}} {{action "addUser"}}>Create New User</button>
</form>

The Struggle

At this point the component is working for our edit form. However, our user creation form is having no such luck. WHY!? If you examine what is being passed to our component in the above example, our issue lies with model=model. Our edit route includes a model hook to find the user in need of updating:

//edit-user/route.js

  model: function(params) {
    return this.store.findRecord('user', params.user_id); 
  },

Our new route is calling createRecord within our action and never specifying a model. Our validations are within our user model and without a hook in our route, model=undefined. Without a reference to the user model our validations won’t be hit.

//new/route.js

addUser() {
  let firstName  = this.controller.get('model.firstName');
  let lastName   = this.controller.get('model.lastName');
  let email      = this.controller.get('model.email');
  let user       = this.store.createRecord('user', {
                    firstName: firstName,
                    lastName: lastName,
                    email: email,
                    account: this.get('currentUser.account'),
                  });
  user.save().then(() => {
    Ember.Logger.log('save successful');
    this.controller.set('model.firstName',null);
    this.controller.set('model.lastName',null);
    this.controller.set('model.email',null);
    this.flashMessages.success("User creation was successful");
    return this.transitionTo('/admin/manage/users');
  });
},

Solutions

1. Add a createRecord model hook in the new/route.js.

  model() {
    this.store.createRecord('user')
  },

This allows our route access to the model and validations within it! YAY! However it also creates a temporary user object until the index is refreshed. After create we transition to the users index. The solution to this issue is to reload the index from our users/route.js.

  return this.store.query('user', { reload: true });

2. Put Validations into a separate file.

1. Create a validations folder.

2. Create a new validation file within your validations folder and include all validation code previously contained in the user model.

//validations/user-validation.js

import { validator, buildValidations } from 'ember-cp-validations';

const Validations = buildValidations({    
  email: {
    validators: [
      validator('presence', true),
      validator('format', { type: email }),
      validator('length', {
        max: 200,
      })
    ]
  },
  firstName: {
    validators: [
      validator('presence', true),
      validator('length', {
        max: 30
      })
    ]
  },
  lastName: {
    validators: [
      validator('presence', true),
      validator('length', {
        max: 30
      })
    ]
  }
});

export default Validations;

3. Import the new Validation Mixin to the user/new route.js

import Validations from '../validations/user-validation';

export default Ember.Route.extend(Validations, {

4. Create a model hook that returns this in your user/new route.js.

this refers to the new route, where our validation file has been imported and been mixed in.

//new/route.js

model() {
  return this;
},

Viola! We exit the struggle bus!