Ember CP Validations

Learn about this batteries-included validation library built on computed properties.

Summary

In this video we'll take a look at the Ember CP Validations addon. We'll add a number of validations to a user sign up form.

Transcript

Ember CP Validations is an addon that let's you add validation rules to components, models, controllers, routes, or any other Ember object. It comes with a number of built-in validators, and creating your own custom validations couldn't be easier.

Today I'm going to use the library to improve this user sign-up form. I want the form to ensure that the email address is valid, the form is complete, and the username that was entered isn't already in use by someone else.

Let's start by opening up our user model and adding some validations. When using Ember CP Validations, we build our validations object separately, and then mix it in to whatever Ember object we want to validate. Let's import the buildValidations and validator helper methods from ember cp validations.

// models/user.js

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

const Validations = buildValidations({
  firstname: validator('presence', true),

  email: [
    validator('presence', true),
    validator('format', { type: 'email' })
  ],

  username: validator('presence', true),

  password: [
    validator('presence', true),
    validator('length', {
      min: 4
    })
  ]
});

export default DS.Model.extend(Validations, {
  firstname: DS.attr('string'),
  email: DS.attr('string'),
  username: DS.attr('stirng'),
  password: DS.attr('string')
});

buildValidations takes a JavaScript object: each key is the property we want to validate, and each value is the validator used to validate that key.

We'll start by validating the user's first name. We just want to make sure it exists, so we'll use the presence validator.

Next, we'll validate the user's email. We want to make sure it both exists, and that it is formatted correctly, so we'll use two validators: the presence validator, and the format validator with type 'email'.

We also want to validate that the username field is present.

Finally, we want to make sure the password is both present and at least 4 characters - so we'll use both the presence and length validators.

Now that we have our validations object, we can mix it in to our User model.

All of these validators are included with the library, so we can already see that Ember CP Validations has saved us a lot of work.


Mixing the validations into our User model adds a validate method to it. To use our new validations, we'll open our form component and have the save method call user.validate(). This method is asynchronous, so we'll use a .then to continue when the validations are complete. The promise returns an object with a validations property that we can inspect to see whether or not our user model is valid. If it was, we'll go ahead and persist the changes to the server.

export default Ember.Component.extend({
  user: null,

  classNames: ['Signup-form'],

  actions: {
    save() {
      let user = this.get('user');

      user.validate()
        .then(({ validations }) => {

          if (validations.get('isValid')) {
            user.save();
          }

        });
    }
  }
});

So far so good - our validations are working as expected. However, in the event that the model changes are invalid, we currently just prevent the model from saving. What we really want to do is to show the user where they've made a mistake in their form.

To do this, we'll use the v-get helper. v-get stands for validations-get, and it's an easy way to get the current state of the validations rules for a given object.

Let's start by displaying the validation message for our user's first name. We'll use v-get in our template and pass in the user model, followed by the firstname property, and lastly the string message, which tells v-get we want to see the human-readable message associated with this validator:

{{input
  value=user.firstname
  placeholder="First name"}}

<div class="error">
  {{v-get user 'firstname' 'message'}}
</div>

We can now see the error message - but, the error is always shown, even before the user has begun to fill out the field. Let's wrap the error message in an if statement that only shows after the user has focused out of the first name field.

{{input
  value=user.firstname
  placeholder="First name"
  focus-out=(action (mut showFirstnameError) true)}}

{{#if showFirstnameError}}
  <div class="error">
    {{v-get user 'firstname' 'message'}}
  </div>
{{/if}}


Now, let's update our template to display validations for the rest of the fields. We can see that when we focus out of any of the fields, a validation error is shown.

If we want to add custom HTML to our validation errors, we can use the v-get helper again, but instead of passing in message we'll use isInvalid. Let's update our template to show a custom error message if the email validator is invalid.

{{#if showEmailError}}
  {{#if (v-get user "email" "isInvalid")}}
    <div class="error">
      E-mail address <strong>{{user.email}}</strong> is invalid.
    </div>
  {{/if}}
{{/if}}

Great - all of our validations are working!


Let's wrap up by adding a custom validator to ensure that our model's username hasn't already been taken. Ember CP Validations comes with an Ember CLI generator to help us along. Let's create a new custom validator and call it username-available by running ember generate validator username-available

ember generate validator username-available

The generator gave us an object with a pre-defined validate method on it - this is where we'll write our custom code. To check if a username is available we'll inject Ember Data's store and query our backend to look for a username with the same value. If no records are found, we know the username is safe to use; otherwise, we'll display an error message.

// app/validators/username-available.js

const UsernameAvailable = BaseValidator.extend({
  store: Ember.inject.service(),

  validate(value) {
    return this.get('store').query('user', { filter: { username: value } })
      .then((result) => {
        if (result.get('length') === 0) {
          return true; // safe to use this username
        } else {
          return "The username is already in use";
        }
      });
  }
});

Let's add this validator to our user model and update our template to show the error after a key-up event. This way the user gets live feedback as they're filling out the field.

// app/models/user.js

const Validations = buildValidations({

  // ...

  username: [
    validator('presence', true),
    validator('username-available')
  ],

  // ...

});
{{input
  value=user.username
  placeholder="Username"
  key-up=(action (mut showUsernameError) true)}}

{{#if showUsernameError}}
  <div class="error">
    {{v-get user 'username' 'message'}}
  </div>
{{/if}}

It's working, but if we look at our console we can see that this validator runs every time we type a character in the username field. This ends up creating a lot of useless HTTP requests - we can use the debounce option to only run this validation after the user is done typing. Now, requests are only made once.


In just few minutes we were able to add client-side validations, server-side validations, and warnings to our signup form, making it more user-friendly and easier to fill out.

There's a lot included in this library: over a dozen built-in validators, relationship validators for Ember Data, and more. So the next time you need to add validation rules to part of your Ember application, make sure you give Ember CP Validations a try.

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.