UNPKG

grunt-doctrine

Version:

Grunt plugin to convert doctrine xml annotations into backbone models and collections. Useful in conjunction with doctrine apigility. Includes option to scaffold an entire BBB application

578 lines (480 loc) 22.9 kB
// Backbone.ModelBinder v1.0.6 // (c) 2013 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 if(typeof module !== 'undefined' && module.exports) { // CommonJS module.exports = factory( require('underscore'), require('jquery'), require('backbone') ); } else { // Browser globals factory(_, jQuery, Backbone); } }(function(_, $, Backbone){ if(!Backbone){ throw 'Please include Backbone.js before Backbone.ModelBinder.js'; } Backbone.ModelBinder = function(){ _.bindAll.apply(_, [this].concat(_.functions(this))); }; // Static setter for class level options Backbone.ModelBinder.SetOptions = function(options){ Backbone.ModelBinder.options = options; }; // Current version of the library. Backbone.ModelBinder.VERSION = '1.0.6'; Backbone.ModelBinder.Constants = {}; Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; _.extend(Backbone.ModelBinder.prototype, { bind:function (model, rootEl, attributeBindings, options) { this.unbind(); this._model = model; this._rootEl = rootEl; this._setOptions(options); if (!this._model) this._throwException('model must be specified'); if (!this._rootEl) this._throwException('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; } }, _setOptions: function(options){ this._options = _.extend({ boundAttribute: 'name' }, Backbone.ModelBinder.options, options); // initialize default options if(!this._options['modelSetOptions']){ this._options['modelSetOptions'] = {}; } this._options['modelSetOptions'].changeSource = 'ModelBinder'; if(!this._options['changeTriggers']){ this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'}; } if(!this._options['initialCopyDirection']){ this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView; } }, // 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 { this._throwException('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 specified attribute, name by default _initializeDefaultBindings: function(){ var elCount, elsWithAttribute, matchedEl, name, attributeBinding; this._attributeBindings = {}; elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl); for(elCount = 0; elCount < elsWithAttribute.length; elCount++){ matchedEl = elsWithAttribute[elCount]; name = $(matchedEl).attr(this._options['boundAttribute']); // 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: [matchedEl]}]; this._attributeBindings[name] = attributeBinding; } else{ this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]}); } } }, _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) { this._throwException('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); if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){ 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); } } }, copyViewValuesToModel: function(){ var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el; for (bindingKey in this._attributeBindings) { attributeBinding = this._attributeBindings[bindingKey]; for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { elementBinding = attributeBinding.elementBindings[bindingCount]; if(this._isBindingUserEditable(elementBinding)){ if(this._isBindingRadioGroup(elementBinding)){ el = this._getRadioButtonGroupCheckedEl(elementBinding); if(el){ this._copyViewToModel(elementBinding, el); } } else { for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){ el = $(elementBinding.boundEls[elCount]); if(this._isElUserEditable(el)){ this._copyViewToModel(elementBinding, el); } } } } } } }, _unbindModelToView: function(){ if(this._model){ this._model.off('change', this._onModelChange); this._model = undefined; } }, _bindViewToModel: function () { _.each(this._options['changeTriggers'], function (event, selector) { $(this._rootEl).delegate(selector, event, this._onElChanged); }, this); if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){ this.copyViewValuesToModel(); } }, _unbindViewToModel: function () { if(this._options && this._options['changeTriggers']){ _.each(this._options['changeTriggers'], function (event, selector) { $(this._rootEl).undelegate(selector, event, this._onElChanged); }, this); } }, _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'; }, _isElUserEditable: function(el){ var isContentEditable = el.attr('contenteditable'); return isContentEditable || el.is('input') || el.is('select') || el.is('textarea'); }, _isBindingRadioGroup: function(elBinding){ var elCount, el; var isAllRadioButtons = elBinding.boundEls.length > 0; for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ el = $(elBinding.boundEls[elCount]); if(el.attr('type') !== 'radio'){ isAllRadioButtons = false; break; } } return isAllRadioButtons; }, _getRadioButtonGroupCheckedEl: function(elBinding){ var elCount, el; for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ el = $(elBinding.boundEls[elCount]); if(el.attr('type') === 'radio' && el.attr('checked')){ return el; } } return undefined; }, _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.prop('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); var currentValue = this._model.get(elementBinding.attributeBinding.attributeName); // is current value is now defined then remove the class the may have been set for the undefined value if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){ 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': el.prop('checked', el.val() === convertedValue); break; case 'checkbox': el.prop('checked', !!convertedValue); break; case 'file': break; default: el.val(convertedValue); } } else if(el.is('input') || el.is('select') || el.is('textarea')){ el.val(convertedValue || (convertedValue === 0 ? '0' : '')); } else { el.text(convertedValue || (convertedValue === 0 ? '0' : '')); } }, _copyViewToModel: function (elementBinding, el) { var result, value, convertedValue; if (!el._isSetting) { el._isSetting = true; result = this._setModel(elementBinding, $(el)); el._isSetting = false; if(result && 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; return this._model.set(data, this._options['modelSetOptions']); }, _getConvertedValue: function (direction, elementBinding, value) { if (elementBinding.converter) { value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); } else if(this._options['converter']){ value = this._options['converter'](direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); } return value; }, _throwException: function(message){ if(this._options.suppressThrows){ if(typeof(console) !== 'undefined' && console.error){ console.error(message); } } else { throw message; } } }); 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; }));