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