marionette-rails 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1013 +1,1065 @@
1
- // Backbone.Marionette v0.8.4
1
+ // Backbone.Marionette v0.9.0
2
2
  //
3
3
  // Copyright (C)2012 Derick Bailey, Muted Solutions, LLC
4
4
  // Distributed Under MIT License
5
5
  //
6
6
  // Documentation and Full License Available at:
7
7
  // http://github.com/derickbailey/backbone.marionette
8
+
8
9
  Backbone.Marionette = (function(Backbone, _, $){
9
10
  var Marionette = {};
10
11
 
11
- Marionette.version = "0.8.4";
12
-
13
- // Marionette.View
14
- // ---------------
15
-
16
- // The core view type that other Marionette views extend from.
17
- Marionette.View = Backbone.View.extend({
18
- // Get the template or template id/selector for this view
19
- // instance. You can set a `template` attribute in the view
20
- // definition or pass a `template: "whatever"` parameter in
21
- // to the constructor options. The `template` can also be
22
- // a function that returns a selector string.
23
- getTemplateSelector: function(){
24
- var template;
25
-
26
- // Get the template from `this.options.template` or
27
- // `this.template`. The `options` takes precedence.
28
- if (this.options && this.options.template){
29
- template = this.options.template;
30
- } else {
31
- template = this.template;
32
- }
33
-
34
- // check if it's a function and execute it, if it is
35
- if (_.isFunction(template)){
36
- template = template.call(this);
37
- }
38
-
39
- return template;
40
- },
41
-
42
- // Serialize the model or collection for the view. If a model is
43
- // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
44
- // is also called, but is used to populate an `items` array in the
45
- // resulting data. If both are found, defaults to the model.
46
- // You can override the `serializeData` method in your own view
47
- // definition, to provide custom serialization for your view's data.
48
- serializeData: function(){
49
- var data;
50
-
51
- if (this.model) {
52
- data = this.model.toJSON();
53
- }
54
- else if (this.collection) {
55
- data = { items: this.collection.toJSON() };
56
- }
12
+ // BindTo: Event Binding
13
+ // ---------------------
57
14
 
58
- data = this.mixinTemplateHelpers(data);
59
-
60
- return data;
61
- },
62
-
63
- // Mix in template helper methods. Looks for a
64
- // `templateHelpers` attribute, which can either be an
65
- // object literal, or a function that returns an object
66
- // literal. All methods and attributes from this object
67
- // are copies to the object passed in.
68
- mixinTemplateHelpers: function(target){
69
- target = target || {};
70
- var templateHelpers = this.templateHelpers;
71
- if (_.isFunction(templateHelpers)){
72
- templateHelpers = templateHelpers.call(this);
73
- }
74
- return _.extend(target, templateHelpers);
75
- },
15
+ // BindTo facilitates the binding and unbinding of events
16
+ // from objects that extend `Backbone.Events`. It makes
17
+ // unbinding events, even with anonymous callback functions,
18
+ // easy.
19
+ //
20
+ // Thanks to Johnny Oshika for this code.
21
+ // http://stackoverflow.com/questions/7567404/backbone-js-repopulate-or-recreate-the-view/7607853#7607853
76
22
 
77
- // Configure `triggers` to forward DOM events to view
78
- // events. `triggers: {"click .foo": "do:foo"}`
79
- configureTriggers: function(){
80
- if (!this.triggers) { return; }
23
+ Marionette.BindTo = {
81
24
 
82
- var triggers = this.triggers;
83
- var that = this;
84
- var triggerEvents = {};
25
+ // Store the event binding in array so it can be unbound
26
+ // easily, at a later point in time.
27
+ bindTo: function (obj, eventName, callback, context) {
28
+ context = context || this;
29
+ obj.on(eventName, callback, context);
85
30
 
86
- // Allow `triggers` to be configured as a function
87
- if (_.isFunction(triggers)){ triggers = triggers.call(this); }
31
+ if (!this.bindings) { this.bindings = []; }
88
32
 
89
- // Configure the triggers, prevent default
90
- // action and stop propagation of DOM events
91
- _.each(triggers, function(value, key){
33
+ var binding = {
34
+ obj: obj,
35
+ eventName: eventName,
36
+ callback: callback,
37
+ context: context
38
+ }
92
39
 
93
- triggerEvents[key] = function(e){
94
- if (e && e.preventDefault){ e.preventDefault(); }
95
- if (e && e.stopPropagation){ e.stopPropagation(); }
96
- that.trigger(value);
97
- }
40
+ this.bindings.push(binding);
98
41
 
99
- });
42
+ return binding;
43
+ },
100
44
 
101
- return triggerEvents;
102
- },
45
+ // Unbind from a single binding object. Binding objects are
46
+ // returned from the `bindTo` method call.
47
+ unbindFrom: function(binding){
48
+ binding.obj.off(binding.eventName, binding.callback, binding.context);
49
+ this.bindings = _.reject(this.bindings, function(bind){return bind === binding});
50
+ },
103
51
 
104
- delegateEvents: function(events){
105
- events = events || this.events;
106
- if (_.isFunction(events)){ events = events.call(this)}
52
+ // Unbind all of the events that we have stored.
53
+ unbindAll: function () {
54
+ var that = this;
107
55
 
108
- var combinedEvents = {};
109
- var triggers = this.configureTriggers();
110
- _.extend(combinedEvents, events, triggers);
56
+ // The `unbindFrom` call removes elements from the array
57
+ // while it is being iterated, so clone it first.
58
+ var bindings = _.map(this.bindings, _.identity);
59
+ _.each(bindings, function (binding, index) {
60
+ that.unbindFrom(binding);
61
+ });
62
+ }
63
+ };
64
+
65
+
66
+ // Marionette.View
67
+ // ---------------
68
+
69
+ // The core view type that other Marionette views extend from.
70
+ Marionette.View = Backbone.View.extend({
71
+ constructor: function(){
72
+ Backbone.View.prototype.constructor.apply(this, arguments);
73
+ this.bindTo(this, "show", this.onShowCalled, this);
74
+ },
75
+
76
+ // Get the template for this view
77
+ // instance. You can set a `template` attribute in the view
78
+ // definition or pass a `template: "whatever"` parameter in
79
+ // to the constructor options.
80
+ getTemplate: function(){
81
+ var template;
82
+
83
+ // Get the template from `this.options.template` or
84
+ // `this.template`. The `options` takes precedence.
85
+ if (this.options && this.options.template){
86
+ template = this.options.template;
87
+ } else {
88
+ template = this.template;
89
+ }
111
90
 
112
- Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
113
- },
91
+ return template;
92
+ },
114
93
 
115
- // Default `close` implementation, for removing a view from the
116
- // DOM and unbinding it. Regions will call this method
117
- // for you. You can specify an `onClose` method in your view to
118
- // add custom code that is called after the view is closed.
119
- close: function(){
120
- if (this.beforeClose) { this.beforeClose(); }
94
+ // Serialize the model or collection for the view. If a model is
95
+ // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
96
+ // is also called, but is used to populate an `items` array in the
97
+ // resulting data. If both are found, defaults to the model.
98
+ // You can override the `serializeData` method in your own view
99
+ // definition, to provide custom serialization for your view's data.
100
+ serializeData: function(){
101
+ var data;
121
102
 
122
- this.unbindAll();
123
- this.remove();
103
+ if (this.model) {
104
+ data = this.model.toJSON();
105
+ }
106
+ else if (this.collection) {
107
+ data = { items: this.collection.toJSON() };
108
+ }
124
109
 
125
- if (this.onClose) { this.onClose(); }
126
- this.trigger('close');
127
- this.unbind();
110
+ data = this.mixinTemplateHelpers(data);
111
+
112
+ return data;
113
+ },
114
+
115
+ // Mix in template helper methods. Looks for a
116
+ // `templateHelpers` attribute, which can either be an
117
+ // object literal, or a function that returns an object
118
+ // literal. All methods and attributes from this object
119
+ // are copies to the object passed in.
120
+ mixinTemplateHelpers: function(target){
121
+ target = target || {};
122
+ var templateHelpers = this.templateHelpers;
123
+ if (_.isFunction(templateHelpers)){
124
+ templateHelpers = templateHelpers.call(this);
128
125
  }
129
- });
126
+ return _.extend(target, templateHelpers);
127
+ },
130
128
 
131
- // Item View
132
- // ---------
133
-
134
- // A single item view implementation that contains code for rendering
135
- // with underscore.js templates, serializing the view's model or collection,
136
- // and calling several methods on extended views, such as `onRender`.
137
- Marionette.ItemView = Marionette.View.extend({
138
- constructor: function(){
139
- var args = slice.call(arguments);
140
- Marionette.View.prototype.constructor.apply(this, args);
129
+ // Configure `triggers` to forward DOM events to view
130
+ // events. `triggers: {"click .foo": "do:foo"}`
131
+ configureTriggers: function(){
132
+ if (!this.triggers) { return; }
133
+
134
+ var triggers = this.triggers;
135
+ var that = this;
136
+ var triggerEvents = {};
141
137
 
142
- _.bindAll(this, "render");
138
+ // Allow `triggers` to be configured as a function
139
+ if (_.isFunction(triggers)){ triggers = triggers.call(this); }
143
140
 
144
- this.initialEvents();
145
- },
141
+ // Configure the triggers, prevent default
142
+ // action and stop propagation of DOM events
143
+ _.each(triggers, function(value, key){
146
144
 
147
- // Configured the initial events that the item view
148
- // binds to. Override this method to prevent the initial
149
- // events, or to add your own initial events.
150
- initialEvents: function(){
151
- if (this.collection){
152
- this.bindTo(this.collection, "reset", this.render, this);
145
+ triggerEvents[key] = function(e){
146
+ if (e && e.preventDefault){ e.preventDefault(); }
147
+ if (e && e.stopPropagation){ e.stopPropagation(); }
148
+ that.trigger(value);
153
149
  }
154
- },
155
150
 
156
- // Render the view, defaulting to underscore.js templates.
157
- // You can override this in your view definition.
158
- render: function(){
159
- var that = this;
151
+ });
160
152
 
161
- var deferredRender = $.Deferred();
153
+ return triggerEvents;
154
+ },
162
155
 
163
- var beforeRenderDone = function() {
164
- that.trigger("before:render", that);
165
- that.trigger("item:before:render", that);
156
+ // Overriding Backbone.View's delegateEvents specifically
157
+ // to handle the `triggers` configuration
158
+ delegateEvents: function(events){
159
+ events = events || this.events;
160
+ if (_.isFunction(events)){ events = events.call(this)}
166
161
 
167
- var deferredData = that.serializeData();
168
- $.when(deferredData).then(dataSerialized);
169
- }
162
+ var combinedEvents = {};
163
+ var triggers = this.configureTriggers();
164
+ _.extend(combinedEvents, events, triggers);
170
165
 
171
- var dataSerialized = function(data){
172
- var asyncRender = that.renderHtml(data);
173
- $.when(asyncRender).then(templateRendered);
174
- }
166
+ Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
167
+ },
175
168
 
176
- var templateRendered = function(html){
177
- that.$el.html(html);
178
- callDeferredMethod(that.onRender, onRenderDone, that);
179
- }
169
+ // Internal method, handles the `show` event.
170
+ onShowCalled: function(){},
180
171
 
181
- var onRenderDone = function(){
182
- that.trigger("render", that);
183
- that.trigger("item:rendered", that);
172
+ // Default `close` implementation, for removing a view from the
173
+ // DOM and unbinding it. Regions will call this method
174
+ // for you. You can specify an `onClose` method in your view to
175
+ // add custom code that is called after the view is closed.
176
+ close: function(){
177
+ if (this.beforeClose) { this.beforeClose(); }
184
178
 
185
- deferredRender.resolve();
186
- }
179
+ this.remove();
187
180
 
188
- callDeferredMethod(this.beforeRender, beforeRenderDone, this);
189
-
190
- return deferredRender.promise();
191
- },
192
-
193
- // Render the data for this item view in to some HTML.
194
- // Override this method to replace the specific way in
195
- // which an item view has it's data rendered in to html.
196
- renderHtml: function(data) {
197
- var template = this.getTemplateSelector();
198
- return Marionette.Renderer.render(template, data);
199
- },
200
-
201
- // Override the default close event to add a few
202
- // more events that are triggered.
203
- close: function(){
204
- this.trigger('item:before:close');
205
- Marionette.View.prototype.close.apply(this, arguments);
206
- this.trigger('item:closed');
181
+ if (this.onClose) { this.onClose(); }
182
+ this.trigger('close');
183
+ this.unbindAll();
184
+ this.unbind();
185
+ }
186
+ });
187
+
188
+ // Copy the features of `BindTo`
189
+ _.extend(Marionette.View.prototype, Marionette.BindTo);
190
+
191
+ // Item View
192
+ // ---------
193
+
194
+ // A single item view implementation that contains code for rendering
195
+ // with underscore.js templates, serializing the view's model or collection,
196
+ // and calling several methods on extended views, such as `onRender`.
197
+ Marionette.ItemView = Marionette.View.extend({
198
+ constructor: function(){
199
+ Marionette.View.prototype.constructor.apply(this, arguments);
200
+ this.initialEvents();
201
+ },
202
+
203
+ // Configured the initial events that the item view
204
+ // binds to. Override this method to prevent the initial
205
+ // events, or to add your own initial events.
206
+ initialEvents: function(){
207
+ if (this.collection){
208
+ this.bindTo(this.collection, "reset", this.render, this);
207
209
  }
208
- });
209
-
210
- // Collection View
211
- // ---------------
212
-
213
- // A view that iterates over a Backbone.Collection
214
- // and renders an individual ItemView for each model.
215
- Marionette.CollectionView = Marionette.View.extend({
216
- constructor: function(){
217
- Marionette.View.prototype.constructor.apply(this, arguments);
218
-
219
- _.bindAll(this, "addItemView", "render");
220
- this.initialEvents();
221
- },
222
-
223
- // Configured the initial events that the collection view
224
- // binds to. Override this method to prevent the initial
225
- // events, or to add your own initial events.
226
- initialEvents: function(){
227
- if (this.collection){
228
- this.bindTo(this.collection, "add", this.addChildView, this);
229
- this.bindTo(this.collection, "remove", this.removeItemView, this);
230
- this.bindTo(this.collection, "reset", this.render, this);
231
- }
232
- },
233
-
234
- // Handle a child item added to the collection
235
- addChildView: function(item){
236
- var ItemView = this.getItemView();
237
- return this.addItemView(item, ItemView);
238
- },
239
-
240
- // Loop through all of the items and render
241
- // each of them with the specified `itemView`.
242
- render: function(){
243
- var that = this;
244
- var deferredRender = $.Deferred();
245
- var promises = [];
246
- var ItemView = this.getItemView();
247
-
248
- if (this.beforeRender) { this.beforeRender(); }
249
- this.trigger("collection:before:render", this);
250
-
251
- this.closeChildren();
252
-
253
- if (this.collection) {
254
- this.collection.each(function(item){
255
- var promise = that.addItemView(item, ItemView);
256
- promises.push(promise);
210
+ },
211
+
212
+ // Render the view, defaulting to underscore.js templates.
213
+ // You can override this in your view definition to provide
214
+ // a very specific rendering for your view. In general, though,
215
+ // you should override the `Marionette.Renderer` object to
216
+ // change how Marionette renders views.
217
+ render: function(){
218
+ if (this.beforeRender){ this.beforeRender(); }
219
+ this.trigger("before:render", this);
220
+ this.trigger("item:before:render", this);
221
+
222
+ var data = this.serializeData();
223
+ var template = this.getTemplate();
224
+ var html = Marionette.Renderer.render(template, data);
225
+ this.$el.html(html);
226
+
227
+ if (this.onRender){ this.onRender(); }
228
+ this.trigger("render", this);
229
+ this.trigger("item:rendered", this);
230
+ },
231
+
232
+ // Override the default close event to add a few
233
+ // more events that are triggered.
234
+ close: function(){
235
+ this.trigger('item:before:close');
236
+ Marionette.View.prototype.close.apply(this, arguments);
237
+ this.trigger('item:closed');
238
+ }
239
+ });
240
+
241
+ // Collection View
242
+ // ---------------
243
+
244
+ // A view that iterates over a Backbone.Collection
245
+ // and renders an individual ItemView for each model.
246
+ Marionette.CollectionView = Marionette.View.extend({
247
+ constructor: function(){
248
+ Marionette.View.prototype.constructor.apply(this, arguments);
249
+ this.initChildViewStorage();
250
+ this.initialEvents();
251
+ this.onShowCallbacks = new Marionette.Callbacks();
252
+ },
253
+
254
+ // Configured the initial events that the collection view
255
+ // binds to. Override this method to prevent the initial
256
+ // events, or to add your own initial events.
257
+ initialEvents: function(){
258
+ if (this.collection){
259
+ this.bindTo(this.collection, "add", this.addChildView, this);
260
+ this.bindTo(this.collection, "remove", this.removeItemView, this);
261
+ this.bindTo(this.collection, "reset", this.render, this);
262
+ }
263
+ },
264
+
265
+ // Handle a child item added to the collection
266
+ addChildView: function(item, collection, options){
267
+ var ItemView = this.getItemView();
268
+ return this.addItemView(item, ItemView, options.index);
269
+ },
270
+
271
+ // Override from `Marionette.View` to guarantee the `onShow` method
272
+ // of child views is called.
273
+ onShowCalled: function(){
274
+ this.onShowCallbacks.run();
275
+ },
276
+
277
+ // Loop through all of the items and render
278
+ // each of them with the specified `itemView`.
279
+ render: function(){
280
+ var that = this;
281
+ var ItemView = this.getItemView();
282
+ var EmptyView = this.options.emptyView || this.emptyView;
283
+
284
+ if (this.beforeRender) { this.beforeRender(); }
285
+ this.trigger("collection:before:render", this);
286
+
287
+ this.closeChildren();
288
+
289
+ if (this.collection) {
290
+ if (this.collection.length === 0 && EmptyView) {
291
+ this.addItemView(new Backbone.Model(), EmptyView, 0);
292
+ } else {
293
+ this.collection.each(function(item, index){
294
+ that.addItemView(item, ItemView, index);
257
295
  });
258
296
  }
297
+ }
259
298
 
260
- deferredRender.done(function(){
261
- if (this.onRender) { this.onRender(); }
262
- this.trigger("collection:rendered", this);
263
- });
299
+ if (this.onRender) { this.onRender(); }
300
+ this.trigger("collection:rendered", this);
301
+ },
264
302
 
265
- $.when.apply(this, promises).then(function(){
266
- deferredRender.resolveWith(that);
267
- });
303
+ // Retrieve the itemView type, either from `this.options.itemView`
304
+ // or from the `itemView` in the object definition. The "options"
305
+ // takes precedence.
306
+ getItemView: function(){
307
+ var itemView = this.options.itemView || this.itemView;
268
308
 
269
- return deferredRender.promise();
270
- },
309
+ if (!itemView){
310
+ var err = new Error("An `itemView` must be specified");
311
+ err.name = "NoItemViewError";
312
+ throw err;
313
+ }
271
314
 
272
- // Retrieve the itemView type, either from `this.options.itemView`
273
- // or from the `itemView` in the object definition. The "options"
274
- // takes precedence.
275
- getItemView: function(){
276
- var itemView = this.options.itemView || this.itemView;
315
+ return itemView;
316
+ },
277
317
 
278
- if (!itemView){
279
- var err = new Error("An `itemView` must be specified");
280
- err.name = "NoItemViewError";
281
- throw err;
282
- }
318
+ // Render the child item's view and add it to the
319
+ // HTML for the collection view.
320
+ addItemView: function(item, ItemView, index){
321
+ var that = this;
283
322
 
284
- return itemView;
285
- },
323
+ var view = this.buildItemView(item, ItemView);
286
324
 
287
- // Render the child item's view and add it to the
288
- // HTML for the collection view.
289
- addItemView: function(item, ItemView){
290
- var that = this;
325
+ // Store the child view itself so we can properly
326
+ // remove and/or close it later
327
+ this.storeChild(view);
328
+ if (this.onItemAdded){ this.onItemAdded(view); }
329
+ this.trigger("item:added", view);
291
330
 
292
- var view = this.buildItemView(item, ItemView);
293
- this.bindTo(view, "all", function(){
331
+ // Render it and show it
332
+ var renderResult = this.renderItemView(view, index);
294
333
 
295
- // get the args, prepend the event name
296
- // with "itemview:" and insert the child view
297
- // as the first event arg (after the event name)
298
- var args = slice.call(arguments);
299
- args[0] = "itemview:" + args[0];
300
- args.splice(1, 0, view);
334
+ // call onShow for child item views
335
+ if (view.onShow){
336
+ this.onShowCallbacks.add(view.onShow, view);
337
+ }
301
338
 
302
- that.trigger.apply(that, args);
303
- });
339
+ // Forward all child item view events through the parent,
340
+ // prepending "itemview:" to the event name
341
+ var childBinding = this.bindTo(view, "all", function(){
342
+ var args = slice.call(arguments);
343
+ args[0] = "itemview:" + args[0];
344
+ args.splice(1, 0, view);
304
345
 
305
- this.storeChild(view);
306
- this.trigger("item:added", view);
346
+ that.trigger.apply(that, args);
347
+ });
307
348
 
308
- var viewRendered = view.render();
309
- $.when(viewRendered).then(function(){
310
- that.appendHtml(that, view);
311
- });
312
-
313
- return viewRendered;
314
- },
315
-
316
- // Build an `itemView` for every model in the collection.
317
- buildItemView: function(item, ItemView){
318
- var view = new ItemView({
319
- model: item
320
- });
321
- return view;
322
- },
323
-
324
- // Remove the child view and close it
325
- removeItemView: function(item){
326
- var view = this.children[item.cid];
327
- if (view){
328
- view.close();
329
- delete this.children[item.cid];
330
- }
331
- this.trigger("item:removed", view);
332
- },
333
-
334
- // Append the HTML to the collection's `el`.
335
- // Override this method to do something other
336
- // then `.append`.
337
- appendHtml: function(collectionView, itemView){
338
- collectionView.$el.append(itemView.el);
339
- },
340
-
341
- // Store references to all of the child `itemView`
342
- // instances so they can be managed and cleaned up, later.
343
- storeChild: function(view){
344
- if (!this.children){
345
- this.children = {};
346
- }
347
- this.children[view.model.cid] = view;
348
- },
349
+ // Store all child event bindings so we can unbind
350
+ // them when removing / closing the child view
351
+ this.childBindings = this.childBindings || {};
352
+ this.childBindings[view.cid] = childBinding;
349
353
 
350
- // Handle cleanup and other closing needs for
351
- // the collection of views.
352
- close: function(){
353
- this.trigger("collection:before:close");
354
- this.closeChildren();
355
- Marionette.View.prototype.close.apply(this, arguments);
356
- this.trigger("collection:closed");
357
- },
358
-
359
- closeChildren: function(){
360
- if (this.children){
361
- _.each(this.children, function(childView){
362
- childView.close();
363
- });
354
+ return renderResult;
355
+ },
356
+
357
+ // render the item view
358
+ renderItemView: function(view, index) {
359
+ view.render();
360
+ this.appendHtml(this, view, index);
361
+ },
362
+
363
+ // Build an `itemView` for every model in the collection.
364
+ buildItemView: function(item, ItemView){
365
+ var itemViewOptions = _.result(this, "itemViewOptions");
366
+ var options = _.extend({model: item}, itemViewOptions);
367
+ var view = new ItemView(options);
368
+ return view;
369
+ },
370
+
371
+ // Remove the child view and close it
372
+ removeItemView: function(item){
373
+ var view = this.children[item.cid];
374
+ if (view){
375
+ var childBinding = this.childBindings[view.cid];
376
+ if (childBinding) {
377
+ this.unbindFrom(childBinding);
378
+ delete this.childBindings[view.cid];
364
379
  }
380
+ view.close();
381
+ delete this.children[item.cid];
365
382
  }
366
- });
367
-
368
- // Composite View
369
- // --------------
370
-
371
- // Used for rendering a branch-leaf, hierarchical structure.
372
- // Extends directly from CollectionView and also renders an
373
- // an item view as `modelView`, for the top leaf
374
- Marionette.CompositeView = Marionette.CollectionView.extend({
375
- constructor: function(options){
376
- Marionette.CollectionView.apply(this, arguments);
377
- this.itemView = this.getItemView();
378
- },
379
-
380
- // Retrieve the `itemView` to be used when rendering each of
381
- // the items in the collection. The default is to return
382
- // `this.itemView` or Marionette.CompositeView if no `itemView`
383
- // has been defined
384
- getItemView: function(){
385
- return this.itemView || this.constructor;
386
- },
387
-
388
- // Renders the model once, and the collection once. Calling
389
- // this again will tell the model's view to re-render itself
390
- // but the collection will not re-render.
391
- render: function(){
392
- var that = this;
393
- var compositeRendered = $.Deferred();
394
-
395
- var modelIsRendered = this.renderModel();
396
- $.when(modelIsRendered).then(function(html){
397
- that.$el.html(html);
398
- that.trigger("composite:model:rendered");
399
- that.trigger("render");
400
-
401
- var collectionIsRendered = that.renderCollection();
402
- $.when(collectionIsRendered).then(function(){
403
- compositeRendered.resolve();
404
- });
383
+ this.trigger("item:removed", view);
384
+ },
385
+
386
+ // Append the HTML to the collection's `el`.
387
+ // Override this method to do something other
388
+ // then `.append`.
389
+ appendHtml: function(collectionView, itemView, index){
390
+ collectionView.$el.append(itemView.el);
391
+ },
392
+
393
+ // Store references to all of the child `itemView`
394
+ // instances so they can be managed and cleaned up, later.
395
+ storeChild: function(view){
396
+ this.children[view.model.cid] = view;
397
+ },
398
+
399
+ // Internal method to set up the `children` object for
400
+ // storing all of the child views
401
+ initChildViewStorage: function(){
402
+ this.children = {};
403
+ },
404
+
405
+ // Handle cleanup and other closing needs for
406
+ // the collection of views.
407
+ close: function(){
408
+ this.trigger("collection:before:close");
409
+ this.closeChildren();
410
+ Marionette.View.prototype.close.apply(this, arguments);
411
+ this.trigger("collection:closed");
412
+ },
413
+
414
+ // Close the child views that this collection view
415
+ // is holding on to, if any
416
+ closeChildren: function(){
417
+ var that = this;
418
+ if (this.children){
419
+ _.each(_.clone(this.children), function(childView){
420
+ that.removeItemView(childView.model);
405
421
  });
422
+ }
423
+ }
424
+ });
425
+
426
+
427
+ // Composite View
428
+ // --------------
429
+
430
+ // Used for rendering a branch-leaf, hierarchical structure.
431
+ // Extends directly from CollectionView and also renders an
432
+ // an item view as `modelView`, for the top leaf
433
+ Marionette.CompositeView = Marionette.CollectionView.extend({
434
+ constructor: function(options){
435
+ Marionette.CollectionView.apply(this, arguments);
436
+ this.itemView = this.getItemView();
437
+ },
438
+
439
+ // Configured the initial events that the composite view
440
+ // binds to. Override this method to prevent the initial
441
+ // events, or to add your own initial events.
442
+ initialEvents: function(){
443
+ if (this.collection){
444
+ this.bindTo(this.collection, "add", this.addChildView, this);
445
+ this.bindTo(this.collection, "remove", this.removeItemView, this);
446
+ this.bindTo(this.collection, "reset", this.renderCollection, this);
447
+ }
448
+ },
449
+
450
+ // Retrieve the `itemView` to be used when rendering each of
451
+ // the items in the collection. The default is to return
452
+ // `this.itemView` or Marionette.CompositeView if no `itemView`
453
+ // has been defined
454
+ getItemView: function(){
455
+ return this.itemView || this.constructor;
456
+ },
457
+
458
+ // Renders the model once, and the collection once. Calling
459
+ // this again will tell the model's view to re-render itself
460
+ // but the collection will not re-render.
461
+ render: function(){
462
+ var that = this;
463
+
464
+ var html = this.renderModel();
465
+ this.$el.html(html);
466
+ this.trigger("composite:model:rendered");
467
+ this.trigger("render");
468
+
469
+ this.renderCollection();
470
+ this.trigger("composite:rendered");
471
+ },
472
+
473
+ // Render the collection for the composite view
474
+ renderCollection: function(){
475
+ Marionette.CollectionView.prototype.render.apply(this, arguments);
476
+ this.trigger("composite:collection:rendered");
477
+ },
478
+
479
+ // Render an individual model, if we have one, as
480
+ // part of a composite view (branch / leaf). For example:
481
+ // a treeview.
482
+ renderModel: function(){
483
+ var data = {};
484
+ data = this.serializeData();
485
+
486
+ var template = this.getTemplate();
487
+ return Marionette.Renderer.render(template, data);
488
+ }
489
+ });
406
490
 
407
- compositeRendered.done(function(){
408
- that.trigger("composite:rendered");
409
- });
410
491
 
411
- return compositeRendered.promise();
412
- },
492
+ // Region
493
+ // ------
413
494
 
414
- // Render the collection for the composite view
415
- renderCollection: function(){
416
- var collectionDeferred = Marionette.CollectionView.prototype.render.apply(this, arguments);
417
- collectionDeferred.done(function(){
418
- this.trigger("composite:collection:rendered");
419
- });
420
- return collectionDeferred.promise();
421
- },
422
-
423
- // Render an individual model, if we have one, as
424
- // part of a composite view (branch / leaf). For example:
425
- // a treeview.
426
- renderModel: function(){
427
- var data = {};
428
- data = this.serializeData();
429
-
430
- var template = this.getTemplateSelector();
431
- return Marionette.Renderer.render(template, data);
432
- }
433
- });
495
+ // Manage the visual regions of your composite application. See
496
+ // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
497
+ Marionette.Region = function(options){
498
+ this.options = options || {};
434
499
 
435
- // Region
436
- // ------
500
+ _.extend(this, options);
437
501
 
438
- // Manage the visual regions of your composite application. See
439
- // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
440
- Marionette.Region = function(options){
441
- this.options = options || {};
502
+ if (!this.el){
503
+ var err = new Error("An 'el' must be specified");
504
+ err.name = "NoElError";
505
+ throw err;
506
+ }
442
507
 
443
- _.extend(this, options);
508
+ if (this.initialize){
509
+ this.initialize.apply(this, arguments);
510
+ }
511
+ };
444
512
 
445
- if (!this.el){
446
- var err = new Error("An 'el' must be specified");
447
- err.name = "NoElError";
448
- throw err;
449
- }
513
+ _.extend(Marionette.Region.prototype, Backbone.Events, {
450
514
 
451
- if (this.initialize){
452
- this.initialize.apply(this, arguments);
453
- }
454
- };
515
+ // Displays a backbone view instance inside of the region.
516
+ // Handles calling the `render` method for you. Reads content
517
+ // directly from the `el` attribute. Also calls an optional
518
+ // `onShow` and `close` method on your view, just after showing
519
+ // or just before closing the view, respectively.
520
+ show: function(view){
521
+ var that = this;
455
522
 
456
- _.extend(Marionette.Region.prototype, Backbone.Events, {
523
+ this.ensureEl();
524
+ this.close();
457
525
 
458
- // Displays a backbone view instance inside of the region.
459
- // Handles calling the `render` method for you. Reads content
460
- // directly from the `el` attribute. Also calls an optional
461
- // `onShow` and `close` method on your view, just after showing
462
- // or just before closing the view, respectively.
463
- show: function(view, appendMethod){
464
- this.ensureEl();
526
+ view.render();
527
+ this.open(view);
465
528
 
466
- this.close();
467
- this.open(view, appendMethod);
529
+ if (view.onShow) { view.onShow(); }
530
+ view.trigger("show");
468
531
 
469
- this.currentView = view;
470
- },
532
+ if (this.onShow) { this.onShow(view); }
533
+ this.trigger("view:show", view);
471
534
 
472
- ensureEl: function(){
473
- if (!this.$el || this.$el.length === 0){
474
- this.$el = this.getEl(this.el);
475
- }
476
- },
477
-
478
- // Override this method to change how the region finds the
479
- // DOM element that it manages. Return a jQuery selector object.
480
- getEl: function(selector){
481
- return $(selector);
482
- },
483
-
484
- // Internal method to render and display a view. Not meant
485
- // to be called from any external code.
486
- open: function(view, appendMethod){
487
- var that = this;
488
- appendMethod = appendMethod || "html";
489
-
490
- $.when(view.render()).then(function () {
491
- that.$el[appendMethod](view.el);
492
- if (view.onShow) { view.onShow(); }
493
- if (that.onShow) { that.onShow(view); }
494
- view.trigger("show");
495
- that.trigger("view:show", view);
496
- });
497
- },
498
-
499
- // Close the current view, if there is one. If there is no
500
- // current view, it does nothing and returns immediately.
501
- close: function(){
502
- var view = this.currentView;
503
- if (!view){ return; }
504
-
505
- if (view.close) { view.close(); }
506
- this.trigger("view:closed", view);
507
-
508
- delete this.currentView;
509
- },
510
-
511
- // Attach an existing view to the region. This
512
- // will not call `render` or `onShow` for the new view,
513
- // and will not replace the current HTML for the `el`
514
- // of the region.
515
- attachView: function(view){
516
- this.currentView = view;
535
+ this.currentView = view;
536
+ },
537
+
538
+ ensureEl: function(){
539
+ if (!this.$el || this.$el.length === 0){
540
+ this.$el = this.getEl(this.el);
517
541
  }
518
- });
542
+ },
543
+
544
+ // Override this method to change how the region finds the
545
+ // DOM element that it manages. Return a jQuery selector object.
546
+ getEl: function(selector){
547
+ return $(selector);
548
+ },
549
+
550
+ // Override this method to change how the new view is
551
+ // appended to the `$el` that the region is managing
552
+ open: function(view){
553
+ this.$el.html(view.el);
554
+ },
555
+
556
+ // Close the current view, if there is one. If there is no
557
+ // current view, it does nothing and returns immediately.
558
+ close: function(){
559
+ var view = this.currentView;
560
+ if (!view){ return; }
561
+
562
+ if (view.close) { view.close(); }
563
+ this.trigger("view:closed", view);
564
+
565
+ delete this.currentView;
566
+ },
567
+
568
+ // Attach an existing view to the region. This
569
+ // will not call `render` or `onShow` for the new view,
570
+ // and will not replace the current HTML for the `el`
571
+ // of the region.
572
+ attachView: function(view){
573
+ this.currentView = view;
574
+ }
575
+ });
519
576
 
520
- // Layout
521
- // ------
577
+ // Copy the `extend` function used by Backbone's classes
578
+ Marionette.Region.extend = Backbone.View.extend;
522
579
 
523
- // Formerly known as Composite Region.
524
- //
525
- // Used for managing application layouts, nested layouts and
526
- // multiple regions within an application or sub-application.
527
- //
528
- // A specialized view type that renders an area of HTML and then
529
- // attaches `Region` instances to the specified `regions`.
530
- // Used for composite view management and sub-application areas.
531
- Marionette.Layout = Marionette.ItemView.extend({
532
- constructor: function () {
533
- this.vent = new Backbone.Marionette.EventAggregator();
534
- Backbone.Marionette.ItemView.apply(this, arguments);
535
- this.regionManagers = {};
536
- },
580
+ // Copy the features of `BindTo`
581
+ _.extend(Marionette.Region.prototype, Marionette.BindTo);
537
582
 
538
- render: function () {
539
- this.initializeRegions();
540
- return Backbone.Marionette.ItemView.prototype.render.call(this, arguments);
541
- },
583
+ // Layout
584
+ // ------
542
585
 
543
- close: function () {
586
+ // Used for managing application layouts, nested layouts and
587
+ // multiple regions within an application or sub-application.
588
+ //
589
+ // A specialized view type that renders an area of HTML and then
590
+ // attaches `Region` instances to the specified `regions`.
591
+ // Used for composite view management and sub-application areas.
592
+ Marionette.Layout = Marionette.ItemView.extend({
593
+ constructor: function () {
594
+ Backbone.Marionette.ItemView.apply(this, arguments);
595
+ this.initializeRegions();
596
+ },
597
+
598
+ // Layout's render will use the existing region objects the
599
+ // first time it is called. Subsequent calls will close the
600
+ // views that the regions are showing and then reset the `el`
601
+ // for the regions to the newly rendered DOM elements.
602
+ render: function(){
603
+ var result = Marionette.ItemView.prototype.render.apply(this, arguments);
604
+
605
+ // Rewrite this function to handle re-rendering and
606
+ // re-initializing the `el` for each region
607
+ this.render = function(){
544
608
  this.closeRegions();
545
- Backbone.Marionette.ItemView.prototype.close.call(this, arguments);
546
- },
547
-
548
- // Initialize the regions that have been defined in a
549
- // `regions` attribute on this layout. The key of the
550
- // hash becomes an attribute on the layout object directly.
551
- // For example: `regions: { menu: ".menu-container" }`
552
- // will product a `layout.menu` object which is a region
553
- // that controls the `.menu-container` DOM element.
554
- initializeRegions: function () {
555
- var that = this;
556
- _.each(this.regions, function (selector, name) {
557
- var regionManager = new Backbone.Marionette.Region({
558
- el: selector,
559
-
560
- getEl: function(selector){
561
- return that.$(selector);
562
- }
563
- });
564
- that.regionManagers[name] = regionManager;
565
- that[name] = regionManager;
566
- });
567
- },
568
-
569
- // Close all of the regions that have been opened by
570
- // this layout. This method is called when the layout
571
- // itself is closed.
572
- closeRegions: function () {
573
- var that = this;
574
- _.each(this.regionManagers, function (manager, name) {
575
- manager.close();
576
- delete that[name];
577
- });
578
- this.regionManagers = {};
609
+ this.reInitializeRegions();
610
+
611
+ var result = Marionette.ItemView.prototype.render.apply(this, arguments);
612
+ return result;
579
613
  }
580
- });
581
614
 
582
- // AppRouter
583
- // ---------
615
+ return result;
616
+ },
617
+
618
+ // Handle closing regions, and then close the view itself.
619
+ close: function () {
620
+ this.closeRegions();
621
+ this.destroyRegions();
622
+ Backbone.Marionette.ItemView.prototype.close.call(this, arguments);
623
+ },
624
+
625
+ // Initialize the regions that have been defined in a
626
+ // `regions` attribute on this layout. The key of the
627
+ // hash becomes an attribute on the layout object directly.
628
+ // For example: `regions: { menu: ".menu-container" }`
629
+ // will product a `layout.menu` object which is a region
630
+ // that controls the `.menu-container` DOM element.
631
+ initializeRegions: function () {
632
+ if (!this.regionManagers){
633
+ this.regionManagers = {};
634
+ }
584
635
 
585
- // Reduce the boilerplate code of handling route events
586
- // and then calling a single method on another object.
587
- // Have your routers configured to call the method on
588
- // your object, directly.
589
- //
590
- // Configure an AppRouter with `appRoutes`.
591
- //
592
- // App routers can only take one `controller` object.
593
- // It is recommended that you divide your controller
594
- // objects in to smaller peices of related functionality
595
- // and have multiple routers / controllers, instead of
596
- // just one giant router and controller.
597
- //
598
- // You can also add standard routes to an AppRouter.
599
-
600
- Marionette.AppRouter = Backbone.Router.extend({
636
+ var that = this;
637
+ _.each(this.regions, function (selector, name) {
601
638
 
602
- constructor: function(options){
603
- Backbone.Router.prototype.constructor.call(this, options);
639
+ var regionManager = new Backbone.Marionette.Region({
640
+ el: selector,
641
+ getEl: function(selector){
642
+ return that.$(selector);
643
+ }
644
+ });
604
645
 
605
- if (this.appRoutes){
606
- var controller = this.controller;
607
- if (options && options.controller) {
608
- controller = options.controller;
646
+ that.regionManagers[name] = regionManager;
647
+ that[name] = regionManager;
648
+ });
649
+
650
+ },
651
+
652
+ // Re-initialize all of the regions by updating the `el` that
653
+ // they point to
654
+ reInitializeRegions: function(){
655
+ _.each(this.regionManagers, function(region){
656
+ delete region.$el;
657
+ });
658
+ },
659
+
660
+ // Close all of the regions that have been opened by
661
+ // this layout. This method is called when the layout
662
+ // itself is closed.
663
+ closeRegions: function () {
664
+ var that = this;
665
+ _.each(this.regionManagers, function (manager, name) {
666
+ manager.close();
667
+ });
668
+ },
669
+
670
+ // Destroys all of the regions by removing references
671
+ // from the Layout
672
+ destroyRegions: function(){
673
+ var that = this;
674
+ _.each(this.regionManagers, function (manager, name) {
675
+ delete that[name];
676
+ });
677
+ this.regionManagers = {};
678
+ }
679
+ });
680
+
681
+
682
+ // Application
683
+ // -----------
684
+
685
+ // Contain and manage the composite application as a whole.
686
+ // Stores and starts up `Region` objects, includes an
687
+ // event aggregator as `app.vent`
688
+ Marionette.Application = function(options){
689
+ this.initCallbacks = new Marionette.Callbacks();
690
+ this.vent = new Marionette.EventAggregator();
691
+ _.extend(this, options);
692
+ };
693
+
694
+ _.extend(Marionette.Application.prototype, Backbone.Events, {
695
+ // Add an initializer that is either run at when the `start`
696
+ // method is called, or run immediately if added after `start`
697
+ // has already been called.
698
+ addInitializer: function(initializer){
699
+ this.initCallbacks.add(initializer);
700
+ },
701
+
702
+ // kick off all of the application's processes.
703
+ // initializes all of the regions that have been added
704
+ // to the app, and runs all of the initializer functions
705
+ start: function(options){
706
+ this.trigger("initialize:before", options);
707
+ this.initCallbacks.run(options, this);
708
+ this.trigger("initialize:after", options);
709
+
710
+ this.trigger("start", options);
711
+ },
712
+
713
+ // Add regions to your app.
714
+ // Accepts a hash of named strings or Region objects
715
+ // addRegions({something: "#someRegion"})
716
+ // addRegions{{something: Region.extend({el: "#someRegion"}) });
717
+ addRegions: function(regions){
718
+ var regionValue, regionObj, region;
719
+
720
+ for(region in regions){
721
+ if (regions.hasOwnProperty(region)){
722
+ regionValue = regions[region];
723
+
724
+ if (typeof regionValue === "string"){
725
+ regionObj = new Marionette.Region({
726
+ el: regionValue
727
+ });
728
+ } else {
729
+ regionObj = new regionValue();
609
730
  }
610
- this.processAppRoutes(controller, this.appRoutes);
731
+
732
+ this[region] = regionObj;
611
733
  }
612
- },
734
+ }
735
+ },
613
736
 
614
- processAppRoutes: function(controller, appRoutes){
615
- var method, methodName;
616
- var route, routesLength, i;
617
- var routes = [];
618
- var router = this;
737
+ // Create a module, attached to the application
738
+ module: function(){
739
+ // see the Marionette.Module object for more information
740
+ return Marionette.Module.create.apply(this, arguments);
741
+ }
742
+ });
619
743
 
620
- for(route in appRoutes){
621
- if (appRoutes.hasOwnProperty(route)){
622
- routes.unshift([route, appRoutes[route]]);
623
- }
624
- }
744
+ // Copy the `extend` function used by Backbone's classes
745
+ Marionette.Application.extend = Backbone.View.extend;
625
746
 
626
- routesLength = routes.length;
627
- for (i = 0; i < routesLength; i++){
628
- route = routes[i][0];
629
- methodName = routes[i][1];
630
- method = controller[methodName];
631
-
632
- if (!method){
633
- var msg = "Method '" + methodName + "' was not found on the controller";
634
- var err = new Error(msg);
635
- err.name = "NoMethodError";
636
- throw err;
637
- }
747
+ // Copy the features of `BindTo`
748
+ _.extend(Marionette.Application.prototype, Marionette.BindTo);
749
+
750
+ // AppRouter
751
+ // ---------
752
+
753
+ // Reduce the boilerplate code of handling route events
754
+ // and then calling a single method on another object.
755
+ // Have your routers configured to call the method on
756
+ // your object, directly.
757
+ //
758
+ // Configure an AppRouter with `appRoutes`.
759
+ //
760
+ // App routers can only take one `controller` object.
761
+ // It is recommended that you divide your controller
762
+ // objects in to smaller peices of related functionality
763
+ // and have multiple routers / controllers, instead of
764
+ // just one giant router and controller.
765
+ //
766
+ // You can also add standard routes to an AppRouter.
767
+
768
+ Marionette.AppRouter = Backbone.Router.extend({
638
769
 
639
- method = _.bind(method, controller);
640
- router.route(route, methodName, method);
770
+ constructor: function(options){
771
+ Backbone.Router.prototype.constructor.call(this, options);
772
+
773
+ if (this.appRoutes){
774
+ var controller = this.controller;
775
+ if (options && options.controller) {
776
+ controller = options.controller;
641
777
  }
778
+ this.processAppRoutes(controller, this.appRoutes);
642
779
  }
643
- });
644
-
645
- // Composite Application
646
- // ---------------------
647
-
648
- // Contain and manage the composite application as a whole.
649
- // Stores and starts up `Region` objects, includes an
650
- // event aggregator as `app.vent`
651
- Marionette.Application = function(options){
652
- this.initCallbacks = new Marionette.Callbacks();
653
- this.vent = new Marionette.EventAggregator();
654
- _.extend(this, options);
655
- };
656
-
657
- _.extend(Marionette.Application.prototype, Backbone.Events, {
658
- // Add an initializer that is either run at when the `start`
659
- // method is called, or run immediately if added after `start`
660
- // has already been called.
661
- addInitializer: function(initializer){
662
- this.initCallbacks.add(initializer);
663
- },
664
-
665
- // kick off all of the application's processes.
666
- // initializes all of the regions that have been added
667
- // to the app, and runs all of the initializer functions
668
- start: function(options){
669
- this.trigger("initialize:before", options);
670
- this.initCallbacks.run(this, options);
671
- this.trigger("initialize:after", options);
672
-
673
- this.trigger("start", options);
674
- },
675
-
676
- // Add regions to your app.
677
- // Accepts a hash of named strings or Region objects
678
- // addRegions({something: "#someRegion"})
679
- // addRegions{{something: Region.extend({el: "#someRegion"}) });
680
- addRegions: function(regions){
681
- var regionValue, regionObj, region;
682
-
683
- for(region in regions){
684
- if (regions.hasOwnProperty(region)){
685
- regionValue = regions[region];
686
-
687
- if (typeof regionValue === "string"){
688
- regionObj = new Marionette.Region({
689
- el: regionValue
690
- });
691
- } else {
692
- regionObj = new regionValue();
693
- }
694
-
695
- this[region] = regionObj;
696
- }
780
+ },
781
+
782
+ // Internal method to process the `appRoutes` for the
783
+ // router, and turn them in to routes that trigger the
784
+ // specified method on the specified `controller`.
785
+ processAppRoutes: function(controller, appRoutes){
786
+ var method, methodName;
787
+ var route, routesLength, i;
788
+ var routes = [];
789
+ var router = this;
790
+
791
+ for(route in appRoutes){
792
+ if (appRoutes.hasOwnProperty(route)){
793
+ routes.unshift([route, appRoutes[route]]);
697
794
  }
698
795
  }
699
- });
700
796
 
701
- // BindTo: Event Binding
702
- // ---------------------
703
-
704
- // BindTo facilitates the binding and unbinding of events
705
- // from objects that extend `Backbone.Events`. It makes
706
- // unbinding events, even with anonymous callback functions,
707
- // easy.
708
- //
709
- // Thanks to Johnny Oshika for this code.
710
- // http://stackoverflow.com/questions/7567404/backbone-js-repopulate-or-recreate-the-view/7607853#7607853
711
- Marionette.BindTo = {
712
- // Store the event binding in array so it can be unbound
713
- // easily, at a later point in time.
714
- bindTo: function (obj, eventName, callback, context) {
715
- context = context || this;
716
- obj.on(eventName, callback, context);
717
-
718
- if (!this.bindings) { this.bindings = []; }
719
-
720
- var binding = {
721
- obj: obj,
722
- eventName: eventName,
723
- callback: callback,
724
- context: context
797
+ routesLength = routes.length;
798
+ for (i = 0; i < routesLength; i++){
799
+ route = routes[i][0];
800
+ methodName = routes[i][1];
801
+ method = controller[methodName];
802
+
803
+ if (!method){
804
+ var msg = "Method '" + methodName + "' was not found on the controller";
805
+ var err = new Error(msg);
806
+ err.name = "NoMethodError";
807
+ throw err;
725
808
  }
726
809
 
727
- this.bindings.push(binding);
810
+ method = _.bind(method, controller);
811
+ router.route(route, methodName, method);
812
+ }
813
+ }
814
+ });
728
815
 
729
- return binding;
730
- },
731
816
 
732
- // Unbind from a single binding object. Binding objects are
733
- // returned from the `bindTo` method call.
734
- unbindFrom: function(binding){
735
- binding.obj.off(binding.eventName, binding.callback);
736
- this.bindings = _.reject(this.bindings, function(bind){return bind === binding});
737
- },
817
+ // Module
818
+ // ------
738
819
 
739
- // Unbind all of the events that we have stored.
740
- unbindAll: function () {
741
- var that = this;
820
+ // A simple module system, used to create privacy and encapsulation in
821
+ // Marionette applications
822
+ Marionette.Module = function(){};
742
823
 
743
- // The `unbindFrom` call removes elements from the array
744
- // while it is being iterated, so clone it first.
745
- var bindings = _.map(this.bindings, _.identity);
746
- _.each(bindings, function (binding, index) {
747
- that.unbindFrom(binding);
748
- });
749
- }
750
- };
824
+ // Extend the Module prototype with events / bindTo, so that the module
825
+ // can be used as an event aggregator or pub/sub.
826
+ _.extend(Marionette.Module.prototype, Backbone.Events, Marionette.BindTo);
751
827
 
752
- // Callbacks
753
- // ---------
828
+ // Function level methods to create modules
829
+ _.extend(Marionette.Module, {
754
830
 
755
- // A simple way of managing a collection of callbacks
756
- // and executing them at a later point in time, using jQuery's
757
- // `Deferred` object.
758
- Marionette.Callbacks = function(){
759
- this.deferred = $.Deferred();
760
- this.promise = this.deferred.promise();
761
- };
831
+ // Create a module, hanging off 'this' as the parent object. This
832
+ // method must be called with .apply or .create
833
+ create: function(moduleNames, moduleDefinition){
834
+ var moduleName, module, moduleOverride;
835
+ var parentObject = this;
836
+ var parentModule = this;
837
+ var moduleNames = moduleNames.split(".");
762
838
 
763
- _.extend(Marionette.Callbacks.prototype, {
764
-
765
- // Add a callback to be executed. Callbacks added here are
766
- // guaranteed to execute, even if they are added after the
767
- // `run` method is called.
768
- add: function(callback){
769
- this.promise.done(function(context, options){
770
- callback.call(context, options);
771
- });
772
- },
839
+ // Loop through all the parts of the module definition
840
+ var length = moduleNames.length;
841
+ for(var i = 0; i < length; i++){
842
+ var isLastModuleInChain = (i === length-1);
773
843
 
774
- // Run all registered callbacks with the context specified.
775
- // Additional callbacks can be added after this has been run
776
- // and they will still be executed.
777
- run: function(context, options){
778
- this.deferred.resolve(context, options);
779
- }
780
- });
781
-
782
- // Event Aggregator
783
- // ----------------
784
-
785
- // A pub-sub object that can be used to decouple various parts
786
- // of an application through event-driven architecture.
787
- Marionette.EventAggregator = function(options){
788
- _.extend(this, options);
789
- };
790
-
791
- _.extend(Marionette.EventAggregator.prototype, Backbone.Events, Marionette.BindTo, {
792
- // Assumes the event aggregator itself is the
793
- // object being bound to.
794
- bindTo: function(eventName, callback, context){
795
- return Marionette.BindTo.bindTo.call(this, this, eventName, callback, context);
796
- }
797
- });
844
+ // Get the module name, and check if it exists on
845
+ // the current parent already
846
+ moduleName = moduleNames[i];
847
+ module = parentModule[moduleName];
798
848
 
799
- // Template Cache
800
- // --------------
801
-
802
- // Manage templates stored in `<script>` blocks,
803
- // caching them for faster access.
804
- Marionette.TemplateCache = {
805
- templates: {},
806
- loaders: {},
807
-
808
- // Get the specified template by id. Either
809
- // retrieves the cached version, or loads it
810
- // from the DOM.
811
- get: function(templateId){
812
- var that = this;
813
- var templateRetrieval = $.Deferred();
814
- var cachedTemplate = this.templates[templateId];
815
-
816
- if (cachedTemplate){
817
- templateRetrieval.resolve(cachedTemplate);
818
- } else {
819
- var loader = this.loaders[templateId];
820
- if(loader) {
821
- templateRetrieval = loader;
822
- } else {
823
- this.loaders[templateId] = templateRetrieval;
849
+ // Create a new module if we don't have one already
850
+ if (!module){
851
+ module = new Marionette.Module();
852
+ }
824
853
 
825
- this.loadTemplate(templateId, function(template){
826
- delete that.loaders[templateId];
827
- that.templates[templateId] = template;
828
- templateRetrieval.resolve(template);
829
- });
830
- }
854
+ // Check to see if we need to run the definition
855
+ // for the module. Only run the definition if one
856
+ // is supplied, and if we're at the last segment
857
+ // of the "Module.Name" chain.
858
+ if (isLastModuleInChain && moduleDefinition){
859
+ // get the custom args passed in after the module definition and
860
+ // get rid of the module name and definition function
861
+ var customArgs = slice.apply(arguments);
862
+ customArgs.shift();
863
+ customArgs.shift();
864
+
865
+ // final arguments list for the module definition
866
+ var argsArray = [module, parentObject, Backbone, Marionette, jQuery, _, customArgs];
867
+
868
+ // flatten the nested array
869
+ var args = _.flatten(argsArray);
870
+
871
+ // ensure the module definition's `this` is the module itself
872
+ moduleDefinition.apply(module, args);
873
+ }
831
874
 
875
+ // If the defined module is not what we are
876
+ // currently storing as the module, replace it
877
+ if (parentModule[moduleName] !== module){
878
+ parentModule[moduleName] = module;
832
879
  }
833
880
 
834
- return templateRetrieval.promise();
835
- },
881
+ // Reset the parent module so that the next child
882
+ // in the list will be added to the correct parent
883
+ parentModule = module;
884
+ }
836
885
 
837
- // Load a template from the DOM, by default. Override
838
- // this method to provide your own template retrieval,
839
- // such as asynchronous loading from a server.
840
- loadTemplate: function(templateId, callback){
841
- var template = $(templateId).html();
886
+ // Return the last module in the definition chain
887
+ return module;
888
+ }
889
+ });
890
+
891
+ // Template Cache
892
+ // --------------
893
+
894
+ // Manage templates stored in `<script>` blocks,
895
+ // caching them for faster access.
896
+ Marionette.TemplateCache = function(templateId){
897
+ this.templateId = templateId;
898
+ };
899
+
900
+ // TemplateCache object-level methods. Manage the template
901
+ // caches from these method calls instead of creating
902
+ // your own TemplateCache instances
903
+ _.extend(Marionette.TemplateCache, {
904
+ templateCaches: {},
905
+
906
+ // Get the specified template by id. Either
907
+ // retrieves the cached version, or loads it
908
+ // from the DOM.
909
+ get: function(templateId){
910
+ var that = this;
911
+ var cachedTemplate = this.templateCaches[templateId];
912
+
913
+ if (!cachedTemplate){
914
+ cachedTemplate = new Marionette.TemplateCache(templateId);
915
+ this.templateCaches[templateId] = cachedTemplate;
916
+ }
842
917
 
843
- // Make sure we have a template before trying to compile it
844
- if (!template || template.length === 0){
845
- var msg = "Could not find template: '" + templateId + "'";
846
- var err = new Error(msg);
847
- err.name = "NoTemplateError";
848
- throw err;
849
- }
918
+ return cachedTemplate.load();
919
+ },
850
920
 
851
- template = this.compileTemplate(template);
852
-
853
- callback.call(this, template);
854
- },
855
-
856
- // Pre-compile the template before caching it. Override
857
- // this method if you do not need to pre-compile a template
858
- // (JST / RequireJS for example) or if you want to change
859
- // the template engine used (Handebars, etc).
860
- compileTemplate: function(rawTemplate){
861
- return _.template(rawTemplate);
862
- },
863
-
864
- // Clear templates from the cache. If no arguments
865
- // are specified, clears all templates:
866
- // `clear()`
867
- //
868
- // If arguments are specified, clears each of the
869
- // specified templates from the cache:
870
- // `clear("#t1", "#t2", "...")`
871
- clear: function(){
872
- var i;
873
- var length = arguments.length;
874
-
875
- if (length > 0){
876
- for(i=0; i<length; i++){
877
- delete this.templates[arguments[i]];
878
- }
879
- } else {
880
- this.templates = {};
921
+ // Clear templates from the cache. If no arguments
922
+ // are specified, clears all templates:
923
+ // `clear()`
924
+ //
925
+ // If arguments are specified, clears each of the
926
+ // specified templates from the cache:
927
+ // `clear("#t1", "#t2", "...")`
928
+ clear: function(){
929
+ var i;
930
+ var length = arguments.length;
931
+
932
+ if (length > 0){
933
+ for(i=0; i<length; i++){
934
+ delete this.templateCaches[arguments[i]];
881
935
  }
936
+ } else {
937
+ this.templateCaches = {};
882
938
  }
883
- };
939
+ }
940
+ });
884
941
 
885
- // Renderer
886
- // --------
887
-
888
- // Render a template with data by passing in the template
889
- // selector and the data to render.
890
- Marionette.Renderer = {
891
-
892
- // Render a template with data. The `template` parameter is
893
- // passed to the `TemplateCache` object to retrieve the
894
- // actual template. Override this method to provide your own
895
- // custom rendering and template handling for all of Marionette.
896
- render: function(template, data){
897
- var that = this;
898
- var asyncRender = $.Deferred();
899
-
900
- var templateRetrieval = Marionette.TemplateCache.get(template);
901
-
902
- $.when(templateRetrieval).then(function(template){
903
- var html = that.renderTemplate(template, data);
904
- asyncRender.resolve(html);
905
- });
942
+ // TemplateCache instance methods, allowing each
943
+ // template cache object to manage it's own state
944
+ // and know whether or not it has been loaded
945
+ _.extend(Marionette.TemplateCache.prototype, {
906
946
 
907
- return asyncRender.promise();
908
- },
947
+ // Internal method to load the template asynchronously.
948
+ load: function(){
949
+ var that = this;
909
950
 
910
- // Default implementation uses underscore.js templates. Override
911
- // this method to use your own templating engine.
912
- renderTemplate: function(template, data){
913
- var html = template(data);
914
- return html;
951
+ // Guard clause to prevent loading this template more than once
952
+ if (this.compiledTemplate){
953
+ return this.compiledTemplate;
915
954
  }
916
955
 
917
- };
918
-
919
- // Modules
920
- // -------
921
-
922
- // The "Modules" object builds modules on an
923
- // object that it is attached to. It should not be
924
- // used on it's own, but should be attached to
925
- // another object that will define modules.
926
- Marionette.Modules = {
927
-
928
- // Add modules to the application, providing direct
929
- // access to your applicaiton object, Backbone,
930
- // Marionette, jQuery and Underscore as parameters
931
- // to a callback function.
932
- module: function(moduleNames, moduleDefinition){
933
- var moduleName, module, moduleOverride;
934
- var parentModule = this;
935
- var parentApp = this;
936
- var moduleNames = moduleNames.split(".");
937
-
938
- // Loop through all the parts of the module definition
939
- var length = moduleNames.length;
940
- for(var i = 0; i < length; i++){
941
- var isLastModuleInChain = (i === length-1);
942
-
943
- // Get the module name, and check if it exists on
944
- // the current parent already
945
- moduleName = moduleNames[i];
946
- module = parentModule[moduleName];
947
-
948
- // Create a new module if we don't have one already
949
- if (!module){
950
- module = new Marionette.Application();
951
- }
952
-
953
- // Check to see if we need to run the definition
954
- // for the module. Only run the definition if one
955
- // is supplied, and if we're at the last segment
956
- // of the "Module.Name" chain.
957
- if (isLastModuleInChain && moduleDefinition){
958
- moduleOverride = moduleDefinition(module, parentApp, Backbone, Marionette, jQuery, _);
959
- // If we have a module override, use it instead.
960
- if (moduleOverride){
961
- module = moduleOverride;
962
- }
963
- }
956
+ // Load the template and compile it
957
+ var template = this.loadTemplate(this.templateId);
958
+ this.compiledTemplate = this.compileTemplate(template);
964
959
 
965
- // If the defined module is not what we are
966
- // currently storing as the module, replace it
967
- if (parentModule[moduleName] !== module){
968
- parentModule[moduleName] = module;
969
- }
960
+ return this.compiledTemplate;
961
+ },
970
962
 
971
- // Reset the parent module so that the next child
972
- // in the list will be added to the correct parent
973
- parentModule = module;
974
- }
963
+ // Load a template from the DOM, by default. Override
964
+ // this method to provide your own template retrieval,
965
+ // such as asynchronous loading from a server.
966
+ loadTemplate: function(templateId){
967
+ var template = $(templateId).html();
975
968
 
976
- // Return the last module in the definition chain
977
- return module;
969
+ if (!template || template.length === 0){
970
+ var msg = "Could not find template: '" + templateId + "'";
971
+ var err = new Error(msg);
972
+ err.name = "NoTemplateError";
973
+ throw err;
978
974
  }
979
- };
980
975
 
981
- // Helpers
982
- // -------
976
+ return template;
977
+ },
983
978
 
984
- // For slicing `arguments` in functions
985
- var slice = Array.prototype.slice;
986
-
987
- // Copy the `extend` function used by Backbone's classes
988
- var extend = Marionette.View.extend;
989
- Marionette.Region.extend = extend;
990
- Marionette.Application.extend = extend;
991
-
992
- // Copy the `modules` feature on to the `Application` object
993
- Marionette.Application.prototype.module = Marionette.Modules.module;
994
-
995
- // Copy the features of `BindTo` on to these objects
996
- _.extend(Marionette.View.prototype, Marionette.BindTo);
997
- _.extend(Marionette.Application.prototype, Marionette.BindTo);
998
- _.extend(Marionette.Region.prototype, Marionette.BindTo);
999
-
1000
- // A simple wrapper method for deferring a callback until
1001
- // after another method has been called, passing the
1002
- // results of the first method to the second. Uses jQuery's
1003
- // deferred / promise objects, and $.when/then to make it
1004
- // work.
1005
- var callDeferredMethod = function(fn, callback, context){
1006
- var promise;
1007
- if (fn) { promise = fn.call(context); }
1008
- $.when(promise).then(callback);
979
+ // Pre-compile the template before caching it. Override
980
+ // this method if you do not need to pre-compile a template
981
+ // (JST / RequireJS for example) or if you want to change
982
+ // the template engine used (Handebars, etc).
983
+ compileTemplate: function(rawTemplate){
984
+ return _.template(rawTemplate);
1009
985
  }
986
+ });
987
+
988
+
989
+ // Renderer
990
+ // --------
991
+
992
+ // Render a template with data by passing in the template
993
+ // selector and the data to render.
994
+ Marionette.Renderer = {
995
+
996
+ // Render a template with data. The `template` parameter is
997
+ // passed to the `TemplateCache` object to retrieve the
998
+ // template function. Override this method to provide your own
999
+ // custom rendering and template handling for all of Marionette.
1000
+ render: function(template, data){
1001
+ var templateFunc = Marionette.TemplateCache.get(template);
1002
+ var html = templateFunc(data);
1003
+ return html;
1004
+ }
1005
+ };
1006
+
1007
+
1008
+ // Callbacks
1009
+ // ---------
1010
+
1011
+ // A simple way of managing a collection of callbacks
1012
+ // and executing them at a later point in time, using jQuery's
1013
+ // `Deferred` object.
1014
+ Marionette.Callbacks = function(){
1015
+ this.deferred = $.Deferred();
1016
+ this.promise = this.deferred.promise();
1017
+ };
1018
+
1019
+ _.extend(Marionette.Callbacks.prototype, {
1020
+
1021
+ // Add a callback to be executed. Callbacks added here are
1022
+ // guaranteed to execute, even if they are added after the
1023
+ // `run` method is called.
1024
+ add: function(callback, contextOverride){
1025
+ this.promise.done(function(context, options){
1026
+ if (contextOverride){ context = contextOverride; }
1027
+ callback.call(context, options);
1028
+ });
1029
+ },
1030
+
1031
+ // Run all registered callbacks with the context specified.
1032
+ // Additional callbacks can be added after this has been run
1033
+ // and they will still be executed.
1034
+ run: function(options, context){
1035
+ this.deferred.resolve(context, options);
1036
+ }
1037
+ });
1038
+
1039
+
1040
+ // Event Aggregator
1041
+ // ----------------
1042
+
1043
+ // A pub-sub object that can be used to decouple various parts
1044
+ // of an application through event-driven architecture.
1045
+ Marionette.EventAggregator = function(options){
1046
+ _.extend(this, options);
1047
+ };
1048
+
1049
+ _.extend(Marionette.EventAggregator.prototype, Backbone.Events, Marionette.BindTo, {
1050
+ // Assumes the event aggregator itself is the
1051
+ // object being bound to.
1052
+ bindTo: function(eventName, callback, context){
1053
+ return Marionette.BindTo.bindTo.call(this, this, eventName, callback, context);
1054
+ }
1055
+ });
1056
+
1057
+
1058
+ // Helpers
1059
+ // -------
1010
1060
 
1061
+ // For slicing `arguments` in functions
1062
+ var slice = Array.prototype.slice;
1011
1063
 
1012
1064
  return Marionette;
1013
1065
  })(Backbone, _, window.jQuery || window.Zepto || window.ender);