(function(_, Backbone) { var bindingSplitter = /^(\S+)\s*(.*)$/; _.extend(Backbone.View.prototype, { bindModel: function(bindings) { // Bindings can be defined three different ways. It can be // defined on the view as an object or function under the key // 'bindings', or as an object passed to bindModel. bindings = bindings || _.result(this, 'bindings'); // Skip if no bindings can be found or if the view has no model. if (!bindings || !this.model) return; // Create the private bindings map if it doesn't exist. this._bindings = this._bindings || {}; // Clear any previous bindings for view. this.unbindModel(); _.each(bindings, function(attribute, binding) { if (!_.isArray(attribute)) attribute = [attribute, [null, null]]; if (!_.isArray(attribute[1])) attribute[1] = [attribute[1], null]; // Check to see if a binding is already bound to another attribute. if (this._bindings[binding]) throw new Error("'" + binding + "' is already bound to '" + attribute[0] + "'."); // Split bindings just like Backbone.View.events where the first half // is the property you want to bind to and the remainder is the selector // for the element in the view that property is for. var match = binding.match(bindingSplitter), property = match[1], selector = match[2], // Find element in view for binding. If there is no selector // use the view's el. el = (selector) ? this.$(selector) : this.$el, // Finder binder definition for binding by property. If it can't be found // default to property 'attr'. binder = Backbone.View.Binders[property] || Backbone.View.Binders['__attr__'], // Fetch accessors from binder. The context of the binder is the view // and binder should return an object that has 'set' and or 'get' keys. // 'set' must be a function and has one argument. `get` can either be // a function or a list [events, function] .The context of both set and // get is the views's $el. accessors = binder.call(this, this.model, attribute[0], property); if (!accessors) return; // Normalize get accessors if only a function was provided. If no // events were provided default to on 'change'. if (!_.isArray(accessors.get)) accessors.get = ['change', accessors.get]; if (!accessors.get[1] && !accessors.set) return; // Event key for model attribute changes. var setTrigger = 'change:' + attribute[0], // Event keys for view.$el namespaced to the view for unbinding. getTrigger = _.reduce(accessors.get[0].split(' '), function(memo, event) { return memo + ' ' + event + '.modelBinding' + this.cid; }, '', this); // Default to identity transformer if not provided for attribute. var setTransformer = attribute[1][0] || _.identity, getTransformer = attribute[1][1] || _.identity; // Create get and set callbacks so that we can reference the functions // when it's time to unbind. 'set' for binding to the model events... var set = _.bind(function(model, value, options) { // Skip if this callback was bound to the element that // triggered the callback. if (options && options.el && options.el.get(0) == el.get(0)) return; // Set the property value for the binder's element. accessors.set.call(el, setTransformer.call(this, value)); }, this); // ...and 'get' callback for binding to DOM events. var get = _.bind(function(event) { // Get the property value from the binder's element. // console.log(attribute[0], getTransformer); var value = getTransformer.call(this, accessors.get[1].call(el)); this.model.set(attribute[0], value, { el: this.$(event.srcElement) }); }, this); if (accessors.set) { this.model.on(setTrigger, set); // Trigger the initial set callback manually so that the view is up // to date with the model bound to it. set(this.model, this.model.get(attribute[0])); } if (accessors.get[1]) this.$el.on(getTrigger, selector, get); // Save a reference to binding so that we can unbind it later. this._bindings[binding] = { selector: selector, getTrigger: getTrigger, setTrigger: setTrigger, get: get, set: set }; }, this); return this; }, unbindModel: function() { // Skip if view has been bound or doesn't have a model. if (!this._bindings || !this.model) return; _.each(this._bindings, function(binding, key) { if (binding.get[1]) this.$el.off(binding.getTrigger, binding.selector); if (binding.set) this.model.off(binding.setTrigger, binding.set); delete this._bindings[key]; }, this); return this; } }); Backbone.View.Binders = { 'value': function(model, attribute, property) { return { get: ['change keyup', function() { return this.val(); }], set: function(value) { this.val(value); } }; }, 'text': function(model, attribute, property) { return { get: ['change', function() { return this.text(); }], set: function(value) { this.text(value); } }; }, 'html': function(model, attribute, property) { return { get: ['change', function() { return this.html(); }], set: function(value) { this.html(value); } }; }, 'class': function(model, attribute, property) { return { set: function(value) { if (this._previousClass) this.removeClass(this._previousClass); this.addClass(value); this._previousClass = value; } }; }, 'checked': function(model, attribute, property) { return { get: ['change', function() { return this.prop('checked'); }], set: function(value) { this.prop('checked', !!value); } }; }, '__attr__': function(model, attribute, property) { return { set: function(value) { this.attr(property, value); } }; } }; })(window._, window.Backbone);