However, Backbone.validation plugin has one disadvantage. It doesn't provide any good way to transfer validation messages across multiple Backbone views nor provide events for single attribute validations. In simple applications this is not a problem, but huge user interface must be divided into multiple views and validation must work automatically for all of those, without generating too much processing overhead.
The solution?
First step was to implement per-attribute. Actually, Backbone.validation provides per-attribute validation callbacks but those are always bound to a view. In order to make our validations more modular, per-attribute validation events must be view-independent. We can create our own events "valid:<attr>" and "invalid:<attr>" by overriding the Backbone.Validation.mixin.validate function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PART 1: tweak backbone validation a little bit | |
# =============================================== | |
# first, allow errorous inputs to update our model | |
Backbone.Validation.configure | |
forceUpdate: true | |
# then disable default callbacks for backbone validation events | |
_.extend Backbone.Validation.callbacks, | |
valid: (view, attr, selector) -> | |
# these are not used | |
invalid: (view, attr, error, selector) -> | |
# these are not used | |
# we are using our custom model events "valid:<attr>" and "invalid:<attr>" to indicate | |
# when model field is validated | |
attrValid = (view, attr, selector) -> | |
# 'view' is not used, views use our implementation ;) | |
@trigger "valid:#{attr}", attr, selector | |
attrInvalid = (view, attr, errorMsg, selector) -> | |
# 'view' is not used, views use our implementation ;) | |
@trigger "invalid:#{attr}", attr, errorMsg, selector | |
origValidate = Backbone.Validation.mixin.validate | |
Backbone.Validation.mixin.validate = (attrs, setOptions) -> | |
options = setOptions || {} | |
# "this" refers now to model so we apply it to our validation | |
# functions, thus can trigger validation events for right model | |
options.valid = => attrValid.apply(@, arguments) | |
options.invalid = => attrInvalid.apply(@, arguments) | |
origValidate.call(@, attrs, options) | |
# add customized validation to our models, when validations occur, they raise | |
# model events valid:<attr> and invalid:<attr> to our models. Views may be interested | |
# in those validation callbacks for showing error messages. | |
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin) |
Ok. Now we have view independent validation events for attributes. Next step is to bind those events into views so that each input listens valid and invalid events from associated models. The problem is that there is no pre-defined information about available events because they are generated dynamically from model constraints. Thus, I've looped every input element from the view after render and used their name attribute for event binding:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PART 2: bind our backbone view to validation events and also for | |
# input changes so that model changes automatically when input changes | |
# ======================================================================= | |
# This defines which element's attribute will be used for linking | |
inputSelector = 'name' | |
# model update function, which is called when input changes | |
updateModel = (model, attr, value) -> | |
model.set(attr, value || null) | |
# This is called when input element is marked as valid | |
# It basically just removes Bootstrap error classes and | |
# custom error messages from input's control-group | |
setValid = ($input, attr, selector) -> | |
$group = $input.closest('.control-group') | |
return if $group.length == 0 | |
$group.find('.help-inline.error').remove() | |
$group.removeClass('error') | |
return | |
# Helper function to check if input has already same an error message | |
containsError = ($err, $group) -> | |
errText = $err.text() | |
for help in $group.find('.help-inline.error') | |
return true if $(help).text() == errText | |
return false | |
# This is called when input is marked as valid | |
setInvalid = ($input, attr, errMsg, selector) -> | |
$group = $input.closest('.control-group') | |
if $group.length == 0 | |
console.log "Invalid input '#{attr}' has no control group. No message will be shown..", $input | |
return | |
$controls = $group.find('.controls') | |
$err = $("<span class='help-inline error'>#{_.escape(errMsg)}</span>") | |
if !containsError($err, $group) | |
if $controls.length > 0 | |
$err.insertAfter($controls).addClass('next-line') | |
else | |
$group.append($err) | |
# finally add error class | |
$group.addClass('error') | |
# This function is called when new stuff is rendered to the document | |
# It searches all inputs and binds update and validation events to | |
# those inputs so that changes/validation results are reflected automatically | |
# between model <-> view <-> template | |
addInputBindings = (view, model) -> | |
return if not model | |
view.$(':input:not(.btn)').each -> | |
$this = $(this) | |
return if not $this.attr(inputSelector) | |
# if we have backbone relational / nested plugin then we may have want | |
# to validate and update also nested attributes | |
attr = $this.attr(inputSelector).replace(/\-/g, '.') | |
# input change to model bindings | |
if !$this.data('ignore-model') | |
# when input changes, then models changes too | |
# we could add also events from model changes to change | |
# input value but we won't do it in this example | |
$this.on 'change', -> updateModel(model, attr, $this.val()) | |
# validation bindings | |
if !$this.data('ignore-validation') | |
# remove previous | |
model.off "valid:#{attr}", null, view | |
model.off "invalid:#{attr}", null, view | |
# add new | |
model.on "valid:#{attr}", ((attr, selector) -> setValid($this, attr, selector)), view | |
model.on "invalid:#{attr}", ((attr, errMsg, selector) -> setInvalid($this, attr, errMsg, selector)), view | |
# This is base class for all our items | |
class window.ItemView extends Backbone.Marionette.ItemView | |
constructor: (options) -> | |
super | |
@on 'render', => addInputBindings(@, @model) |
Because these kind of validation and input-to-model bindings are kind of boilerplate code, I wanted to automate them by using CoffeeScript inheritance and Backbone.marionette plugin: all that developers must do is to inherit their new view classes from ItemView class and after that all validations and input-to-model synchronizations are working automagically! Cool, I think!
And usage...
Here is a little example code about the usage of our brand new "framework":
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jQuery -> | |
# Simple person model with two attributes | |
class Person extends Backbone.Model | |
validation: | |
age: | |
required: true | |
min: 10 | |
name: | |
required: true | |
msg: "All persons have a name..." | |
class PersonView extends ItemView | |
el: $("#person-form") | |
template: Handlebars.compile($("#person-form-template").html()) | |
events: | |
"click #send-btn" : 'send' | |
"click #cancel-btn" : 'cancel' | |
send: -> | |
@model.validate() | |
if @model.isValid() | |
alert "OK!" | |
# do some save stuff here | |
cancel: -> | |
@model.clear() | |
@render() | |
view = new PersonView(model: new Person()) | |
view.render() |
A working example can be found from jsFiddle: http://jsfiddle.net/RfcMK/4/
No comments:
Post a Comment