forked from gmac/backbone.epoxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backbone.epoxy.js
1358 lines (1124 loc) · 41.7 KB
/
backbone.epoxy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Backbone.Epoxy
// (c) 2013 Greg MacWilliam
// Freely distributed under the MIT license
// For usage and documentation:
// http://epoxyjs.org
(function(root, factory) {
if (typeof exports !== 'undefined') {
// Define as CommonJS export:
module.exports = factory(require("underscore"), require("backbone"));
} else if (typeof define === 'function' && define.amd) {
// Define as AMD:
define(["underscore", "backbone"], factory);
} else {
// Just run it:
factory(root._, root.Backbone);
}
}(this, function(_, Backbone) {
// Epoxy namespace:
var Epoxy = Backbone.Epoxy = {};
// Object-type utils:
var array = Array.prototype;
var isUndefined = _.isUndefined;
var isFunction = _.isFunction;
var isObject = _.isObject;
var isArray = _.isArray;
var isModel = function(obj) { return obj instanceof Backbone.Model; };
var isCollection = function(obj) { return obj instanceof Backbone.Collection; };
var blankMethod = function() {};
// Static mixins API:
// added as a static member to Epoxy class objects (Model & View);
// generates a set of class attributes for mixin with other objects.
var mixins = {
mixin: function(extend) {
extend = extend || {};
for (var i in this.prototype) {
if (this.prototype.hasOwnProperty(i) && i !== 'constructor' && i !== 'bindings' ) {
extend[i] = this.prototype[i];
}
}
return extend;
}
};
// Calls method implementations of a super-class object:
function _super(instance, method, args) {
return instance._super.prototype[method].apply(instance, args);
}
// Epoxy.Model
// -----------
var modelMap;
var modelProps = ['computeds'];
Epoxy.Model = Backbone.Model.extend({
_super: Backbone.Model,
// Backbone.Model constructor override:
// configures computed model attributes around the underlying native Backbone model.
constructor: function(attributes, options) {
_.extend(this, _.pick(options||{}, modelProps));
_super(this, 'constructor', arguments);
this.initComputeds(attributes, options);
},
// Gets a copy of a model attribute value:
// Array and Object values will return a shallow copy,
// primitive values will be returned directly.
getCopy: function(attribute) {
return _.clone(this.get(attribute));
},
// Backbone.Model.get() override:
// provides access to computed attributes,
// and maps computed dependency references while establishing bindings.
get: function(attribute) {
// Automatically register bindings while building out computed dependency graphs:
modelMap && modelMap.push(['change:'+attribute, this]);
// Return a computed property value, if available:
if (this.hasComputed(attribute)) {
return this.c()[ attribute ].get();
}
// Default to native Backbone.Model get operation:
return _super(this, 'get', arguments);
},
// Backbone.Model.set() override:
// will process any computed attribute setters,
// and then pass along all results to the underlying model.
set: function(key, value, options) {
var params = key;
// Convert key/value arguments into {key:value} format:
if (params && !isObject(params)) {
params = {};
params[ key ] = value;
} else {
options = value;
}
// Default options definition:
options = options || {};
// Create store for capturing computed change events:
var computedEvents = this._setting = [];
// Attempt to set computed attributes while not unsetting:
if (!options.unset) {
// All param properties are tested against computed setters,
// properties set to computeds will be removed from the params table.
// Optionally, an computed setter may return key/value pairs to be merged into the set.
params = deepModelSet(this, params, {}, []);
}
// Remove computed change events store:
delete this._setting;
// Pass all resulting set params along to the underlying Backbone Model.
var result = _super(this, 'set', [params, options]);
// Dispatch all outstanding events:
if (!options.silent) {
_.each(computedEvents, function(evt) {
this.trigger.apply(this, evt);
}, this);
if (!this.hasChanged()) {
this.trigger('change', this);
}
}
return result;
},
// Backbone.Model.toJSON() override:
// adds a 'computed' option, specifying to include computed attributes.
toJSON: function(options) {
var json = _super(this, 'toJSON', arguments);
if (options && options.computed) {
_.each(this.c(), function(computed, attribute) {
json[ attribute ] = computed.value;
});
}
return json;
},
// Backbone.Model.destroy() override:
// clears all computed attributes before destroying.
destroy: function() {
this.clearComputeds();
return _super(this, 'destroy', arguments);
},
// Computed namespace manager:
// Allows the model to operate as a mixin.
c: function() {
return this._c || (this._c = {});
},
// Initializes the Epoxy model:
// called automatically by the native constructor,
// or may be called manually when adding Epoxy as a mixin.
initComputeds: function(attributes, options) {
this.clearComputeds();
// Resolve computeds hash, and extend it with any preset attribute keys:
// TODO: write test.
var computeds = _.result(this, 'computeds')||{};
computeds = _.extend(computeds, _.pick(attributes||{}, _.keys(computeds)));
// Add all computed attributes:
_.each(computeds, function(params, attribute) {
params._init = 1;
this.addComputed(attribute, params);
}, this);
// Initialize all computed attributes:
// all presets have been constructed and may reference each other now.
_.invoke(this.c(), 'init');
},
// Adds a computed attribute to the model:
// computed attribute will assemble and return customized values.
// @param attribute (string)
// @param getter (function) OR params (object)
// @param [setter (function)]
// @param [dependencies ...]
addComputed: function(attribute, getter, setter) {
this.removeComputed(attribute);
var params = getter;
var delayInit = params._init;
// Test if getter and/or setter are provided:
if (isFunction(getter)) {
var depsIndex = 2;
// Add getter param:
params = {};
params._get = getter;
// Test for setter param:
if (isFunction(setter)) {
params._set = setter;
depsIndex++;
}
// Collect all additional arguments as dependency definitions:
params.deps = array.slice.call(arguments, depsIndex);
}
// Create a new computed attribute:
this.c()[ attribute ] = new EpoxyComputedModel(this, attribute, params, delayInit);
return this;
},
// Tests the model for a computed attribute definition:
hasComputed: function(attribute) {
return this.c().hasOwnProperty(attribute);
},
// Removes an computed attribute from the model:
removeComputed: function(attribute) {
if (this.hasComputed(attribute)) {
this.c()[ attribute ].dispose();
delete this.c()[ attribute ];
}
return this;
},
// Removes all computed attributes:
clearComputeds: function() {
for (var attribute in this.c()) {
this.removeComputed(attribute);
}
return this;
},
// Internal array value modifier:
// performs array ops on a stored array value, then fires change.
// No action is taken if the specified attribute value is not an array.
modifyArray: function(attribute, method, options) {
var obj = this.get(attribute);
if (isArray(obj) && isFunction(array[method])) {
var args = array.slice.call(arguments, 2);
var result = array[ method ].apply(obj, args);
options = options || {};
if (!options.silent) {
this.trigger('change:'+attribute+' change', this, array, options);
}
return result;
}
return null;
},
// Internal object value modifier:
// sets new property values on a stored object value, then fires change.
// No action is taken if the specified attribute value is not an object.
modifyObject: function(attribute, property, value, options) {
var obj = this.get(attribute);
var change = false;
// If property is Object:
if (isObject(obj)) {
options = options || {};
// Delete existing property in response to undefined values:
if (isUndefined(value) && obj.hasOwnProperty(property)) {
delete obj[property];
change = true;
}
// Set new and/or changed property values:
else if (obj[ property ] !== value) {
obj[ property ] = value;
change = true;
}
// Trigger model change:
if (change && !options.silent) {
this.trigger('change:'+attribute+' change', this, obj, options);
}
// Return the modified object:
return obj;
}
return null;
}
}, mixins);
// Epoxy.Model -> Private
// ----------------------
// Model deep-setter:
// Attempts to set a collection of key/value attribute pairs to computed attributes.
// Observable setters may digest values, and then return mutated key/value pairs for inclusion into the set operation.
// Values returned from computed setters will be recursively deep-set, allowing computeds to set other computeds.
// The final collection of resolved key/value pairs (after setting all computeds) will be returned to the native model.
// @param model: target Epoxy model on which to operate.
// @param toSet: an object of key/value pairs to attempt to set within the computed model.
// @param toReturn: resolved non-ovservable attribute values to be returned back to the native model.
// @param trace: property stack trace (prevents circular setter loops).
function deepModelSet(model, toSet, toReturn, stack) {
// Loop through all setter properties:
for (var attribute in toSet) {
if (toSet.hasOwnProperty(attribute)) {
// Pull each setter value:
var value = toSet[ attribute ];
if (model.hasComputed(attribute)) {
// Has a computed attribute:
// comfirm attribute does not already exist within the stack trace.
if (!stack.length || !_.contains(stack, attribute)) {
// Non-recursive:
// set and collect value from computed attribute.
value = model.c()[attribute].set(value);
// Recursively set new values for a returned params object:
// creates a new copy of the stack trace for each new search branch.
if (value && isObject(value)) {
toReturn = deepModelSet(model, value, toReturn, stack.concat(attribute));
}
} else {
// Recursive:
// Throw circular reference error.
throw('Recursive setter: '+stack.join(' > '));
}
} else {
// No computed attribute:
// set the value to the keeper values.
toReturn[ attribute ] = value;
}
}
}
return toReturn;
}
// Epoxy.Model -> Computed
// -----------------------
// Computed objects store model values independently from the model's attributes table.
// Computeds define custom getter/setter functions to manage their value.
function EpoxyComputedModel(model, name, params, delayInit) {
params = params || {};
// Rewrite getter param:
if (params.get && isFunction(params.get)) {
params._get = params.get;
}
// Rewrite setter param:
if (params.set && isFunction(params.set)) {
params._set = params.set;
}
// Prohibit override of 'get()' and 'set()', then extend:
delete params.get;
delete params.set;
_.extend(this, params);
// Set model, name, and default dependencies array:
this.model = model;
this.name = name;
this.deps = this.deps || [];
// Skip init while parent model is initializing:
// Model will initialize in two passes...
// the first pass sets up all computed attributes,
// then the second pass initializes all bindings.
if (!delayInit) this.init();
}
_.extend(EpoxyComputedModel.prototype, Backbone.Events, {
// Initializes the computed's value and bindings:
// this method is called independently from the object constructor,
// allowing computeds to build and initialize in two passes by the parent model.
init: function() {
// Configure dependency map, then update the computed's value:
// All Epoxy.Model attributes accessed while getting the initial value
// will automatically register themselves within the model bindings map.
var bindings = {};
var deps = modelMap = [];
this.get(true);
modelMap = null;
// If the computed has dependencies, then proceed to binding it:
if (deps.length) {
// Compile normalized bindings table:
// Ultimately, we want a table of event types, each with an array of their associated targets:
// {'change:name':[<model1>], 'change:status':[<model1>,<model2>]}
// Compile normalized bindings map:
_.each(deps, function(value) {
var attribute = value[0];
var target = value[1];
// Populate event target arrays:
if (!bindings[attribute]) {
bindings[attribute] = [ target ];
} else if (!_.contains(bindings[attribute], target)) {
bindings[attribute].push(target);
}
});
// Bind all event declarations to their respective targets:
_.each(bindings, function(targets, binding) {
for (var i=0, len=targets.length; i < len; i++) {
this.listenTo(targets[i], binding, _.bind(this.get, this, true));
}
}, this);
}
},
// Gets an attribute value from the parent model.
val: function(attribute) {
return this.model.get(attribute);
},
// Gets the computed's current value:
// Computed values flagged as dirty will need to regenerate themselves.
// Note: 'update' is strongly checked as TRUE to prevent unintended arguments (handler events, etc) from qualifying.
get: function(update) {
if (update === true && this._get) {
var val = this._get.apply(this.model, _.map(this.deps, this.val, this));
this.change(val);
}
return this.value;
},
// Sets the computed's current value:
// computed values (have a custom getter method) require a custom setter.
// Custom setters should return an object of key/values pairs;
// key/value pairs returned to the parent model will be merged into its main .set() operation.
set: function(val) {
if (this._get) {
if (this._set) return this._set.apply(this.model, arguments);
else throw('Cannot set read-only computed attribute.');
}
this.change(val);
return null;
},
// Changes the computed's value:
// new values are cached, then fire an update event.
change: function(value) {
if (!_.isEqual(value, this.value)) {
this.value = value;
var evt = ['change:'+this.name, this.model, value];
if (this.model._setting) {
this.model._setting.push(evt);
} else {
evt[0] += ' change';
this.model.trigger.apply(this.model, evt);
}
}
},
// Disposal:
// cleans up events and releases references.
dispose: function() {
this.stopListening();
this.off();
this.model = this.value = null;
}
});
// Epoxy.binding -> Binding API
// ----------------------------
var bindingSettings = {
optionText: 'label',
optionValue: 'value'
};
// Cache for storing binding parser functions:
// Cuts down on redundancy when building repetitive binding views.
var bindingCache = {};
// Reads value from an accessor:
// Accessors come in three potential forms:
// => A function to call for the requested value.
// => An object with a collection of attribute accessors.
// => A primitive (string, number, boolean, etc).
// This function unpacks an accessor and returns its underlying value(s).
function readAccessor(accessor) {
if (isFunction(accessor)) {
// Accessor is function: return invoked value.
return accessor();
}
else if (isObject(accessor)) {
// Accessor is object/array: return copy with all attributes read.
accessor = _.clone(accessor);
_.each(accessor, function(value, key) {
accessor[ key ] = readAccessor(value);
});
}
// return formatted value, or pass through primitives:
return accessor;
}
// Binding Handlers
// ----------------
// Handlers define set/get methods for exchanging data with the DOM.
// Formatting function for defining new handler objects:
function makeHandler(handler) {
return isFunction(handler) ? {set: handler} : handler;
}
var bindingHandlers = {
// Attribute: write-only. Sets element attributes.
attr: makeHandler(function($element, value) {
$element.attr(value);
}),
// Checked: read-write. Toggles the checked status of a form element.
checked: makeHandler({
get: function($element, currentValue) {
var checked = !!$element.prop('checked');
var value = $element.val();
if (this.isRadio($element)) {
// Radio button: return value directly.
return value;
} else if (isArray(currentValue)) {
// Checkbox array: add/remove value from list.
currentValue = currentValue.slice();
var index = _.indexOf(currentValue, value);
if (checked && index < 0) {
currentValue.push(value);
} else if (!checked && index > -1) {
currentValue.splice(index, 1);
}
return currentValue;
}
// Checkbox: return boolean toggle.
return checked;
},
set: function($element, value) {
// Default as loosely-typed boolean:
var checked = !!value;
if (this.isRadio($element)) {
// Radio button: match checked state to radio value.
checked = (value == $element.val());
} else if (isArray(value)) {
// Checkbox array: match checked state to checkbox value in array contents.
checked = _.contains(value, $element.val());
}
// Set checked property to element:
$element.prop('checked', checked);
},
// Is radio button: avoids '.is(":radio");' check for basic Zepto compatibility.
isRadio: function($element) {
return $element.attr('type').toLowerCase() === 'radio';
}
}),
// Class Name: write-only. Toggles a collection of class name definitions.
classes: makeHandler(function($element, value) {
_.each(value, function(enabled, className) {
$element.toggleClass(className, !!enabled);
});
}),
// Collection: write-only. Manages a list of views bound to a Backbone.Collection.
collection: makeHandler({
init: function($element, collection, context) {
if (!isCollection(collection)) throw('Binding "collection" requires a Collection.');
if (!isFunction(this.view.itemView)) throw('Binding "collection" requires an itemView.');
this.v = {};
},
set: function($element, collection, target) {
var view;
var views = this.v;
var ItemView = this.view.itemView;
var models = collection.models;
// Cache and reset the current dependency graph state:
// sub-views may be created (each with their own dependency graph),
// therefore we need to suspend the working graph map here before making children...
var mapCache = viewMap;
viewMap = null;
// Default target to the bound collection object:
// during init (or failure), the binding will reset.
target = target || collection;
if (isModel(target)) {
// ADD/REMOVE Event (from a Model):
// test if view exists within the binding...
if (!views.hasOwnProperty(target.cid)) {
// Add new view:
views[ target.cid ] = view = new ItemView({model: target});
var index = _.indexOf(models, target);
var $children = $element.children();
// Attempt to add at proper index,
// otherwise just append into the element.
if (index < $children.length) {
$children.eq(index).before(view.$el);
} else {
$element.append(view.$el);
}
} else {
// Remove existing view:
views[ target.cid ].remove();
delete views[ target.cid ];
}
} else if (isCollection(target)) {
// SORT/RESET Event (from a Collection):
// First test if we're sorting...
// (number of models has not changed and all their views are present)
var sort = models.length === _.size(views) && collection.every(function(model) {
return views.hasOwnProperty(model.cid);
});
// Hide element before manipulating:
$element.children().detach();
var frag = document.createDocumentFragment();
if (sort) {
// Sort existing views:
collection.each(function(model) {
frag.appendChild(views[model.cid].el);
});
} else {
// Reset with new views:
this.clean();
collection.each(function(model) {
views[ model.cid ] = view = new ItemView({model: model});
frag.appendChild(view.el);
});
}
$element.append(frag);
}
// Restore cached dependency graph configuration:
viewMap = mapCache;
},
clean: function() {
for (var id in this.v) {
if (this.v.hasOwnProperty(id)) {
this.v[ id ].remove();
delete this.v[ id ];
}
}
}
}),
// CSS: write-only. Sets a collection of CSS styles to an element.
css: makeHandler(function($element, value) {
$element.css(value);
}),
// Disabled: write-only. Sets the 'disabled' status of a form element (true :: disabled).
disabled: makeHandler(function($element, value) {
$element.prop('disabled', !!value);
}),
// Enabled: write-only. Sets the 'disabled' status of a form element (true :: !disabled).
enabled: makeHandler(function($element, value) {
$element.prop('disabled', !value);
}),
// HTML: write-only. Sets the inner HTML value of an element.
html: makeHandler(function($element, value) {
$element.html(value);
}),
// Options: write-only. Sets option items to a <select> element, then updates the value.
options: makeHandler({
init: function($element, value, context, bindings) {
this.e = bindings.optionsEmpty;
this.d = bindings.optionsDefault;
this.v = bindings.value;
},
set: function($element, value) {
// Pre-compile empty and default option values:
// both values MUST be accessed, for two reasons:
// 1) we need to need to guarentee that both values are reached for mapping purposes.
// 2) we'll need their values anyway to determine their defined/undefined status.
var self = this;
var optionsEmpty = readAccessor(self.e);
var optionsDefault = readAccessor(self.d);
var currentValue = readAccessor(self.v);
var options = isCollection(value) ? value.models : value;
var numOptions = options.length;
var enabled = true;
var html = '';
// No options or default, and has an empty options placeholder:
// display placeholder and disable select menu.
if (!numOptions && !optionsDefault && optionsEmpty) {
html += self.opt(optionsEmpty, numOptions);
enabled = false;
} else {
// Try to populate default option and options list:
// Configure list with a default first option, if defined:
if (optionsDefault) {
options = [ optionsDefault ].concat(options);
}
// Create all option items:
_.each(options, function(option, index) {
html += self.opt(option, numOptions);
});
}
// Set new HTML to the element and toggle disabled status:
$element.html(html).prop('disabled', !enabled).val(currentValue);
// Pull revised value with new options selection state:
var revisedValue = $element.val();
// Test if the current value was successfully applied:
// if not, set the new selection state into the model.
if (self.v && !_.isEqual(currentValue, revisedValue)) {
self.v(revisedValue);
}
},
opt: function(option, numOptions) {
// Set both label and value as the raw option object by default:
var label = option;
var value = option;
var textAttr = bindingSettings.optionText;
var valueAttr = bindingSettings.optionValue;
// Dig deeper into label/value settings for non-primitive values:
if (isObject(option)) {
// Extract a label and value from each object:
// a model's 'get' method is used to access potential computed values.
label = isModel(option) ? option.get(textAttr) : option[ textAttr ];
value = isModel(option) ? option.get(valueAttr) : option[ valueAttr ];
}
return ['<option value="', value, '">', label, '</option>'].join('');
},
clean: function() {
this.d = this.e = this.v = 0;
}
}),
// Template: write-only. Renders the bound element with an Underscore template.
template: makeHandler({
init: function($element, value, context) {
var raw = $element.find('script,template');
this.t = _.template(raw.length ? raw.html() : $element.html());
// If an array of template attributes was provided,
// then replace array with a compiled hash of attribute accessors:
if (isArray(value)) {
return _.pick(context, value);
}
},
set: function($element, value) {
value = isModel(value) ? value.toJSON({computed:true}) : value;
$element.html(this.t(value));
},
clean: function() {
this.t = null;
}
}),
// Text: write-only. Sets the text value of an element.
text: makeHandler(function($element, value) {
$element.text(value);
}),
// Toggle: write-only. Toggles the visibility of an element.
toggle: makeHandler(function($element, value) {
$element.toggle(!!value);
}),
// Value: read-write. Gets and sets the value of a form element.
value: makeHandler({
get: function($element) {
return $element.val();
},
set: function($element, value) {
try {
if ($element.val() != value) $element.val(value);
} catch (error) {
// Error setting value: IGNORE.
// This occurs in IE6 while attempting to set an undefined multi-select option.
// unfortuantely, jQuery doesn't gracefully handle this error for us.
// remove this try/catch block when IE6 is officially deprecated.
}
}
})
};
// Binding Filters
// ---------------
// Filters are special binding handlers that may be invoked while binding;
// they will return a wrapper function used to modify how accessors are read.
// Partial application wrapper for creating binding filters:
function makeFilter(handler) {
return function() {
var params = arguments;
var read = isFunction(handler) ? handler : handler.get;
var write = handler.set;
return function(value) {
return isUndefined(value) ?
read.apply(this, _.map(params, readAccessor)) :
params[0]((write ? write : read).call(this, value));
};
};
}
var bindingFilters = {
// Positive collection assessment [read-only]:
// Tests if all of the provided accessors are truthy (and).
all: makeFilter(function() {
var params = arguments;
for (var i=0, len=params.length; i < len; i++) {
if (!params[i]) return false;
}
return true;
}),
// Partial collection assessment [read-only]:
// tests if any of the provided accessors are truthy (or).
any: makeFilter(function() {
var params = arguments;
for (var i=0, len=params.length; i < len; i++) {
if (params[i]) return true;
}
return false;
}),
// Collection length accessor [read-only]:
// assumes accessor value to be an Array or Collection; defaults to 0.
length: makeFilter(function(value) {
return value.length || 0;
}),
// Negative collection assessment [read-only]:
// tests if none of the provided accessors are truthy (and not).
none: makeFilter(function() {
var params = arguments;
for (var i=0, len=params.length; i < len; i++) {
if (params[i]) return false;
}
return true;
}),
// Negation [read-only]:
not: makeFilter(function(value) {
return !value;
}),
// Formats one or more accessors into a text string:
// ('$1 $2 did $3', firstName, lastName, action)
format: makeFilter(function(str) {
var params = arguments;
for (var i=1, len=params.length; i < len; i++) {
// TODO: need to make something like this work: (?<!\\)\$1
str = str.replace(new RegExp('\\$'+i, 'g'), params[i]);
}
return str;
}),
// Provides one of two values based on a ternary condition:
// uses first param (a) as condition, and returns either b (truthy) or c (falsey).
select: makeFilter(function(condition, truthy, falsey) {
return condition ? truthy : falsey;
}),
// CSV array formatting [read-write]:
csv: makeFilter({
get: function(value) {
value = String(value);
return value ? value.split(',') : [];
},
set: function(value) {
return isArray(value) ? value.join(',') : value;
}
}),
// Integer formatting [read-write]:
integer: makeFilter(function(value) {
return value ? parseInt(value, 10) : 0;
}),
// Float formatting [read-write]:
decimal: makeFilter(function(value) {
return value ? parseFloat(value) : 0;
})
};
// Define allowed binding parameters:
// These params may be included in binding handlers without throwing errors.
var allowedParams = {
events: 1,
optionsDefault: 1,
optionsEmpty: 1
};
// Define binding API:
Epoxy.binding = {
allowedParams: allowedParams,
addHandler: function(name, handler) {
bindingHandlers[ name ] = makeHandler(handler);
},
addFilter: function(name, handler) {
bindingFilters[ name ] = makeFilter(handler);
},
config: function(settings) {
_.extend(bindingSettings, settings);
},
emptyCache: function() {
bindingCache = {};
}
};
// Epoxy.View
// ----------
var viewMap;
var viewProps = ['viewModel', 'bindings', 'bindingFilters', 'bindingHandlers', 'bindingSources', 'computeds'];
Epoxy.View = Backbone.View.extend({
_super: Backbone.View,
// Backbone.View constructor override:
// sets up binding controls around call to super.
constructor: function(options) {
_.extend(this, _.pick(options||{}, viewProps));
_super(this, 'constructor', arguments);
this.applyBindings();
},
// Bindings list accessor:
b: function() {
return this._b || (this._b = []);
},
// Bindings definition:
// this setting defines a DOM attribute name used to query for bindings.
// Alternatively, this be replaced with a hash table of key/value pairs,
// where 'key' is a DOM query and 'value' is its binding declaration.
bindings: 'data-bind',
// Setter options:
// Defines an optional hashtable of options to be passed to setter operations.
// Accepts a custom option '{save:true}' that will write to the model via ".save()".