更好的模型属性验证

优质
小牛编辑
118浏览
2023-12-01

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) return 'first name is empty';
    if(!attrs.lastname) return 'last name is empty';
    if(!attrs.email) return 'email is empty';

}

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.

Note: A demo of the above solution by @braddunbar is also available.

Unfortunately, this solution forces each form field to be validated every time a blur event is fired. 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 solution is to use @gfranko’s Backbone.validateAll plugin, which was specifically created to provide an option to validate only Model properties that are currently being set/saved.

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';
        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';
        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.

Note: A demo of the above solution by @gfranko is also available.

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});

That’s it!

The Backbone.validateAll logic doesn’t override the default Backbone logic by default and so it’s perfectly capable of being used for scenarios where you might care more about field-validation performance as well as those where you don’t. Both solutions presented in this section should work fine however.