Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Created August 29, 2012 17:44
Show Gist options
  • Save addyosmani/3516103 to your computer and use it in GitHub Desktop.
Save addyosmani/3516103 to your computer and use it in GitHub Desktop.
Better Model Property Validation On Set/Save

(rough-cut for Backbone Fundamentals)

Better Model Property Validation On Set/Save

As we learned earlier in the book, the validate method on a Model is called before set and save, and is passed the model attributes updated with the values from these methods.

By default, where we define a custom validate method, Backbone passes all of a Model's attributes through this validation each time, regardless of which model attributes are being set.

This means that it can be a challenge to determine which specific fields are being set or validated without being concerned about the others that aren't being set at the same time.

To illustrate this problem better, let us look at a typical registration form use case that:

  • Validates form fields using the blur event
  • Validates each field regardless of whether other model attributes (aka other form data) are valid or not.

Here is one example of a desired use case:

We have a form where a user focuses and blurs first name, last name, and email HTML input boxes without entering any data. A "this field is required" message should be presented next to each form field.

HTML:

<!doctype html>
<html>
<head>
  <meta charset=utf-8>
  <title>Form Validation - Model#validate</title>
  <script src='http://code.jquery.com/jquery.js'></script>
  <script src='http://underscorejs.org/underscore.js'></script>
  <script src='http://backbonejs.org/backbone.js'></script>
</head>
<body>
  <form>
    <label>First Name</label>
    <input name='firstname'>
    <span data-msg='firstname'></span>
    <br>
    <label>Last Name</label>
    <input name='lastname'>
    <span data-msg='lastname'></span>
    <br>
    <label>Email</label>
    <input name='email'>
    <span data-msg='email'></span>
  </form>
</body>
</html>

Some simple validation that could be written using the current Backbone validate method to work with this form could be implemented using something like:

validate: function(attrs) {

    if(!attrs.firstname) {
         console.log('first name is empty');
         return false;
    }

    if(!attrs.lastname) {
        console.log('last name is empty');
        return false;
    }

    if(!attrs.email) {
        console.log('email is empty');
        return false;
    }

}

Unfortunately, this method would trigger a first name error each time any of the fields were blurred and only an error message next to the first name field would be presented.

One potential solution to the problem could be to validate all fields and return all of the errors:

validate: function(attrs) {
  var errors = {};

  if (!attrs.firstname) errors.firstname = 'first name is empty';
  if (!attrs.lastname) errors.lastname = 'last name is empty';
  if (!attrs.email) errors.email = 'email is empty';

  if (!_.isEmpty(errors)) return errors;
}

This can be adapted into a complete solution that defines a Field model for each input in our form and works within the parameters of our use-case as follows:

$(function($) {
  
  var User = Backbone.Model.extend({
    validate: function(attrs) {
      var errors = this.errors = {};
      
      if (!attrs.firstname) errors.firstname = 'firstname is required';
      if (!attrs.lastname) errors.lastname = 'lastname is required';
      if (!attrs.email) errors.email = 'email is required';
      
      if (!_.isEmpty(errors)) return errors;
    }
  });
  
  var Field = Backbone.View.extend({
    events: {blur: 'validate'},
    initialize: function() {
      this.name = this.$el.attr('name');
      this.$msg = $('[data-msg=' + this.name + ']');
    },
    validate: function() {
      this.model.set(this.name, this.$el.val());
      this.$msg.text(this.model.errors[this.name] || '');
    }
  });
  
  var user = new User;
  
  $('input').each(function() {
    new Field({el: this, model: user});
  });
  
});
```

This works great as the solution checks the validation for each attribute individually and sets the message for the correct blurred field (Demo http://jsbin.com/afetez/2/edit by braddunbar)

It unfortunately however forces us to validate all of our form fields every time. If we have multiple client-side validation methods with our particular use case, we may not want to have to call each validation method on every attribute every time, so this solution might not be ideal for everyone.

A potentially better alternative to the above is to use @gfranko's Backbone.validateAll plugin, specifically created to validate specific Model properties (or form fields) without worrying about the validation of any other Model properties (or form fields).

Here is how we would setup a partial User Model and validate method using this plugin, that caters to our use-case:

```  
// Create a new User Model
var User = Backbone.Model.extend({

      // RegEx Patterns
      patterns: {

          specialCharacters: "[^a-zA-Z 0-9]+",

          digits: "[0-9]",

          email: "^[a-zA-Z0-9._-]+@[a-zA-Z0-9][a-zA-Z0-9.-]*[.]{1}[a-zA-Z]{2,6}$"
      },

 	  // Validators
      validators: {

 		  minLength: function(value, minLength) {
            return value.length >= minLength;

          },

          maxLength: function(value, maxLength) {
            return value.length <= maxLength;

          },

      	   isEmail: function(value) {
      	   	return User.prototype.validators.pattern(value, User.prototype.patterns.email);

          },

          hasSpecialCharacter: function(value) {
            return User.prototype.validators.pattern(value, User.prototype.patterns.specialCharacters);

          },
         ...

		// We can determine which properties are getting validated by 
		// checking to see if properties are equal to null
      
        validate: function(attrs) {

          var errors = this.errors = {};

          if(attrs.firstname != null) {
              if (!attrs.firstname) {
                  errors.firstname = 'firstname is required';
                  console.log('first name isEmpty validation called');
              }

              else if(!this.validators.minLength(attrs.firstname, 2)) errors.firstname = 'firstname is too short';
              else if(!this.validators.maxLength(attrs.firstname, 15)) errors.firstname = 'firstname is too large';
              else if(this.validators.hasSpecialCharacter(attrs.firstname)) errors.firstname = 'firstname cannot contain special characters';
          }

          if(attrs.lastname != null) {

              if (!attrs.lastname) {
                  errors.lastname = 'lastname is required';
                  console.log('last name isEmpty validation called');
              }

              else if(!this.validators.minLength(attrs.lastname, 2)) errors.lastname = 'lastname is too short';
              else if(!this.validators.maxLength(attrs.lastname, 15)) errors.lastname = 'lastname is too large';
              else if(this.validators.hasSpecialCharacter(attrs.lastname)) errors.lastname = 'lastname cannot contain special characters';  

          }
```

This allows the logic inside of our validate methods to determine which form fields are currently being set/validated, and does not care about the other model properties that are not trying to be set.

It's fairly straight-forward to use as well. We can simply define a new Model instance and then set the data on our model using the `validateAll` option to use the behavior defined by the plugin:


```
var user = new User();
user.set({ "firstname": "Greg" }, {validateAll: false});

```

Outstanding questions:

* Where should the plugin ideally be used vs. Backbone's native behaviour?
* Should we be saying the plugin is more performant? jsPerf test case?
* Anything else missing?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment