1 // Backbone.ModelBinder v1.0.2
3 // Distributed Under MIT License
6 if (typeof define
=== 'function' && define
.amd
) {
7 // AMD. Register as an anonymous module.
8 define(['underscore', 'jquery', 'backbone'], factory
);
11 factory(_
, $, Backbone
);
13 }(function(_
, $, Backbone
){
16 throw 'Please include Backbone.js before Backbone.ModelBinder.js';
19 Backbone
.ModelBinder = function(){
20 _
.bindAll
.apply(_
, [this].concat(_
.functions(this)));
23 // Static setter for class level options
24 Backbone
.ModelBinder
.SetOptions = function(options
){
25 Backbone
.ModelBinder
.options
= options
;
28 // Current version of the library.
29 Backbone
.ModelBinder
.VERSION
= '1.0.2';
30 Backbone
.ModelBinder
.Constants
= {};
31 Backbone
.ModelBinder
.Constants
.ModelToView
= 'ModelToView';
32 Backbone
.ModelBinder
.Constants
.ViewToModel
= 'ViewToModel';
34 _
.extend(Backbone
.ModelBinder
.prototype, {
36 bind:function (model
, rootEl
, attributeBindings
, options
) {
40 this._rootEl
= rootEl
;
41 this._setOptions(options
);
43 if (!this._model
) this._throwException('model must be specified');
44 if (!this._rootEl
) this._throwException('rootEl must be specified');
46 if(attributeBindings
){
47 // Create a deep clone of the attribute bindings
48 this._attributeBindings
= $.extend(true, {}, attributeBindings
);
50 this._initializeAttributeBindings();
51 this._initializeElBindings();
54 this._initializeDefaultBindings();
57 this._bindModelToView();
58 this._bindViewToModel();
61 bindCustomTriggers: function (model
, rootEl
, triggers
, attributeBindings
, modelSetOptions
) {
62 this._triggers
= triggers
;
63 this.bind(model
, rootEl
, attributeBindings
, modelSetOptions
)
67 this._unbindModelToView();
68 this._unbindViewToModel();
70 if(this._attributeBindings
){
71 delete this._attributeBindings
;
72 this._attributeBindings
= undefined;
76 _setOptions: function(options
){
77 this._options
= _
.extend({
78 boundAttribute
: 'name'
79 }, Backbone
.ModelBinder
.options
, options
);
81 // initialize default options
82 if(!this._options
['modelSetOptions']){
83 this._options
['modelSetOptions'] = {};
85 this._options
['modelSetOptions'].changeSource
= 'ModelBinder';
87 if(!this._options
['changeTriggers']){
88 this._options
['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'};
91 if(!this._options
['initialCopyDirection']){
92 this._options
['initialCopyDirection'] = Backbone
.ModelBinder
.Constants
.ModelToView
;
96 // Converts the input bindings, which might just be empty or strings, to binding objects
97 _initializeAttributeBindings:function () {
98 var attributeBindingKey
, inputBinding
, attributeBinding
, elementBindingCount
, elementBinding
;
100 for (attributeBindingKey
in this._attributeBindings
) {
101 inputBinding
= this._attributeBindings
[attributeBindingKey
];
103 if (_
.isString(inputBinding
)) {
104 attributeBinding
= {elementBindings
: [{selector
: inputBinding
}]};
106 else if (_
.isArray(inputBinding
)) {
107 attributeBinding
= {elementBindings
: inputBinding
};
109 else if(_
.isObject(inputBinding
)){
110 attributeBinding
= {elementBindings
: [inputBinding
]};
113 this._throwException('Unsupported type passed to Model Binder ' + attributeBinding
);
116 // Add a linkage from the element binding back to the attribute binding
117 for(elementBindingCount
= 0; elementBindingCount
< attributeBinding
.elementBindings
.length
; elementBindingCount
++){
118 elementBinding
= attributeBinding
.elementBindings
[elementBindingCount
];
119 elementBinding
.attributeBinding
= attributeBinding
;
122 attributeBinding
.attributeName
= attributeBindingKey
;
123 this._attributeBindings
[attributeBindingKey
] = attributeBinding
;
127 // If the bindings are not specified, the default binding is performed on the specified attribute, name by default
128 _initializeDefaultBindings: function(){
129 var elCount
, elsWithAttribute
, matchedEl
, name
, attributeBinding
;
131 this._attributeBindings
= {};
132 elsWithAttribute
= $('[' + this._options
['boundAttribute'] + ']', this._rootEl
);
134 for(elCount
= 0; elCount
< elsWithAttribute
.length
; elCount
++){
135 matchedEl
= elsWithAttribute
[elCount
];
136 name
= $(matchedEl
).attr(this._options
['boundAttribute']);
138 // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings
139 if(!this._attributeBindings
[name
]){
140 attributeBinding
= {attributeName
: name
};
141 attributeBinding
.elementBindings
= [{attributeBinding
: attributeBinding
, boundEls
: [matchedEl
]}];
142 this._attributeBindings
[name
] = attributeBinding
;
145 this._attributeBindings
[name
].elementBindings
.push({attributeBinding
: this._attributeBindings
[name
], boundEls
: [matchedEl
]});
150 _initializeElBindings:function () {
151 var bindingKey
, attributeBinding
, bindingCount
, elementBinding
, foundEls
, elCount
, el
;
152 for (bindingKey
in this._attributeBindings
) {
153 attributeBinding
= this._attributeBindings
[bindingKey
];
155 for (bindingCount
= 0; bindingCount
< attributeBinding
.elementBindings
.length
; bindingCount
++) {
156 elementBinding
= attributeBinding
.elementBindings
[bindingCount
];
157 if (elementBinding
.selector
=== '') {
158 foundEls
= $(this._rootEl
);
161 foundEls
= $(elementBinding
.selector
, this._rootEl
);
164 if (foundEls
.length
=== 0) {
165 this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding
.selector
);
168 elementBinding
.boundEls
= [];
169 for (elCount
= 0; elCount
< foundEls
.length
; elCount
++) {
170 el
= foundEls
[elCount
];
171 elementBinding
.boundEls
.push(el
);
178 _bindModelToView: function () {
179 this._model
.on('change', this._onModelChange
, this);
181 if(this._options
['initialCopyDirection'] === Backbone
.ModelBinder
.Constants
.ModelToView
){
182 this.copyModelAttributesToView();
186 // attributesToCopy is an optional parameter - if empty, all attributes
187 // that are bound will be copied. Otherwise, only attributeBindings specified
188 // in the attributesToCopy are copied.
189 copyModelAttributesToView: function(attributesToCopy
){
190 var attributeName
, attributeBinding
;
192 for (attributeName
in this._attributeBindings
) {
193 if(attributesToCopy
=== undefined || _
.indexOf(attributesToCopy
, attributeName
) !== -1){
194 attributeBinding
= this._attributeBindings
[attributeName
];
195 this._copyModelToView(attributeBinding
);
200 copyViewValuesToModel: function(){
201 var bindingKey
, attributeBinding
, bindingCount
, elementBinding
, elCount
, el
;
202 for (bindingKey
in this._attributeBindings
) {
203 attributeBinding
= this._attributeBindings
[bindingKey
];
205 for (bindingCount
= 0; bindingCount
< attributeBinding
.elementBindings
.length
; bindingCount
++) {
206 elementBinding
= attributeBinding
.elementBindings
[bindingCount
];
208 if(this._isBindingUserEditable(elementBinding
)){
209 if(this._isBindingRadioGroup(elementBinding
)){
210 el
= this._getRadioButtonGroupCheckedEl(elementBinding
);
212 this._copyViewToModel(elementBinding
, el
);
216 for(elCount
= 0; elCount
< elementBinding
.boundEls
.length
; elCount
++){
217 el
= $(elementBinding
.boundEls
[elCount
]);
218 if(this._isElUserEditable(el
)){
219 this._copyViewToModel(elementBinding
, el
);
228 _unbindModelToView: function(){
230 this._model
.off('change', this._onModelChange
);
231 this._model
= undefined;
235 _bindViewToModel: function () {
236 _
.each(this._options
['changeTriggers'], function (event
, selector
) {
237 $(this._rootEl
).delegate(selector
, event
, this._onElChanged
);
240 if(this._options
['initialCopyDirection'] === Backbone
.ModelBinder
.Constants
.ViewToModel
){
241 this.copyViewValuesToModel();
245 _unbindViewToModel: function () {
246 if(this._options
&& this._options
['changeTriggers']){
247 _
.each(this._options
['changeTriggers'], function (event
, selector
) {
248 $(this._rootEl
).undelegate(selector
, event
, this._onElChanged
);
253 _onElChanged:function (event
) {
254 var el
, elBindings
, elBindingCount
, elBinding
;
256 el
= $(event
.target
)[0];
257 elBindings
= this._getElBindings(el
);
259 for(elBindingCount
= 0; elBindingCount
< elBindings
.length
; elBindingCount
++){
260 elBinding
= elBindings
[elBindingCount
];
261 if (this._isBindingUserEditable(elBinding
)) {
262 this._copyViewToModel(elBinding
, el
);
267 _isBindingUserEditable: function(elBinding
){
268 return elBinding
.elAttribute
=== undefined ||
269 elBinding
.elAttribute
=== 'text' ||
270 elBinding
.elAttribute
=== 'html';
273 _isElUserEditable: function(el
){
274 var isContentEditable
= el
.attr('contenteditable');
275 return isContentEditable
|| el
.is('input') || el
.is('select') || el
.is('textarea');
278 _isBindingRadioGroup: function(elBinding
){
280 var isAllRadioButtons
= elBinding
.boundEls
.length
> 0;
281 for(elCount
= 0; elCount
< elBinding
.boundEls
.length
; elCount
++){
282 el
= $(elBinding
.boundEls
[elCount
]);
283 if(el
.attr('type') !== 'radio'){
284 isAllRadioButtons
= false;
289 return isAllRadioButtons
;
292 _getRadioButtonGroupCheckedEl: function(elBinding
){
294 for(elCount
= 0; elCount
< elBinding
.boundEls
.length
; elCount
++){
295 el
= $(elBinding
.boundEls
[elCount
]);
296 if(el
.attr('type') === 'radio' && el
.attr('checked')){
304 _getElBindings:function (findEl
) {
305 var attributeName
, attributeBinding
, elementBindingCount
, elementBinding
, boundElCount
, boundEl
;
308 for (attributeName
in this._attributeBindings
) {
309 attributeBinding
= this._attributeBindings
[attributeName
];
311 for (elementBindingCount
= 0; elementBindingCount
< attributeBinding
.elementBindings
.length
; elementBindingCount
++) {
312 elementBinding
= attributeBinding
.elementBindings
[elementBindingCount
];
314 for (boundElCount
= 0; boundElCount
< elementBinding
.boundEls
.length
; boundElCount
++) {
315 boundEl
= elementBinding
.boundEls
[boundElCount
];
317 if (boundEl
=== findEl
) {
318 elBindings
.push(elementBinding
);
327 _onModelChange:function () {
328 var changedAttribute
, attributeBinding
;
330 for (changedAttribute
in this._model
.changedAttributes()) {
331 attributeBinding
= this._attributeBindings
[changedAttribute
];
333 if (attributeBinding
) {
334 this._copyModelToView(attributeBinding
);
339 _copyModelToView:function (attributeBinding
) {
340 var elementBindingCount
, elementBinding
, boundElCount
, boundEl
, value
, convertedValue
;
342 value
= this._model
.get(attributeBinding
.attributeName
);
344 for (elementBindingCount
= 0; elementBindingCount
< attributeBinding
.elementBindings
.length
; elementBindingCount
++) {
345 elementBinding
= attributeBinding
.elementBindings
[elementBindingCount
];
347 for (boundElCount
= 0; boundElCount
< elementBinding
.boundEls
.length
; boundElCount
++) {
348 boundEl
= elementBinding
.boundEls
[boundElCount
];
350 if(!boundEl
._isSetting
){
351 convertedValue
= this._getConvertedValue(Backbone
.ModelBinder
.Constants
.ModelToView
, elementBinding
, value
);
352 this._setEl($(boundEl
), elementBinding
, convertedValue
);
358 _setEl: function (el
, elementBinding
, convertedValue
) {
359 if (elementBinding
.elAttribute
) {
360 this._setElAttribute(el
, elementBinding
, convertedValue
);
363 this._setElValue(el
, convertedValue
);
367 _setElAttribute:function (el
, elementBinding
, convertedValue
) {
368 switch (elementBinding
.elAttribute
) {
370 el
.html(convertedValue
);
373 el
.text(convertedValue
);
376 el
.prop('disabled', !convertedValue
);
379 el
[convertedValue
? 'show' : 'hide']();
382 el
[convertedValue
? 'hide' : 'show']();
385 el
.css(elementBinding
.cssAttribute
, convertedValue
);
388 var previousValue
= this._model
.previous(elementBinding
.attributeBinding
.attributeName
);
389 var currentValue
= this._model
.get(elementBinding
.attributeBinding
.attributeName
);
390 // is current value is now defined then remove the class the may have been set for the undefined value
391 if(!_
.isUndefined(previousValue
) || !_
.isUndefined(currentValue
)){
392 previousValue
= this._getConvertedValue(Backbone
.ModelBinder
.Constants
.ModelToView
, elementBinding
, previousValue
);
393 el
.removeClass(previousValue
);
397 el
.addClass(convertedValue
);
401 el
.attr(elementBinding
.elAttribute
, convertedValue
);
405 _setElValue:function (el
, convertedValue
) {
407 switch (el
.attr('type')) {
409 if (el
.val() === convertedValue
) {
410 // must defer the change trigger or the change will actually fire with the old value
411 el
.prop('checked') || _
.defer(function() { el
.trigger('change'); });
412 el
.prop('checked', true);
415 // must defer the change trigger or the change will actually fire with the old value
416 el
.prop('checked', false);
420 // must defer the change trigger or the change will actually fire with the old value
421 el
.prop('checked') === !!convertedValue
|| _
.defer(function() { el
.trigger('change') });
422 el
.prop('checked', !!convertedValue
);
427 el
.val(convertedValue
);
430 else if(el
.is('input') || el
.is('select') || el
.is('textarea')){
431 el
.val(convertedValue
|| (convertedValue
=== 0 ? '0' : ''));
434 el
.text(convertedValue
|| (convertedValue
=== 0 ? '0' : ''));
438 _copyViewToModel: function (elementBinding
, el
) {
439 var result
, value
, convertedValue
;
441 if (!el
._isSetting
) {
443 el
._isSetting
= true;
444 result
= this._setModel(elementBinding
, $(el
));
445 el
._isSetting
= false;
447 if(result
&& elementBinding
.converter
){
448 value
= this._model
.get(elementBinding
.attributeBinding
.attributeName
);
449 convertedValue
= this._getConvertedValue(Backbone
.ModelBinder
.Constants
.ModelToView
, elementBinding
, value
);
450 this._setEl($(el
), elementBinding
, convertedValue
);
455 _getElValue: function(elementBinding
, el
){
456 switch (el
.attr('type')) {
458 return el
.prop('checked') ? true : false;
460 if(el
.attr('contenteditable') !== undefined){
469 _setModel: function (elementBinding
, el
) {
471 var elVal
= this._getElValue(elementBinding
, el
);
472 elVal
= this._getConvertedValue(Backbone
.ModelBinder
.Constants
.ViewToModel
, elementBinding
, elVal
);
473 data
[elementBinding
.attributeBinding
.attributeName
] = elVal
;
474 return this._model
.set(data
, this._options
['modelSetOptions']);
477 _getConvertedValue: function (direction
, elementBinding
, value
) {
478 if (elementBinding
.converter
) {
479 value
= elementBinding
.converter(direction
, value
, elementBinding
.attributeBinding
.attributeName
, this._model
, elementBinding
.boundEls
);
485 _throwException: function(message
){
486 if(this._options
.suppressThrows
){
487 if(console
&& console
.error
){
488 console
.error(message
);
497 Backbone
.ModelBinder
.CollectionConverter = function(collection
){
498 this._collection
= collection
;
500 if(!this._collection
){
501 throw 'Collection must be defined';
503 _
.bindAll(this, 'convert');
506 _
.extend(Backbone
.ModelBinder
.CollectionConverter
.prototype, {
507 convert: function(direction
, value
){
508 if (direction
=== Backbone
.ModelBinder
.Constants
.ModelToView
) {
509 return value
? value
.id
: undefined;
512 return this._collection
.get(value
);
517 // A static helper function to create a default set of bindings that you can customize before calling the bind() function
518 // rootEl - where to find all of the bound elements
519 // attributeType - probably 'name' or 'id' in most cases
520 // converter(optional) - the default converter you want applied to all your bindings
521 // elAttribute(optional) - the default elAttribute you want applied to all your bindings
522 Backbone
.ModelBinder
.createDefaultBindings = function(rootEl
, attributeType
, converter
, elAttribute
){
523 var foundEls
, elCount
, foundEl
, attributeName
;
526 foundEls
= $('[' + attributeType
+ ']', rootEl
);
528 for(elCount
= 0; elCount
< foundEls
.length
; elCount
++){
529 foundEl
= foundEls
[elCount
];
530 attributeName
= $(foundEl
).attr(attributeType
);
532 if(!bindings
[attributeName
]){
533 var attributeBinding
= {selector
: '[' + attributeType
+ '="' + attributeName
+ '"]'};
534 bindings
[attributeName
] = attributeBinding
;
537 bindings
[attributeName
].converter
= converter
;
541 bindings
[attributeName
].elAttribute
= elAttribute
;
549 // Helps you to combine 2 sets of bindings
550 Backbone
.ModelBinder
.combineBindings = function(destination
, source
){
551 _
.each(source
, function(value
, key
){
552 var elementBinding
= {selector
: value
.selector
};
555 elementBinding
.converter
= value
.converter
;
558 if(value
.elAttribute
){
559 elementBinding
.elAttribute
= value
.elAttribute
;
562 if(!destination
[key
]){
563 destination
[key
] = elementBinding
;
566 destination
[key
] = [destination
[key
], elementBinding
];
574 return Backbone
.ModelBinder
;