Mike G
Mike G

Reputation: 758

How to report invalid form fields using Backbone.js

I'm using Backbone to manage the state of an HTML form. The Model's role is to handle validation. The View's role is to wrap the HTML form and respond to the change or error events emitted by the model.

Backbone seems to only emit change events when the given field is actually valid. This is causing some really unexpected behavior that makes me thing that I'm doing this wrong.

Here is a summary of what I'm doing: 1. Initial load serializes the form and injects it into the model 2. When an error event is emitted, I generate error nodes next to the invalid field. 3. When a change event is emitted, I remove the error notes next to the (now valid) field.

When a page is rendered with an initially valid form, and a user invalidates a field, the message is displayed as expected; however, the model never updates the field internally. Thus when the user corrects the error, a change event is never emitted.

Example: Initially valid

When a page is rendered with an initially invalid form, things appear to be working fine... but this is only because the model's initial attributes are empty. Correcting the field makes the messages disappear, but if you change it again to an invalid state, the message never disappears.

Example: Initially invalid

What am I doing wrong? Perhaps there's another approach I should be using instead?

My Model

var Foo = Backbone.Model.extend({
    validate: function(attr) {
        var errors = {};

        if (_.isEmpty(attr)) return;

        if (attr.foo && attr.foo != 123) {
            errors.foo = ['foo is not equal to 123'];
        }

        if (attr.bar && attr.bar != 456) {
            errors.bar = ['bar is not equal to 456'];
        }

        return _.isEmpty(errors) ? undefined : errors;
    }
});

My View

FooForm = Backbone.View.extend({
    events: {
        'change :input': 'onFieldChange'
    },

    initialize: function(options) {
        this.model.on('error', this.renderErrors, this);
        this.model.on('change', this.updateFields, this);

        // Debugging only
        this.model.on('all', function() {
            console.info('[Foo all]', arguments, this.toJSON())
        });

        this.model.set(this.serialize());
    },

    onFieldChange: function(event) {
        var field = event.target,
            name = field.name,
            value = field.value;

        this.model.set(name, value);
    },

    renderErrors: function(model, errors) {
        _.each(errors, function(messages, fieldName) {
            var el = $('#' + fieldName),
                alert = $('<div/>').addClass('error');

            el.parent().find('.error').remove();

            _.each(messages, function(message) {
                alert.clone().text(message).insertAfter(el);
            });
        });
    },

    updateFields: function(model, options) {
        if (!options || !options.changes) return;

        _.each(_.keys(options.changes), function(fieldName) {
            var el = $('#' + fieldName);

            el.parent().find('.error').remove();
        });
    },

    serialize: function() {
        var raw = this.$el.find(':input').serializeArray(),
            data = {},
            view = this;

        $.each(raw, function() {
            // Get the model's field name from the form field's name
            var name = this.name;

            if (data[name] !== undefined) {
                if (!data[name].push) {
                    data[name] = [data[name]];
                }

                data[name].push(this.value || '');
            }
            else {
                data[name] = this.value || '';
            }
        });
        return data;

    }
});

Upvotes: 3

Views: 8960

Answers (1)

mvbl fst
mvbl fst

Reputation: 5263

You can't validate individual field using native Backbone validation.

In my app I use this validation plugin: https://github.com/thedersen/backbone.validation

Then in your model you add validation rules per each field (it's optional, so you don't need to add this to all models):

var NewReview = Backbone.Model.extend({
  initialize: function() {
     /* ... */
  },

  validation: {
    summary: {
      required: true,
      minLength: 10
    },
    pros: {
      required: true,
      minLength: 10
    },
    cons: {
      required: true,
      minLength: 10
    },
    overall: function(value) {
      var text = $(value).text().replace(/\s{2,}/g, ' ');
      if (text.length == 0) text = value;
      if (text.length < 20) return "Overall review is too short";
    },
    rating: {
      range: [0.5, 5]
    },
    product_id: {
      required: true
    }
  }
});

Than in views or elsewhere you can validate either entire model or individual fields:

if (this.model.validate()) { ... }

or

if (this.model.isValid("summary")) { ... }

Upvotes: 2

Related Questions