// Backbone.ModelBinder v0.1.6 // (c) 2012 Bart Wood // Distributed Under MIT License (function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['underscore', 'jquery', 'backbone'], factory); } else { // Browser globals factory(_, $, Backbone); } }(function(_, $, Backbone){ if(!Backbone){ throw 'Please include Backbone.js before Backbone.ModelBinder.js'; } Backbone.ModelBinder = function(modelSetOptions){ _.bindAll(this); this._modelSetOptions = modelSetOptions || {}; }; // Current version of the library. Backbone.ModelBinder.VERSION = '0.1.6'; Backbone.ModelBinder.Constants = {}; Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; _.extend(Backbone.ModelBinder.prototype, { bind:function (model, rootEl, attributeBindings, modelSetOptions) { this.unbind(); this._model = model; this._rootEl = rootEl; this._modelSetOptions = _.extend({}, this._modelSetOptions, modelSetOptions); if (!this._model) throw 'model must be specified'; if (!this._rootEl) throw 'rootEl must be specified'; if(attributeBindings){ // Create a deep clone of the attribute bindings this._attributeBindings = $.extend(true, {}, attributeBindings); this._initializeAttributeBindings(); this._initializeElBindings(); } else { this._initializeDefaultBindings(); } this._bindModelToView(); this._bindViewToModel(); }, bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) { this._triggers = triggers; this.bind(model, rootEl, attributeBindings, modelSetOptions) }, unbind:function () { this._unbindModelToView(); this._unbindViewToModel(); if(this._attributeBindings){ delete this._attributeBindings; this._attributeBindings = undefined; } }, // Converts the input bindings, which might just be empty or strings, to binding objects _initializeAttributeBindings:function () { var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding; for (attributeBindingKey in this._attributeBindings) { inputBinding = this._attributeBindings[attributeBindingKey]; if (_.isString(inputBinding)) { attributeBinding = {elementBindings: [{selector: inputBinding}]}; } else if (_.isArray(inputBinding)) { attributeBinding = {elementBindings: inputBinding}; } else if(_.isObject(inputBinding)){ attributeBinding = {elementBindings: [inputBinding]}; } else { throw 'Unsupported type passed to Model Binder ' + attributeBinding; } // Add a linkage from the element binding back to the attribute binding for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){ elementBinding = attributeBinding.elementBindings[elementBindingCount]; elementBinding.attributeBinding = attributeBinding; } attributeBinding.attributeName = attributeBindingKey; this._attributeBindings[attributeBindingKey] = attributeBinding; } }, // If the bindings are not specified, the default binding is performed on the name attribute _initializeDefaultBindings: function(){ var elCount, namedEls, namedEl, name, attributeBinding; this._attributeBindings = {}; namedEls = $('[name]', this._rootEl); for(elCount = 0; elCount < namedEls.length; elCount++){ namedEl = namedEls[elCount]; name = $(namedEl).attr('name'); // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings if(!this._attributeBindings[name]){ attributeBinding = {attributeName: name}; attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [namedEl]}]; this._attributeBindings[name] = attributeBinding; } else{ this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [namedEl]}); } } }, _initializeElBindings:function () { var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el; for (bindingKey in this._attributeBindings) { attributeBinding = this._attributeBindings[bindingKey]; for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { elementBinding = attributeBinding.elementBindings[bindingCount]; if (elementBinding.selector === '') { foundEls = $(this._rootEl); } else { foundEls = $(elementBinding.selector, this._rootEl); } if (foundEls.length === 0) { throw 'Bad binding found. No elements returned for binding selector ' + elementBinding.selector; } else { elementBinding.boundEls = []; for (elCount = 0; elCount < foundEls.length; elCount++) { el = foundEls[elCount]; elementBinding.boundEls.push(el); } } } } }, _bindModelToView: function () { this._model.on('change', this._onModelChange, this); this.copyModelAttributesToView(); }, // attributesToCopy is an optional parameter - if empty, all attributes // that are bound will be copied. Otherwise, only attributeBindings specified // in the attributesToCopy are copied. copyModelAttributesToView: function(attributesToCopy){ var attributeName, attributeBinding; for (attributeName in this._attributeBindings) { if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){ attributeBinding = this._attributeBindings[attributeName]; this._copyModelToView(attributeBinding); } } }, _unbindModelToView: function(){ if(this._model){ this._model.off('change', this._onModelChange); this._model = undefined; } }, _bindViewToModel: function () { if (this._triggers) { _.each(this._triggers, function (event, selector) { $(this._rootEl).delegate(selector, event, this._onElChanged); }, this); } else { $(this._rootEl).delegate('', 'change', this._onElChanged); // The change event doesn't work properly for contenteditable elements - but blur does $(this._rootEl).delegate('[contenteditable]', 'blur', this._onElChanged); } }, _unbindViewToModel: function () { if (this._rootEl) { if (this._triggers) { _.each(this._triggers, function (event, selector) { $(this._rootEl).undelegate(selector, event, this._onElChanged); }, this); } else { $(this._rootEl).undelegate('', 'change', this._onElChanged); $(this._rootEl).undelegate('[contenteditable]', 'blur', this._onElChanged); } } }, _onElChanged:function (event) { var el, elBindings, elBindingCount, elBinding; el = $(event.target)[0]; elBindings = this._getElBindings(el); for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){ elBinding = elBindings[elBindingCount]; if (this._isBindingUserEditable(elBinding)) { this._copyViewToModel(elBinding, el); } } }, _isBindingUserEditable: function(elBinding){ return elBinding.elAttribute === undefined || elBinding.elAttribute === 'text' || elBinding.elAttribute === 'html'; }, _getElBindings:function (findEl) { var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl; var elBindings = []; for (attributeName in this._attributeBindings) { attributeBinding = this._attributeBindings[attributeName]; for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { elementBinding = attributeBinding.elementBindings[elementBindingCount]; for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { boundEl = elementBinding.boundEls[boundElCount]; if (boundEl === findEl) { elBindings.push(elementBinding); } } } } return elBindings; }, _onModelChange:function () { var changedAttribute, attributeBinding; for (changedAttribute in this._model.changedAttributes()) { attributeBinding = this._attributeBindings[changedAttribute]; if (attributeBinding) { this._copyModelToView(attributeBinding); } } }, _copyModelToView:function (attributeBinding) { var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue; value = this._model.get(attributeBinding.attributeName); for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { elementBinding = attributeBinding.elementBindings[elementBindingCount]; for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { boundEl = elementBinding.boundEls[boundElCount]; if(!boundEl._isSetting){ convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); this._setEl($(boundEl), elementBinding, convertedValue); } } } }, _setEl: function (el, elementBinding, convertedValue) { if (elementBinding.elAttribute) { this._setElAttribute(el, elementBinding, convertedValue); } else { this._setElValue(el, convertedValue); } }, _setElAttribute:function (el, elementBinding, convertedValue) { switch (elementBinding.elAttribute) { case 'html': el.html(convertedValue); break; case 'text': el.text(convertedValue); break; case 'enabled': el.attr('disabled', !convertedValue); break; case 'displayed': el[convertedValue ? 'show' : 'hide'](); break; case 'hidden': el[convertedValue ? 'hide' : 'show'](); break; case 'css': el.css(elementBinding.cssAttribute, convertedValue); break; case 'class': var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName); if(!_.isUndefined(previousValue)){ previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue); el.removeClass(previousValue); } if(convertedValue){ el.addClass(convertedValue); } break; default: el.attr(elementBinding.elAttribute, convertedValue); } }, _setElValue:function (el, convertedValue) { if(el.attr('type')){ switch (el.attr('type')) { case 'radio': if (el.val() === convertedValue) { el.attr('checked', 'checked'); } break; case 'checkbox': if (convertedValue) { el.attr('checked', 'checked'); } else { el.removeAttr('checked'); } break; default: el.val(convertedValue); } } else if(el.is('input') || el.is('select') || el.is('textarea')){ el.val(convertedValue); } else { el.text(convertedValue); } }, _copyViewToModel: function (elementBinding, el) { var value, convertedValue; if (!el._isSetting) { el._isSetting = true; this._setModel(elementBinding, $(el)); el._isSetting = false; if(elementBinding.converter){ value = this._model.get(elementBinding.attributeBinding.attributeName); convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); this._setEl($(el), elementBinding, convertedValue); } } }, _getElValue: function(elementBinding, el){ switch (el.attr('type')) { case 'checkbox': return el.prop('checked') ? true : false; default: if(el.attr('contenteditable') !== undefined){ return el.html(); } else { return el.val(); } } }, _setModel: function (elementBinding, el) { var data = {}; var elVal = this._getElValue(elementBinding, el); elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal); data[elementBinding.attributeBinding.attributeName] = elVal; var opts = _.extend({}, this._modelSetOptions, {changeSource: 'ModelBinder'}); this._model.set(data, opts); }, _getConvertedValue: function (direction, elementBinding, value) { if (elementBinding.converter) { value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model); } return value; } }); Backbone.ModelBinder.CollectionConverter = function(collection){ this._collection = collection; if(!this._collection){ throw 'Collection must be defined'; } _.bindAll(this, 'convert'); }; _.extend(Backbone.ModelBinder.CollectionConverter.prototype, { convert: function(direction, value){ if (direction === Backbone.ModelBinder.Constants.ModelToView) { return value ? value.id : undefined; } else { return this._collection.get(value); } } }); // A static helper function to create a default set of bindings that you can customize before calling the bind() function // rootEl - where to find all of the bound elements // attributeType - probably 'name' or 'id' in most cases // converter(optional) - the default converter you want applied to all your bindings // elAttribute(optional) - the default elAttribute you want applied to all your bindings Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){ var foundEls, elCount, foundEl, attributeName; var bindings = {}; foundEls = $('[' + attributeType + ']', rootEl); for(elCount = 0; elCount < foundEls.length; elCount++){ foundEl = foundEls[elCount]; attributeName = $(foundEl).attr(attributeType); if(!bindings[attributeName]){ var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'}; bindings[attributeName] = attributeBinding; if(converter){ bindings[attributeName].converter = converter; } if(elAttribute){ bindings[attributeName].elAttribute = elAttribute; } } } return bindings; }; // Helps you to combine 2 sets of bindings Backbone.ModelBinder.combineBindings = function(destination, source){ _.each(source, function(value, key){ var elementBinding = {selector: value.selector}; if(value.converter){ elementBinding.converter = value.converter; } if(value.elAttribute){ elementBinding.elAttribute = value.elAttribute; } if(!destination[key]){ destination[key] = elementBinding; } else { destination[key] = [destination[key], elementBinding]; } }); return destination; }; return Backbone.ModelBinder; }));