marionette-amd-rails 0.8.4.1 → 0.10.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,7 +3,7 @@ marionette-amd-rails
3
3
 
4
4
  [![Dependency Status](https://gemnasium.com/eploko/marionette-amd-rails.png)](https://gemnasium.com/eploko/marionette-amd-rails)
5
5
 
6
- This gem is a wrapper for the AMD version of Derick Bailey's [Backbone.Marionette](https://github.com/derickbailey/backbone.marionette) library. It vendors the javascript library code for use with Rails' asset pipeline (Rails 3.1+).
6
+ This gem is a wrapper for the AMD version of Derick Bailey's [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) library. It vendors the javascript library code for use with Rails' asset pipeline (Rails 3.1+).
7
7
 
8
8
  ## Dependencies
9
9
 
@@ -15,7 +15,7 @@ Add it to your Gemfile:
15
15
 
16
16
  group :assets do
17
17
  # Your other asset gems (sass-rails, coffee-rails, etc)
18
-
18
+
19
19
  gem 'marionette-amd-rails'
20
20
  end
21
21
 
@@ -23,20 +23,20 @@ Load `backbone.marionette` module as a dependency when appropriate.
23
23
 
24
24
  ## Versioning
25
25
 
26
- The gem will mirror the [Backbone.Marionette](https://github.com/derickbailey/backbone.marionette) versioning scheme. That is, version 0.8.2.* of `marionette-amd-rails` would vendor [Backbone.Marionette](https://github.com/derickbailey/backbone.marionette) v0.8.2.
26
+ The gem will mirror the [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) versioning scheme. That is, version 0.8.2.* of `marionette-amd-rails` would vendor [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) v0.8.2.
27
27
 
28
28
  ## Contributing
29
29
 
30
- For bugs in [Backbone.Marionette](https://github.com/derickbailey/backbone.marionette) itself, head over to their [issue tracker](https://github.com/derickbailey/backbone.marionette/issues). If you have a question, post it at [StackOverflow under the `backbone.marionette` tag](http://stackoverflow.com/questions/tagged/backbone.marionette).
30
+ For bugs in [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) itself, head over to their [issue tracker](https://github.com/derickbailey/backbone.marionette/issues). If you have a question, post it at [StackOverflow under the `backbone.marionette` tag](http://stackoverflow.com/questions/tagged/backbone.marionette).
31
31
 
32
32
  For bugs in this gem distribution, use the [GitHub issue tracker](https://github.com/eploko/marionette-amd-rails/issues). If you could submit a pull request - that's even better!
33
33
 
34
34
  ## Donations
35
35
 
36
- If you're using Marionette and you're finding that it is saving you time and effort, then please consider donating to the upstream [Backbone.Marionette](https://github.com/derickbailey/backbone.marionette) project.
36
+ If you're using Marionette and you're finding that it is saving you time and effort, then please consider donating to the upstream [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) project.
37
37
 
38
38
  [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7SJHYWJ487SF4)
39
39
 
40
40
  ## License
41
41
 
42
- This library is distributed under the MIT license. Please see the LICENSE file.
42
+ This library is distributed under the MIT license. Please see the LICENSE file.
@@ -1,9 +1,6 @@
1
- // Backbone.Marionette v0.8.4
2
- //
3
- // Copyright (C)2012 Derick Bailey, Muted Solutions, LLC
4
- // Distributed Under MIT License
5
- //
6
- // Documentation and Full License Available at:
1
+ // Backbone.Marionette, v0.10.2
2
+ // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
3
+ // Distributed under MIT license
7
4
  // http://github.com/derickbailey/backbone.marionette
8
5
  (function (root, factory) {
9
6
  if (typeof exports === 'object') {
@@ -18,25 +15,91 @@
18
15
 
19
16
  define(['jquery', 'underscore', 'backbone'], factory);
20
17
 
21
- }
18
+ }
22
19
  }(this, function ($, _, Backbone) {
23
20
 
24
21
  Backbone.Marionette = (function(Backbone, _, $){
25
- var Marionette = {};
22
+ var Marionette = {};
23
+
24
+ // EventBinder
25
+ // -----------
26
+
27
+ // The event binder facilitates the binding and unbinding of events
28
+ // from objects that extend `Backbone.Events`. It makes
29
+ // unbinding events, even with anonymous callback functions,
30
+ // easy.
31
+ //
32
+ // Inspired by [Johnny Oshika](http://stackoverflow.com/questions/7567404/backbone-js-repopulate-or-recreate-the-view/7607853#7607853)
33
+
34
+ Marionette.EventBinder = function(){
35
+ this._eventBindings = [];
36
+ };
37
+
38
+ _.extend(Marionette.EventBinder.prototype, {
39
+ // Store the event binding in array so it can be unbound
40
+ // easily, at a later point in time.
41
+ bindTo: function (obj, eventName, callback, context) {
42
+ context = context || this;
43
+ obj.on(eventName, callback, context);
44
+
45
+ var binding = {
46
+ obj: obj,
47
+ eventName: eventName,
48
+ callback: callback,
49
+ context: context
50
+ };
51
+
52
+ this._eventBindings.push(binding);
53
+
54
+ return binding;
55
+ },
56
+
57
+ // Unbind from a single binding object. Binding objects are
58
+ // returned from the `bindTo` method call.
59
+ unbindFrom: function(binding){
60
+ binding.obj.off(binding.eventName, binding.callback, binding.context);
61
+ this._eventBindings = _.reject(this._eventBindings, function(bind){return bind === binding;});
62
+ },
63
+
64
+ // Unbind all of the events that we have stored.
65
+ unbindAll: function () {
66
+ var that = this;
67
+
68
+ // The `unbindFrom` call removes elements from the array
69
+ // while it is being iterated, so clone it first.
70
+ var bindings = _.map(this._eventBindings, _.identity);
71
+ _.each(bindings, function (binding, index) {
72
+ that.unbindFrom(binding);
73
+ });
74
+ }
75
+ });
26
76
 
27
- Marionette.version = "0.8.4";
77
+ // Copy the `extend` function used by Backbone's classes
78
+ Marionette.EventBinder.extend = Backbone.View.extend;
28
79
 
29
80
  // Marionette.View
30
81
  // ---------------
31
82
 
32
83
  // The core view type that other Marionette views extend from.
33
84
  Marionette.View = Backbone.View.extend({
34
- // Get the template or template id/selector for this view
85
+
86
+ constructor: function(){
87
+ var eventBinder = new Marionette.EventBinder();
88
+ _.extend(this, eventBinder);
89
+
90
+ Backbone.View.prototype.constructor.apply(this, arguments);
91
+
92
+ this.bindBackboneEntityTo(this.model, this.modelEvents);
93
+ this.bindBackboneEntityTo(this.collection, this.collectionEvents);
94
+
95
+ this.bindTo(this, "show", this.onShowCalled, this);
96
+ },
97
+
98
+ // Get the template for this view
35
99
  // instance. You can set a `template` attribute in the view
36
100
  // definition or pass a `template: "whatever"` parameter in
37
- // to the constructor options. The `template` can also be
38
- // a function that returns a selector string.
39
- getTemplateSelector: function(){
101
+ // to the constructor options.
102
+ getTemplate: function(){
40
103
  var template;
41
104
 
42
105
  // Get the template from `this.options.template` or
@@ -47,25 +110,20 @@
47
110
  template = this.template;
48
111
  }
49
112
 
50
- // check if it's a function and execute it, if it is
51
- if (_.isFunction(template)){
52
- template = template.call(this);
53
- }
54
-
55
113
  return template;
56
114
  },
57
115
 
58
116
  // Serialize the model or collection for the view. If a model is
59
117
  // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
60
118
  // is also called, but is used to populate an `items` array in the
61
- // resulting data. If both are found, defaults to the model.
62
- // You can override the `serializeData` method in your own view
119
+ // resulting data. If both are found, defaults to the model.
120
+ // You can override the `serializeData` method in your own view
63
121
  // definition, to provide custom serialization for your view's data.
64
122
  serializeData: function(){
65
123
  var data;
66
124
 
67
- if (this.model) {
68
- data = this.model.toJSON();
125
+ if (this.model) {
126
+ data = this.model.toJSON();
69
127
  }
70
128
  else if (this.collection) {
71
129
  data = { items: this.collection.toJSON() };
@@ -110,16 +168,18 @@
110
168
  if (e && e.preventDefault){ e.preventDefault(); }
111
169
  if (e && e.stopPropagation){ e.stopPropagation(); }
112
170
  that.trigger(value);
113
- }
171
+ };
114
172
 
115
173
  });
116
174
 
117
175
  return triggerEvents;
118
176
  },
119
177
 
178
+ // Overriding Backbone.View's delegateEvents specifically
179
+ // to handle the `triggers` configuration
120
180
  delegateEvents: function(events){
121
181
  events = events || this.events;
122
- if (_.isFunction(events)){ events = events.call(this)}
182
+ if (_.isFunction(events)){ events = events.call(this); }
123
183
 
124
184
  var combinedEvents = {};
125
185
  var triggers = this.configureTriggers();
@@ -128,6 +188,9 @@
128
188
  Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
129
189
  },
130
190
 
191
+ // Internal method, handles the `show` event.
192
+ onShowCalled: function(){},
193
+
131
194
  // Default `close` implementation, for removing a view from the
132
195
  // DOM and unbinding it. Regions will call this method
133
196
  // for you. You can specify an `onClose` method in your view to
@@ -135,83 +198,87 @@
135
198
  close: function(){
136
199
  if (this.beforeClose) { this.beforeClose(); }
137
200
 
138
- this.unbindAll();
139
201
  this.remove();
140
202
 
141
203
  if (this.onClose) { this.onClose(); }
142
204
  this.trigger('close');
205
+ this.unbindAll();
143
206
  this.unbind();
144
- }
145
- });
146
-
147
- // Item View
148
- // ---------
149
-
150
- // A single item view implementation that contains code for rendering
151
- // with underscore.js templates, serializing the view's model or collection,
152
- // and calling several methods on extended views, such as `onRender`.
153
- Marionette.ItemView = Marionette.View.extend({
154
- constructor: function(){
155
- var args = slice.call(arguments);
156
- Marionette.View.prototype.constructor.apply(this, args);
157
-
158
- _.bindAll(this, "render");
159
-
160
- this.initialEvents();
161
207
  },
162
208
 
163
- // Configured the initial events that the item view
164
- // binds to. Override this method to prevent the initial
165
- // events, or to add your own initial events.
166
- initialEvents: function(){
167
- if (this.collection){
168
- this.bindTo(this.collection, "reset", this.render, this);
169
- }
170
- },
209
+ // This method binds the elements specified in the "ui" hash inside the view's code with
210
+ // the associated jQuery selectors.
211
+ bindUIElements: function(){
212
+ if (!this.ui) { return; }
171
213
 
172
- // Render the view, defaulting to underscore.js templates.
173
- // You can override this in your view definition.
174
- render: function(){
175
214
  var that = this;
176
215
 
177
- var deferredRender = $.Deferred();
216
+ if (!this.uiBindings) {
217
+ // We want to store the ui hash in uiBindings, since afterwards the values in the ui hash
218
+ // will be overridden with jQuery selectors.
219
+ this.uiBindings = this.ui;
220
+ }
178
221
 
179
- var beforeRenderDone = function() {
180
- that.trigger("before:render", that);
181
- that.trigger("item:before:render", that);
222
+ // refreshing the associated selectors since they should point to the newly rendered elements.
223
+ this.ui = {};
224
+ _.each(_.keys(this.uiBindings), function(key) {
225
+ var selector = that.uiBindings[key];
226
+ that.ui[key] = that.$(selector);
227
+ });
228
+ },
182
229
 
183
- var deferredData = that.serializeData();
184
- $.when(deferredData).then(dataSerialized);
185
- }
230
+ // This method is used to bind a backbone "entity" (collection/model) to methods on the view.
231
+ bindBackboneEntityTo: function(entity, bindings){
232
+ if (!entity || !bindings) { return; }
186
233
 
187
- var dataSerialized = function(data){
188
- var asyncRender = that.renderHtml(data);
189
- $.when(asyncRender).then(templateRendered);
190
- }
234
+ var view = this;
235
+ _.each(bindings, function(methodName, evt){
191
236
 
192
- var templateRendered = function(html){
193
- that.$el.html(html);
194
- callDeferredMethod(that.onRender, onRenderDone, that);
195
- }
237
+ var method = view[methodName];
238
+ if(!method) {
239
+ throw new Error("View method '"+ methodName +"' was configured as an event handler, but does not exist.");
240
+ }
196
241
 
197
- var onRenderDone = function(){
198
- that.trigger("render", that);
199
- that.trigger("item:rendered", that);
242
+ view.bindTo(entity, evt, method, view);
243
+ });
244
+ }
245
+ });
200
246
 
201
- deferredRender.resolve();
202
- }
247
+ // Item View
248
+ // ---------
203
249
 
204
- callDeferredMethod(this.beforeRender, beforeRenderDone, this);
250
+ // A single item view implementation that contains code for rendering
251
+ // with underscore.js templates, serializing the view's model or collection,
252
+ // and calling several methods on extended views, such as `onRender`.
253
+ Marionette.ItemView = Marionette.View.extend({
254
+ constructor: function(){
255
+ Marionette.View.prototype.constructor.apply(this, arguments);
205
256
 
206
- return deferredRender.promise();
257
+ if (this.initialEvents){
258
+ this.initialEvents();
259
+ }
207
260
  },
208
261
 
209
- // Render the data for this item view in to some HTML.
210
- // Override this method to replace the specific way in
211
- // which an item view has it's data rendered in to html.
212
- renderHtml: function(data) {
213
- var template = this.getTemplateSelector();
214
- return Marionette.Renderer.render(template, data);
262
+ // Render the view, defaulting to underscore.js templates.
263
+ // You can override this in your view definition to provide
264
+ // a very specific rendering for your view. In general, though,
265
+ // you should override the `Marionette.Renderer` object to
266
+ // change how Marionette renders views.
267
+ render: function(){
268
+ if (this.beforeRender){ this.beforeRender(); }
269
+ this.trigger("before:render", this);
270
+ this.trigger("item:before:render", this);
271
+
272
+ var data = this.serializeData();
273
+ var template = this.getTemplate();
274
+ var html = Marionette.Renderer.render(template, data);
275
+ this.$el.html(html);
276
+ this.bindUIElements();
277
+
278
+ if (this.onRender){ this.onRender(); }
279
+ this.trigger("render", this);
280
+ this.trigger("item:rendered", this);
281
+ return this;
215
282
  },
216
283
 
217
284
  // Override the default close event to add a few
@@ -231,12 +298,12 @@
231
298
  Marionette.CollectionView = Marionette.View.extend({
232
299
  constructor: function(){
233
300
  Marionette.View.prototype.constructor.apply(this, arguments);
234
-
235
- _.bindAll(this, "addItemView", "render");
301
+ this.initChildViewStorage();
236
302
  this.initialEvents();
303
+ this.onShowCallbacks = new Marionette.Callbacks();
237
304
  },
238
305
 
239
- // Configured the initial events that the collection view
306
+ // Configured the initial events that the collection view
240
307
  // binds to. Override this method to prevent the initial
241
308
  // events, or to add your own initial events.
242
309
  initialEvents: function(){
@@ -248,47 +315,89 @@
248
315
  },
249
316
 
250
317
  // Handle a child item added to the collection
251
- addChildView: function(item){
252
- var ItemView = this.getItemView();
253
- return this.addItemView(item, ItemView);
318
+ addChildView: function(item, collection, options){
319
+ this.closeEmptyView();
320
+ var ItemView = this.getItemView(item);
321
+ return this.addItemView(item, ItemView, options.index);
254
322
  },
255
323
 
256
- // Loop through all of the items and render
257
- // each of them with the specified `itemView`.
258
- render: function(){
259
- var that = this;
260
- var deferredRender = $.Deferred();
261
- var promises = [];
262
- var ItemView = this.getItemView();
324
+ // Override from `Marionette.View` to guarantee the `onShow` method
325
+ // of child views is called.
326
+ onShowCalled: function(){
327
+ this.onShowCallbacks.run();
328
+ },
263
329
 
330
+ // Internal method to trigger the before render callbacks
331
+ // and events
332
+ triggerBeforeRender: function(){
264
333
  if (this.beforeRender) { this.beforeRender(); }
334
+ this.trigger("before:render", this);
265
335
  this.trigger("collection:before:render", this);
336
+ },
337
+
338
+ // Internal method to trigger the rendered callbacks and
339
+ // events
340
+ triggerRendered: function(){
341
+ if (this.onRender) { this.onRender(); }
342
+ this.trigger("render", this);
343
+ this.trigger("collection:rendered", this);
344
+ },
266
345
 
346
+ // Render the collection of items. Override this method to
347
+ // provide your own implementation of a render function for
348
+ // the collection view.
349
+ render: function(){
350
+ this.triggerBeforeRender();
351
+ this.closeEmptyView();
267
352
  this.closeChildren();
268
353
 
269
- if (this.collection) {
270
- this.collection.each(function(item){
271
- var promise = that.addItemView(item, ItemView);
272
- promises.push(promise);
273
- });
354
+ if (this.collection && this.collection.length > 0) {
355
+ this.showCollection();
356
+ } else {
357
+ this.showEmptyView();
274
358
  }
275
359
 
276
- deferredRender.done(function(){
277
- if (this.onRender) { this.onRender(); }
278
- this.trigger("collection:rendered", this);
279
- });
360
+ this.triggerRendered();
361
+ return this;
362
+ },
280
363
 
281
- $.when.apply(this, promises).then(function(){
282
- deferredRender.resolveWith(that);
364
+ // Internal method to loop through each item in the
365
+ // collection view and show it
366
+ showCollection: function(){
367
+ var that = this;
368
+ var ItemView;
369
+ this.collection.each(function(item, index){
370
+ ItemView = that.getItemView(item);
371
+ that.addItemView(item, ItemView, index);
283
372
  });
373
+ },
374
+
375
+ // Internal method to show an empty view in place of
376
+ // a collection of item views, when the collection is
377
+ // empty
378
+ showEmptyView: function(){
379
+ var EmptyView = this.options.emptyView || this.emptyView;
380
+ if (EmptyView && !this._showingEmptyView){
381
+ this._showingEmptyView = true;
382
+ var model = new Backbone.Model();
383
+ this.addItemView(model, EmptyView, 0);
384
+ }
385
+ },
284
386
 
285
- return deferredRender.promise();
387
+ // Internal method to close an existing emptyView instance
388
+ // if one exists. Called when a collection view has been
389
+ // rendered empty, and then an item is added to the collection.
390
+ closeEmptyView: function(){
391
+ if (this._showingEmptyView){
392
+ this.closeChildren();
393
+ delete this._showingEmptyView;
394
+ }
286
395
  },
287
396
 
288
397
  // Retrieve the itemView type, either from `this.options.itemView`
289
398
  // or from the `itemView` in the object definition. The "options"
290
399
  // takes precedence.
291
- getItemView: function(){
400
+ getItemView: function(item){
292
401
  var itemView = this.options.itemView || this.itemView;
293
402
 
294
403
  if (!itemView){
@@ -302,15 +411,28 @@
302
411
 
303
412
  // Render the child item's view and add it to the
304
413
  // HTML for the collection view.
305
- addItemView: function(item, ItemView){
414
+ addItemView: function(item, ItemView, index){
306
415
  var that = this;
307
416
 
308
417
  var view = this.buildItemView(item, ItemView);
309
- this.bindTo(view, "all", function(){
310
418
 
311
- // get the args, prepend the event name
312
- // with "itemview:" and insert the child view
313
- // as the first event arg (after the event name)
419
+ // Store the child view itself so we can properly
420
+ // remove and/or close it later
421
+ this.storeChild(view);
422
+ if (this.onItemAdded){ this.onItemAdded(view); }
423
+ this.trigger("item:added", view);
424
+
425
+ // Render it and show it
426
+ var renderResult = this.renderItemView(view, index);
427
+
428
+ // call onShow for child item views
429
+ if (view.onShow){
430
+ this.onShowCallbacks.add(view.onShow, view);
431
+ }
432
+
433
+ // Forward all child item view events through the parent,
434
+ // prepending "itemview:" to the event name
435
+ var childBinding = this.bindTo(view, "all", function(){
314
436
  var args = slice.call(arguments);
315
437
  args[0] = "itemview:" + args[0];
316
438
  args.splice(1, 0, view);
@@ -318,22 +440,32 @@
318
440
  that.trigger.apply(that, args);
319
441
  });
320
442
 
321
- this.storeChild(view);
322
- this.trigger("item:added", view);
443
+ // Store all child event bindings so we can unbind
444
+ // them when removing / closing the child view
445
+ this.childBindings = this.childBindings || {};
446
+ this.childBindings[view.cid] = childBinding;
323
447
 
324
- var viewRendered = view.render();
325
- $.when(viewRendered).then(function(){
326
- that.appendHtml(that, view);
327
- });
328
-
329
- return viewRendered;
448
+ return renderResult;
330
449
  },
331
450
 
332
- // Build an `itemView` for every model in the collection.
451
+ // render the item view
452
+ renderItemView: function(view, index) {
453
+ view.render();
454
+ this.appendHtml(this, view, index);
455
+ },
456
+
457
+ // Build an `itemView` for every model in the collection.
333
458
  buildItemView: function(item, ItemView){
334
- var view = new ItemView({
335
- model: item
336
- });
459
+ var itemViewOptions;
460
+
461
+ if (_.isFunction(this.itemViewOptions)){
462
+ itemViewOptions = this.itemViewOptions(item);
463
+ } else {
464
+ itemViewOptions = this.itemViewOptions;
465
+ }
466
+
467
+ var options = _.extend({model: item}, itemViewOptions);
468
+ var view = new ItemView(options);
337
469
  return view;
338
470
  },
339
471
 
@@ -341,46 +473,63 @@
341
473
  removeItemView: function(item){
342
474
  var view = this.children[item.cid];
343
475
  if (view){
476
+ var childBinding = this.childBindings[view.cid];
477
+ if (childBinding) {
478
+ this.unbindFrom(childBinding);
479
+ delete this.childBindings[view.cid];
480
+ }
344
481
  view.close();
345
482
  delete this.children[item.cid];
346
483
  }
484
+
485
+ if (!this.collection || this.collection.length === 0){
486
+ this.showEmptyView();
487
+ }
488
+
347
489
  this.trigger("item:removed", view);
348
490
  },
349
491
 
350
492
  // Append the HTML to the collection's `el`.
351
493
  // Override this method to do something other
352
494
  // then `.append`.
353
- appendHtml: function(collectionView, itemView){
495
+ appendHtml: function(collectionView, itemView, index){
354
496
  collectionView.$el.append(itemView.el);
355
497
  },
356
498
 
357
499
  // Store references to all of the child `itemView`
358
500
  // instances so they can be managed and cleaned up, later.
359
501
  storeChild: function(view){
360
- if (!this.children){
361
- this.children = {};
362
- }
363
502
  this.children[view.model.cid] = view;
364
503
  },
365
-
504
+
505
+ // Internal method to set up the `children` object for
506
+ // storing all of the child views
507
+ initChildViewStorage: function(){
508
+ this.children = {};
509
+ },
510
+
366
511
  // Handle cleanup and other closing needs for
367
512
  // the collection of views.
368
513
  close: function(){
369
514
  this.trigger("collection:before:close");
370
515
  this.closeChildren();
371
- Marionette.View.prototype.close.apply(this, arguments);
372
516
  this.trigger("collection:closed");
517
+ Marionette.View.prototype.close.apply(this, arguments);
373
518
  },
374
519
 
520
+ // Close the child views that this collection view
521
+ // is holding on to, if any
375
522
  closeChildren: function(){
523
+ var that = this;
376
524
  if (this.children){
377
- _.each(this.children, function(childView){
378
- childView.close();
525
+ _.each(_.clone(this.children), function(childView){
526
+ that.removeItemView(childView.model);
379
527
  });
380
528
  }
381
529
  }
382
530
  });
383
531
 
532
+
384
533
  // Composite View
385
534
  // --------------
386
535
 
@@ -393,12 +542,31 @@
393
542
  this.itemView = this.getItemView();
394
543
  },
395
544
 
545
+ // Configured the initial events that the composite view
546
+ // binds to. Override this method to prevent the initial
547
+ // events, or to add your own initial events.
548
+ initialEvents: function(){
549
+ if (this.collection){
550
+ this.bindTo(this.collection, "add", this.addChildView, this);
551
+ this.bindTo(this.collection, "remove", this.removeItemView, this);
552
+ this.bindTo(this.collection, "reset", this.renderCollection, this);
553
+ }
554
+ },
555
+
396
556
  // Retrieve the `itemView` to be used when rendering each of
397
557
  // the items in the collection. The default is to return
398
558
  // `this.itemView` or Marionette.CompositeView if no `itemView`
399
559
  // has been defined
400
- getItemView: function(){
401
- return this.itemView || this.constructor;
560
+ getItemView: function(item){
561
+ var itemView = this.options.itemView || this.itemView || this.constructor;
562
+
563
+ if (!itemView){
564
+ var err = new Error("An `itemView` must be specified");
565
+ err.name = "NoItemViewError";
566
+ throw err;
567
+ }
568
+
569
+ return itemView;
402
570
  },
403
571
 
404
572
  // Renders the model once, and the collection once. Calling
@@ -406,34 +574,26 @@
406
574
  // but the collection will not re-render.
407
575
  render: function(){
408
576
  var that = this;
409
- var compositeRendered = $.Deferred();
410
577
 
411
- var modelIsRendered = this.renderModel();
412
- $.when(modelIsRendered).then(function(html){
413
- that.$el.html(html);
414
- that.trigger("composite:model:rendered");
415
- that.trigger("render");
578
+ this.resetItemViewContainer();
416
579
 
417
- var collectionIsRendered = that.renderCollection();
418
- $.when(collectionIsRendered).then(function(){
419
- compositeRendered.resolve();
420
- });
421
- });
422
-
423
- compositeRendered.done(function(){
424
- that.trigger("composite:rendered");
425
- });
580
+ var html = this.renderModel();
581
+ this.$el.html(html);
582
+ // the ui bindings is done here and not at the end of render since they should be
583
+ // available before the collection is rendered.
584
+ this.bindUIElements();
585
+ this.trigger("composite:model:rendered");
586
+ this.trigger("render");
426
587
 
427
- return compositeRendered.promise();
588
+ this.renderCollection();
589
+ this.trigger("composite:rendered");
590
+ return this;
428
591
  },
429
592
 
430
593
  // Render the collection for the composite view
431
594
  renderCollection: function(){
432
- var collectionDeferred = Marionette.CollectionView.prototype.render.apply(this, arguments);
433
- collectionDeferred.done(function(){
434
- this.trigger("composite:collection:rendered");
435
- });
436
- return collectionDeferred.promise();
595
+ Marionette.CollectionView.prototype.render.apply(this, arguments);
596
+ this.trigger("composite:collection:rendered");
437
597
  },
438
598
 
439
599
  // Render an individual model, if we have one, as
@@ -443,12 +603,52 @@
443
603
  var data = {};
444
604
  data = this.serializeData();
445
605
 
446
- var template = this.getTemplateSelector();
606
+ var template = this.getTemplate();
447
607
  return Marionette.Renderer.render(template, data);
608
+ },
609
+
610
+ // Appends the `el` of itemView instances to the specified
611
+ // `itemViewContainer` (a jQuery selector). Override this method to
612
+ // provide custom logic of how the child item view instances have their
613
+ // HTML appended to the composite view instance.
614
+ appendHtml: function(cv, iv){
615
+ var $container = this.getItemViewContainer(cv);
616
+ $container.append(iv.el);
617
+ },
618
+
619
+ // Internal method to ensure an `$itemViewContainer` exists, for the
620
+ // `appendHtml` method to use.
621
+ getItemViewContainer: function(containerView){
622
+ var container;
623
+ if ("$itemViewContainer" in containerView){
624
+ container = containerView.$itemViewContainer;
625
+ } else {
626
+ if (containerView.itemViewContainer){
627
+ container = containerView.$(_.result(containerView, "itemViewContainer"));
628
+
629
+ if (container.length <= 0) {
630
+ var err = new Error("Missing `itemViewContainer`");
631
+ err.name = "ItemViewContainerMissingError";
632
+ throw err;
633
+ }
634
+ } else {
635
+ container = containerView.$el;
636
+ }
637
+ containerView.$itemViewContainer = container;
638
+ }
639
+ return container;
640
+ },
641
+
642
+ // Internal method to reset the `$itemViewContainer` on render
643
+ resetItemViewContainer: function(){
644
+ if (this.$itemViewContainer){
645
+ delete this.$itemViewContainer;
646
+ }
448
647
  }
449
648
  });
450
649
 
451
- // Region
650
+
651
+ // Region
452
652
  // ------
453
653
 
454
654
  // Manage the visual regions of your composite application. See
@@ -456,7 +656,8 @@
456
656
  Marionette.Region = function(options){
457
657
  this.options = options || {};
458
658
 
459
- _.extend(this, options);
659
+ var eventBinder = new Marionette.EventBinder();
660
+ _.extend(this, eventBinder, options);
460
661
 
461
662
  if (!this.el){
462
663
  var err = new Error("An 'el' must be specified");
@@ -476,11 +677,19 @@
476
677
  // directly from the `el` attribute. Also calls an optional
477
678
  // `onShow` and `close` method on your view, just after showing
478
679
  // or just before closing the view, respectively.
479
- show: function(view, appendMethod){
480
- this.ensureEl();
680
+ show: function(view){
481
681
 
682
+ this.ensureEl();
482
683
  this.close();
483
- this.open(view, appendMethod);
684
+
685
+ view.render();
686
+ this.open(view);
687
+
688
+ if (view.onShow) { view.onShow(); }
689
+ view.trigger("show");
690
+
691
+ if (this.onShow) { this.onShow(view); }
692
+ this.trigger("view:show", view);
484
693
 
485
694
  this.currentView = view;
486
695
  },
@@ -494,22 +703,13 @@
494
703
  // Override this method to change how the region finds the
495
704
  // DOM element that it manages. Return a jQuery selector object.
496
705
  getEl: function(selector){
497
- return $(selector);
706
+ return $(selector);
498
707
  },
499
708
 
500
- // Internal method to render and display a view. Not meant
501
- // to be called from any external code.
502
- open: function(view, appendMethod){
503
- var that = this;
504
- appendMethod = appendMethod || "html";
505
-
506
- $.when(view.render()).then(function () {
507
- that.$el[appendMethod](view.el);
508
- if (view.onShow) { view.onShow(); }
509
- if (that.onShow) { that.onShow(view); }
510
- view.trigger("show");
511
- that.trigger("view:show", view);
512
- });
709
+ // Override this method to change how the new view is
710
+ // appended to the `$el` that the region is managing
711
+ open: function(view){
712
+ this.$el.html(view.el);
513
713
  },
514
714
 
515
715
  // Close the current view, if there is one. If there is no
@@ -524,20 +724,30 @@
524
724
  delete this.currentView;
525
725
  },
526
726
 
527
- // Attach an existing view to the region. This
528
- // will not call `render` or `onShow` for the new view,
727
+ // Attach an existing view to the region. This
728
+ // will not call `render` or `onShow` for the new view,
529
729
  // and will not replace the current HTML for the `el`
530
730
  // of the region.
531
731
  attachView: function(view){
532
732
  this.currentView = view;
733
+ },
734
+
735
+ // Reset the region by closing any existing view and
736
+ // clearing out the cached `$el`. The next time a view
737
+ // is shown via this region, the region will re-query the
738
+ // DOM for the region's `el`.
739
+ reset: function(){
740
+ this.close();
741
+ delete this.$el;
533
742
  }
534
743
  });
535
744
 
745
+ // Copy the `extend` function used by Backbone's classes
746
+ Marionette.Region.extend = Backbone.View.extend;
747
+
536
748
  // Layout
537
749
  // ------
538
750
 
539
- // Formerly known as Composite Region.
540
- //
541
751
  // Used for managing application layouts, nested layouts and
542
752
  // multiple regions within an application or sub-application.
543
753
  //
@@ -545,19 +755,37 @@
545
755
  // attaches `Region` instances to the specified `regions`.
546
756
  // Used for composite view management and sub-application areas.
547
757
  Marionette.Layout = Marionette.ItemView.extend({
758
+ regionType: Marionette.Region,
759
+
760
+ // Ensure the regions are avialable when the `initialize` method
761
+ // is called.
548
762
  constructor: function () {
549
- this.vent = new Backbone.Marionette.EventAggregator();
763
+ this.initializeRegions();
550
764
  Backbone.Marionette.ItemView.apply(this, arguments);
551
- this.regionManagers = {};
552
765
  },
553
766
 
554
- render: function () {
555
- this.initializeRegions();
556
- return Backbone.Marionette.ItemView.prototype.render.call(this, arguments);
767
+ // Layout's render will use the existing region objects the
768
+ // first time it is called. Subsequent calls will close the
769
+ // views that the regions are showing and then reset the `el`
770
+ // for the regions to the newly rendered DOM elements.
771
+ render: function(){
772
+ // If this is not the first render call, then we need to
773
+ // re-initializing the `el` for each region
774
+ if (!this._firstRender){
775
+ this.closeRegions();
776
+ this.reInitializeRegions();
777
+ } else {
778
+ this._firstRender = false;
779
+ }
780
+
781
+ var result = Marionette.ItemView.prototype.render.apply(this, arguments);
782
+ return result;
557
783
  },
558
784
 
785
+ // Handle closing regions, and then close the view itself.
559
786
  close: function () {
560
787
  this.closeRegions();
788
+ this.destroyRegions();
561
789
  Backbone.Marionette.ItemView.prototype.close.call(this, arguments);
562
790
  },
563
791
 
@@ -568,18 +796,57 @@
568
796
  // will product a `layout.menu` object which is a region
569
797
  // that controls the `.menu-container` DOM element.
570
798
  initializeRegions: function () {
799
+ if (!this.regionManagers){
800
+ this.regionManagers = {};
801
+ }
802
+
571
803
  var that = this;
572
- _.each(this.regions, function (selector, name) {
573
- var regionManager = new Backbone.Marionette.Region({
574
- el: selector,
804
+ _.each(this.regions, function (region, name) {
805
+ var regionIsString = (typeof region === "string");
806
+ var regionSelectorIsString = (typeof region.selector === "string");
807
+ var regionTypeIsUndefined = (typeof region.regionType === "undefined");
808
+
809
+ if (!regionIsString && !regionSelectorIsString) {
810
+ throw new Error("Region must be specified as a selector string or an object with selector property");
811
+ }
812
+
813
+ var selector, RegionType;
814
+
815
+ if (regionIsString) {
816
+ selector = region;
817
+ } else {
818
+ selector = region.selector;
819
+ }
820
+
821
+ if (regionTypeIsUndefined){
822
+ RegionType = that.regionType;
823
+ } else {
824
+ RegionType = region.regionType;
825
+ }
575
826
 
827
+ var regionManager = new RegionType({
828
+ el: selector,
576
829
  getEl: function(selector){
577
830
  return that.$(selector);
578
831
  }
579
832
  });
833
+
580
834
  that.regionManagers[name] = regionManager;
581
835
  that[name] = regionManager;
582
836
  });
837
+
838
+ },
839
+
840
+ // Re-initialize all of the regions by updating the `el` that
841
+ // they point to
842
+ reInitializeRegions: function(){
843
+ if (this.regionManagers && _.size(this.regionManagers)===0){
844
+ this.initializeRegions();
845
+ } else {
846
+ _.each(this.regionManagers, function(region){
847
+ region.reset();
848
+ });
849
+ }
583
850
  },
584
851
 
585
852
  // Close all of the regions that have been opened by
@@ -589,12 +856,102 @@
589
856
  var that = this;
590
857
  _.each(this.regionManagers, function (manager, name) {
591
858
  manager.close();
859
+ });
860
+ },
861
+
862
+ // Destroys all of the regions by removing references
863
+ // from the Layout
864
+ destroyRegions: function(){
865
+ var that = this;
866
+ _.each(this.regionManagers, function (manager, name) {
592
867
  delete that[name];
593
868
  });
594
869
  this.regionManagers = {};
595
870
  }
596
871
  });
597
872
 
873
+
874
+ // Application
875
+ // -----------
876
+
877
+ // Contain and manage the composite application as a whole.
878
+ // Stores and starts up `Region` objects, includes an
879
+ // event aggregator as `app.vent`
880
+ Marionette.Application = function(options){
881
+ this.initCallbacks = new Marionette.Callbacks();
882
+ this.vent = new Marionette.EventAggregator();
883
+ this.submodules = {};
884
+
885
+ var eventBinder = new Marionette.EventBinder();
886
+ _.extend(this, eventBinder, options);
887
+ };
888
+
889
+ _.extend(Marionette.Application.prototype, Backbone.Events, {
890
+ // Add an initializer that is either run at when the `start`
891
+ // method is called, or run immediately if added after `start`
892
+ // has already been called.
893
+ addInitializer: function(initializer){
894
+ this.initCallbacks.add(initializer);
895
+ },
896
+
897
+ // kick off all of the application's processes.
898
+ // initializes all of the regions that have been added
899
+ // to the app, and runs all of the initializer functions
900
+ start: function(options){
901
+ this.trigger("initialize:before", options);
902
+ this.initCallbacks.run(options, this);
903
+ this.trigger("initialize:after", options);
904
+
905
+ this.trigger("start", options);
906
+ },
907
+
908
+ // Add regions to your app.
909
+ // Accepts a hash of named strings or Region objects
910
+ // addRegions({something: "#someRegion"})
911
+ // addRegions{{something: Region.extend({el: "#someRegion"}) });
912
+ addRegions: function(regions){
913
+ var RegionValue, regionObj, region;
914
+
915
+ for(region in regions){
916
+ if (regions.hasOwnProperty(region)){
917
+ RegionValue = regions[region];
918
+
919
+ if (typeof RegionValue === "string"){
920
+ regionObj = new Marionette.Region({
921
+ el: RegionValue
922
+ });
923
+ } else {
924
+ regionObj = new RegionValue();
925
+ }
926
+
927
+ this[region] = regionObj;
928
+ }
929
+ }
930
+ },
931
+
932
+ // Removes a region from your app.
933
+ // Accepts the regions name
934
+ // removeRegion('myRegion')
935
+ removeRegion: function(region) {
936
+ this[region].close();
937
+ delete this[region];
938
+ },
939
+
940
+ // Create a module, attached to the application
941
+ module: function(moduleNames, moduleDefinition){
942
+ // slice the args, and add this application object as the
943
+ // first argument of the array
944
+ var args = slice.call(arguments);
945
+ args.unshift(this);
946
+
947
+ // see the Marionette.Module object for more information
948
+ return Marionette.Module.create.apply(Marionette.Module, args);
949
+ }
950
+ });
951
+
952
+ // Copy the `extend` function used by Backbone's classes
953
+ Marionette.Application.extend = Backbone.View.extend;
954
+
598
955
  // AppRouter
599
956
  // ---------
600
957
 
@@ -605,14 +962,14 @@
605
962
  //
606
963
  // Configure an AppRouter with `appRoutes`.
607
964
  //
608
- // App routers can only take one `controller` object.
965
+ // App routers can only take one `controller` object.
609
966
  // It is recommended that you divide your controller
610
967
  // objects in to smaller peices of related functionality
611
968
  // and have multiple routers / controllers, instead of
612
969
  // just one giant router and controller.
613
970
  //
614
971
  // You can also add standard routes to an AppRouter.
615
-
972
+
616
973
  Marionette.AppRouter = Backbone.Router.extend({
617
974
 
618
975
  constructor: function(options){
@@ -627,6 +984,9 @@
627
984
  }
628
985
  },
629
986
 
987
+ // Internal method to process the `appRoutes` for the
988
+ // router, and turn them in to routes that trigger the
989
+ // specified method on the specified `controller`.
630
990
  processAppRoutes: function(controller, appRoutes){
631
991
  var method, methodName;
632
992
  var route, routesLength, i;
@@ -657,206 +1017,304 @@
657
1017
  }
658
1018
  }
659
1019
  });
660
-
661
- // Composite Application
662
- // ---------------------
663
1020
 
664
- // Contain and manage the composite application as a whole.
665
- // Stores and starts up `Region` objects, includes an
666
- // event aggregator as `app.vent`
667
- Marionette.Application = function(options){
668
- this.initCallbacks = new Marionette.Callbacks();
669
- this.vent = new Marionette.EventAggregator();
670
- _.extend(this, options);
1021
+
1022
+ // Module
1023
+ // ------
1024
+
1025
+ // A simple module system, used to create privacy and encapsulation in
1026
+ // Marionette applications
1027
+ Marionette.Module = function(moduleName, app, customArgs){
1028
+ this.moduleName = moduleName;
1029
+
1030
+ // store sub-modules
1031
+ this.submodules = {};
1032
+
1033
+ this._setupInitializersAndFinalizers();
1034
+
1035
+ // store the configuration for this module
1036
+ this.config = {};
1037
+ this.config.app = app;
1038
+ this.config.customArgs = customArgs;
1039
+ this.config.definitions = [];
1040
+
1041
+ // extend this module with an event binder
1042
+ var eventBinder = new Marionette.EventBinder();
1043
+ _.extend(this, eventBinder);
671
1044
  };
672
1045
 
673
- _.extend(Marionette.Application.prototype, Backbone.Events, {
674
- // Add an initializer that is either run at when the `start`
675
- // method is called, or run immediately if added after `start`
676
- // has already been called.
677
- addInitializer: function(initializer){
678
- this.initCallbacks.add(initializer);
1046
+ // Extend the Module prototype with events / bindTo, so that the module
1047
+ // can be used as an event aggregator or pub/sub.
1048
+ _.extend(Marionette.Module.prototype, Backbone.Events, {
1049
+
1050
+ // Initializer for a specific module. Initializers are run when the
1051
+ // module's `start` method is called.
1052
+ addInitializer: function(callback){
1053
+ this._initializerCallbacks.add(callback);
679
1054
  },
680
1055
 
681
- // kick off all of the application's processes.
682
- // initializes all of the regions that have been added
683
- // to the app, and runs all of the initializer functions
1056
+ // Finalizers are run when a module is stopped. They are used to teardown
1057
+ // and finalize any variables, references, events and other code that the
1058
+ // module had set up.
1059
+ addFinalizer: function(callback){
1060
+ this._finalizerCallbacks.add(callback);
1061
+ },
1062
+
1063
+ // Start the module, and run all of it's initializers
684
1064
  start: function(options){
685
- this.trigger("initialize:before", options);
686
- this.initCallbacks.run(this, options);
687
- this.trigger("initialize:after", options);
1065
+ // Prevent re-start the module
1066
+ if (this._isInitialized){ return; }
688
1067
 
689
- this.trigger("start", options);
1068
+ // start the sub-modules (depth-first hierarchy)
1069
+ _.each(this.submodules, function(mod){
1070
+ if (mod.config.options.startWithParent){
1071
+ mod.start(options);
1072
+ }
1073
+ });
1074
+
1075
+ // run the callbacks to "start" the current module
1076
+ this._initializerCallbacks.run(options, this);
1077
+ this._isInitialized = true;
690
1078
  },
691
1079
 
692
- // Add regions to your app.
693
- // Accepts a hash of named strings or Region objects
694
- // addRegions({something: "#someRegion"})
695
- // addRegions{{something: Region.extend({el: "#someRegion"}) });
696
- addRegions: function(regions){
697
- var regionValue, regionObj, region;
1080
+ // Stop this module by running its finalizers and then stop all of
1081
+ // the sub-modules for this module
1082
+ stop: function(){
1083
+ // if we are not initialized, don't bother finalizing
1084
+ if (!this._isInitialized){ return; }
1085
+ this._isInitialized = false;
698
1086
 
699
- for(region in regions){
700
- if (regions.hasOwnProperty(region)){
701
- regionValue = regions[region];
702
-
703
- if (typeof regionValue === "string"){
704
- regionObj = new Marionette.Region({
705
- el: regionValue
706
- });
707
- } else {
708
- regionObj = new regionValue();
709
- }
1087
+ // stop the sub-modules; depth-first, to make sure the
1088
+ // sub-modules are stopped / finalized before parents
1089
+ _.each(this.submodules, function(mod){ mod.stop(); });
710
1090
 
711
- this[region] = regionObj;
712
- }
713
- }
1091
+ // run the finalizers
1092
+ this._finalizerCallbacks.run();
1093
+
1094
+ // reset the initializers and finalizers
1095
+ this._initializerCallbacks.reset();
1096
+ this._finalizerCallbacks.reset();
1097
+ },
1098
+
1099
+ // Configure the module with a definition function and any custom args
1100
+ // that are to be passed in to the definition function
1101
+ addDefinition: function(moduleDefinition){
1102
+ this._runModuleDefinition(moduleDefinition);
1103
+ },
1104
+
1105
+ // Internal method: run the module definition function with the correct
1106
+ // arguments
1107
+ _runModuleDefinition: function(definition){
1108
+ if (!definition){ return; }
1109
+
1110
+ // build the correct list of arguments for the module definition
1111
+ var args = _.flatten([
1112
+ this,
1113
+ this.config.app,
1114
+ Backbone,
1115
+ Marionette,
1116
+ $, _,
1117
+ this.config.customArgs
1118
+ ]);
1119
+
1120
+ definition.apply(this, args);
1121
+ },
1122
+
1123
+ // Internal method: set up new copies of initializers and finalizers.
1124
+ // Calling this method will wipe out all existing initializers and
1125
+ // finalizers.
1126
+ _setupInitializersAndFinalizers: function(){
1127
+ this._initializerCallbacks = new Marionette.Callbacks();
1128
+ this._finalizerCallbacks = new Marionette.Callbacks();
714
1129
  }
715
1130
  });
716
1131
 
717
- // BindTo: Event Binding
718
- // ---------------------
719
-
720
- // BindTo facilitates the binding and unbinding of events
721
- // from objects that extend `Backbone.Events`. It makes
722
- // unbinding events, even with anonymous callback functions,
723
- // easy.
724
- //
725
- // Thanks to Johnny Oshika for this code.
726
- // http://stackoverflow.com/questions/7567404/backbone-js-repopulate-or-recreate-the-view/7607853#7607853
727
- Marionette.BindTo = {
728
- // Store the event binding in array so it can be unbound
729
- // easily, at a later point in time.
730
- bindTo: function (obj, eventName, callback, context) {
731
- context = context || this;
732
- obj.on(eventName, callback, context);
1132
+ // Function level methods to create modules
1133
+ _.extend(Marionette.Module, {
733
1134
 
734
- if (!this.bindings) { this.bindings = []; }
1135
+ // Create a module, hanging off the app parameter as the parent object.
1136
+ create: function(app, moduleNames, moduleDefinition){
1137
+ var that = this;
1138
+ var parentModule = app;
1139
+ moduleNames = moduleNames.split(".");
735
1140
 
736
- var binding = {
737
- obj: obj,
738
- eventName: eventName,
739
- callback: callback,
740
- context: context
741
- }
1141
+ // get the custom args passed in after the module definition and
1142
+ // get rid of the module name and definition function
1143
+ var customArgs = slice.apply(arguments);
1144
+ customArgs.splice(0, 3);
742
1145
 
743
- this.bindings.push(binding);
1146
+ // Loop through all the parts of the module definition
1147
+ var length = moduleNames.length;
1148
+ _.each(moduleNames, function(moduleName, i){
1149
+ var isLastModuleInChain = (i === length-1);
744
1150
 
745
- return binding;
746
- },
1151
+ var module = that._getModuleDefinition(parentModule, moduleName, app, customArgs);
1152
+ module.config.options = that._getModuleOptions(parentModule, moduleDefinition);
747
1153
 
748
- // Unbind from a single binding object. Binding objects are
749
- // returned from the `bindTo` method call.
750
- unbindFrom: function(binding){
751
- binding.obj.off(binding.eventName, binding.callback);
752
- this.bindings = _.reject(this.bindings, function(bind){return bind === binding});
753
- },
1154
+ // if it's the first module in the chain, configure it
1155
+ // for auto-start, as specified by the options
1156
+ if (isLastModuleInChain){
1157
+ that._configureAutoStart(app, module);
1158
+ }
754
1159
 
755
- // Unbind all of the events that we have stored.
756
- unbindAll: function () {
757
- var that = this;
1160
+ // Only add a module definition and initializer when this is
1161
+ // the last module in a "parent.child.grandchild" hierarchy of
1162
+ // module names
1163
+ if (isLastModuleInChain && module.config.options.hasDefinition){
1164
+ module.addDefinition(module.config.options.definition);
1165
+ }
758
1166
 
759
- // The `unbindFrom` call removes elements from the array
760
- // while it is being iterated, so clone it first.
761
- var bindings = _.map(this.bindings, _.identity);
762
- _.each(bindings, function (binding, index) {
763
- that.unbindFrom(binding);
1167
+ // Reset the parent module so that the next child
1168
+ // in the list will be added to the correct parent
1169
+ parentModule = module;
764
1170
  });
765
- }
766
- };
767
1171
 
768
- // Callbacks
769
- // ---------
1172
+ // Return the last module in the definition chain
1173
+ return parentModule;
1174
+ },
770
1175
 
771
- // A simple way of managing a collection of callbacks
772
- // and executing them at a later point in time, using jQuery's
773
- // `Deferred` object.
774
- Marionette.Callbacks = function(){
775
- this.deferred = $.Deferred();
776
- this.promise = this.deferred.promise();
777
- };
1176
+ _configureAutoStart: function(app, module){
1177
+ // Only add the initializer if it's the first module, and
1178
+ // if it is set to auto-start, and if it has not yet been added
1179
+ if (module.config.options.startWithParent && !module.config.autoStartConfigured){
1180
+ // start the module when the app starts
1181
+ app.addInitializer(function(options){
1182
+ module.start(options);
1183
+ });
1184
+ }
778
1185
 
779
- _.extend(Marionette.Callbacks.prototype, {
780
-
781
- // Add a callback to be executed. Callbacks added here are
782
- // guaranteed to execute, even if they are added after the
783
- // `run` method is called.
784
- add: function(callback){
785
- this.promise.done(function(context, options){
786
- callback.call(context, options);
787
- });
1186
+ // prevent this module from being configured for
1187
+ // auto start again. the first time the module
1188
+ // is defined, determines it's auto-start
1189
+ module.config.autoStartConfigured = true;
788
1190
  },
789
1191
 
790
- // Run all registered callbacks with the context specified.
791
- // Additional callbacks can be added after this has been run
792
- // and they will still be executed.
793
- run: function(context, options){
794
- this.deferred.resolve(context, options);
795
- }
796
- });
1192
+ _getModuleDefinition: function(parentModule, moduleName, app, customArgs){
1193
+ // Get an existing module of this name if we have one
1194
+ var module = parentModule[moduleName];
797
1195
 
798
- // Event Aggregator
799
- // ----------------
1196
+ if (!module){
1197
+ // Create a new module if we don't have one
1198
+ module = new Marionette.Module(moduleName, app, customArgs);
1199
+ parentModule[moduleName] = module;
1200
+ // store the module on the parent
1201
+ parentModule.submodules[moduleName] = module;
1202
+ }
800
1203
 
801
- // A pub-sub object that can be used to decouple various parts
802
- // of an application through event-driven architecture.
803
- Marionette.EventAggregator = function(options){
804
- _.extend(this, options);
805
- };
1204
+ return module;
1205
+ },
806
1206
 
807
- _.extend(Marionette.EventAggregator.prototype, Backbone.Events, Marionette.BindTo, {
808
- // Assumes the event aggregator itself is the
809
- // object being bound to.
810
- bindTo: function(eventName, callback, context){
811
- return Marionette.BindTo.bindTo.call(this, this, eventName, callback, context);
1207
+ _getModuleOptions: function(parentModule, moduleDefinition){
1208
+ // default to starting the module with the app
1209
+ var options = {
1210
+ startWithParent: true,
1211
+ hasDefinition: !!moduleDefinition
1212
+ };
1213
+
1214
+ // short circuit if we don't have a module definition
1215
+ if (!options.hasDefinition){ return options; }
1216
+
1217
+ if (_.isFunction(moduleDefinition)){
1218
+ // if the definition is a function, assign it directly
1219
+ // and use the defaults
1220
+ options.definition = moduleDefinition;
1221
+
1222
+ } else {
1223
+
1224
+ // the definition is an object.
1225
+
1226
+ // grab the "define" attribute
1227
+ options.hasDefinition = !!moduleDefinition.define;
1228
+ options.definition = moduleDefinition.define;
1229
+
1230
+ // grab the "startWithParent" attribute if one exists
1231
+ if (moduleDefinition.hasOwnProperty("startWithParent")){
1232
+ options.startWithParent = moduleDefinition.startWithParent;
1233
+ }
1234
+ }
1235
+
1236
+ return options;
812
1237
  }
813
1238
  });
814
1239
 
815
1240
  // Template Cache
816
1241
  // --------------
817
-
1242
+
818
1243
  // Manage templates stored in `<script>` blocks,
819
1244
  // caching them for faster access.
820
- Marionette.TemplateCache = {
821
- templates: {},
822
- loaders: {},
1245
+ Marionette.TemplateCache = function(templateId){
1246
+ this.templateId = templateId;
1247
+ };
1248
+
1249
+ // TemplateCache object-level methods. Manage the template
1250
+ // caches from these method calls instead of creating
1251
+ // your own TemplateCache instances
1252
+ _.extend(Marionette.TemplateCache, {
1253
+ templateCaches: {},
823
1254
 
824
1255
  // Get the specified template by id. Either
825
1256
  // retrieves the cached version, or loads it
826
1257
  // from the DOM.
827
1258
  get: function(templateId){
828
1259
  var that = this;
829
- var templateRetrieval = $.Deferred();
830
- var cachedTemplate = this.templates[templateId];
1260
+ var cachedTemplate = this.templateCaches[templateId];
831
1261
 
832
- if (cachedTemplate){
833
- templateRetrieval.resolve(cachedTemplate);
834
- } else {
835
- var loader = this.loaders[templateId];
836
- if(loader) {
837
- templateRetrieval = loader;
838
- } else {
839
- this.loaders[templateId] = templateRetrieval;
1262
+ if (!cachedTemplate){
1263
+ cachedTemplate = new Marionette.TemplateCache(templateId);
1264
+ this.templateCaches[templateId] = cachedTemplate;
1265
+ }
1266
+
1267
+ return cachedTemplate.load();
1268
+ },
840
1269
 
841
- this.loadTemplate(templateId, function(template){
842
- delete that.loaders[templateId];
843
- that.templates[templateId] = template;
844
- templateRetrieval.resolve(template);
845
- });
1270
+ // Clear templates from the cache. If no arguments
1271
+ // are specified, clears all templates:
1272
+ // `clear()`
1273
+ //
1274
+ // If arguments are specified, clears each of the
1275
+ // specified templates from the cache:
1276
+ // `clear("#t1", "#t2", "...")`
1277
+ clear: function(){
1278
+ var i;
1279
+ var length = arguments.length;
1280
+
1281
+ if (length > 0){
1282
+ for(i=0; i<length; i++){
1283
+ delete this.templateCaches[arguments[i]];
846
1284
  }
1285
+ } else {
1286
+ this.templateCaches = {};
1287
+ }
1288
+ }
1289
+ });
1290
+
1291
+ // TemplateCache instance methods, allowing each
1292
+ // template cache object to manage it's own state
1293
+ // and know whether or not it has been loaded
1294
+ _.extend(Marionette.TemplateCache.prototype, {
1295
+
1296
+ // Internal method to load the template asynchronously.
1297
+ load: function(){
1298
+ var that = this;
847
1299
 
1300
+ // Guard clause to prevent loading this template more than once
1301
+ if (this.compiledTemplate){
1302
+ return this.compiledTemplate;
848
1303
  }
849
1304
 
850
- return templateRetrieval.promise();
1305
+ // Load the template and compile it
1306
+ var template = this.loadTemplate(this.templateId);
1307
+ this.compiledTemplate = this.compileTemplate(template);
1308
+
1309
+ return this.compiledTemplate;
851
1310
  },
852
1311
 
853
1312
  // Load a template from the DOM, by default. Override
854
1313
  // this method to provide your own template retrieval,
855
1314
  // such as asynchronous loading from a server.
856
- loadTemplate: function(templateId, callback){
1315
+ loadTemplate: function(templateId){
857
1316
  var template = $(templateId).html();
858
1317
 
859
- // Make sure we have a template before trying to compile it
860
1318
  if (!template || template.length === 0){
861
1319
  var msg = "Could not find template: '" + templateId + "'";
862
1320
  var err = new Error(msg);
@@ -864,9 +1322,7 @@
864
1322
  throw err;
865
1323
  }
866
1324
 
867
- template = this.compileTemplate(template);
868
-
869
- callback.call(this, template);
1325
+ return template;
870
1326
  },
871
1327
 
872
1328
  // Pre-compile the template before caching it. Override
@@ -875,159 +1331,112 @@
875
1331
  // the template engine used (Handebars, etc).
876
1332
  compileTemplate: function(rawTemplate){
877
1333
  return _.template(rawTemplate);
878
- },
879
-
880
- // Clear templates from the cache. If no arguments
881
- // are specified, clears all templates:
882
- // `clear()`
883
- //
884
- // If arguments are specified, clears each of the
885
- // specified templates from the cache:
886
- // `clear("#t1", "#t2", "...")`
887
- clear: function(){
888
- var i;
889
- var length = arguments.length;
890
-
891
- if (length > 0){
892
- for(i=0; i<length; i++){
893
- delete this.templates[arguments[i]];
894
- }
895
- } else {
896
- this.templates = {};
897
- }
898
1334
  }
899
- };
1335
+ });
1336
+
900
1337
 
901
1338
  // Renderer
902
1339
  // --------
903
-
1340
+
904
1341
  // Render a template with data by passing in the template
905
1342
  // selector and the data to render.
906
1343
  Marionette.Renderer = {
907
1344
 
908
1345
  // Render a template with data. The `template` parameter is
909
1346
  // passed to the `TemplateCache` object to retrieve the
910
- // actual template. Override this method to provide your own
1347
+ // template function. Override this method to provide your own
911
1348
  // custom rendering and template handling for all of Marionette.
912
1349
  render: function(template, data){
913
- var that = this;
914
- var asyncRender = $.Deferred();
915
-
916
- var templateRetrieval = Marionette.TemplateCache.get(template);
917
-
918
- $.when(templateRetrieval).then(function(template){
919
- var html = that.renderTemplate(template, data);
920
- asyncRender.resolve(html);
921
- });
922
-
923
- return asyncRender.promise();
924
- },
925
-
926
- // Default implementation uses underscore.js templates. Override
927
- // this method to use your own templating engine.
928
- renderTemplate: function(template, data){
929
- var html = template(data);
1350
+ var templateFunc = typeof template === 'function' ? template : Marionette.TemplateCache.get(template);
1351
+ var html = templateFunc(data);
930
1352
  return html;
931
1353
  }
1354
+ };
1355
+
1356
+
1357
+ // Callbacks
1358
+ // ---------
932
1359
 
1360
+ // A simple way of managing a collection of callbacks
1361
+ // and executing them at a later point in time, using jQuery's
1362
+ // `Deferred` object.
1363
+ Marionette.Callbacks = function(){
1364
+ this._deferred = $.Deferred();
1365
+ this._callbacks = [];
933
1366
  };
934
1367
 
935
- // Modules
936
- // -------
1368
+ _.extend(Marionette.Callbacks.prototype, {
937
1369
 
938
- // The "Modules" object builds modules on an
939
- // object that it is attached to. It should not be
940
- // used on it's own, but should be attached to
941
- // another object that will define modules.
942
- Marionette.Modules = {
1370
+ // Add a callback to be executed. Callbacks added here are
1371
+ // guaranteed to execute, even if they are added after the
1372
+ // `run` method is called.
1373
+ add: function(callback, contextOverride){
1374
+ this._callbacks.push({cb: callback, ctx: contextOverride});
943
1375
 
944
- // Add modules to the application, providing direct
945
- // access to your applicaiton object, Backbone,
946
- // Marionette, jQuery and Underscore as parameters
947
- // to a callback function.
948
- module: function(moduleNames, moduleDefinition){
949
- var moduleName, module, moduleOverride;
950
- var parentModule = this;
951
- var parentApp = this;
952
- var moduleNames = moduleNames.split(".");
1376
+ this._deferred.done(function(context, options){
1377
+ if (contextOverride){ context = contextOverride; }
1378
+ callback.call(context, options);
1379
+ });
1380
+ },
953
1381
 
954
- // Loop through all the parts of the module definition
955
- var length = moduleNames.length;
956
- for(var i = 0; i < length; i++){
957
- var isLastModuleInChain = (i === length-1);
1382
+ // Run all registered callbacks with the context specified.
1383
+ // Additional callbacks can be added after this has been run
1384
+ // and they will still be executed.
1385
+ run: function(options, context){
1386
+ this._deferred.resolve(context, options);
1387
+ },
958
1388
 
959
- // Get the module name, and check if it exists on
960
- // the current parent already
961
- moduleName = moduleNames[i];
962
- module = parentModule[moduleName];
1389
+ // Resets the list of callbacks to be run, allowing the same list
1390
+ // to be run multiple times - whenever the `run` method is called.
1391
+ reset: function(){
1392
+ var that = this;
1393
+ var callbacks = this._callbacks;
1394
+ this._deferred = $.Deferred();
1395
+ this._callbacks = [];
1396
+ _.each(callbacks, function(cb){
1397
+ that.add(cb.cb, cb.ctx);
1398
+ });
1399
+ }
1400
+ });
963
1401
 
964
- // Create a new module if we don't have one already
965
- if (!module){
966
- module = new Marionette.Application();
967
- }
968
1402
 
969
- // Check to see if we need to run the definition
970
- // for the module. Only run the definition if one
971
- // is supplied, and if we're at the last segment
972
- // of the "Module.Name" chain.
973
- if (isLastModuleInChain && moduleDefinition){
974
- moduleOverride = moduleDefinition(module, parentApp, Backbone, Marionette, jQuery, _);
975
- // If we have a module override, use it instead.
976
- if (moduleOverride){
977
- module = moduleOverride;
978
- }
979
- }
1403
+ // Event Aggregator
1404
+ // ----------------
980
1405
 
981
- // If the defined module is not what we are
982
- // currently storing as the module, replace it
983
- if (parentModule[moduleName] !== module){
984
- parentModule[moduleName] = module;
985
- }
1406
+ // A pub-sub object that can be used to decouple various parts
1407
+ // of an application through event-driven architecture.
1408
+ Marionette.EventAggregator = Marionette.EventBinder.extend({
986
1409
 
987
- // Reset the parent module so that the next child
988
- // in the list will be added to the correct parent
989
- parentModule = module;
990
- }
1410
+ // Extend any provided options directly on to the event binder
1411
+ constructor: function(options){
1412
+ Marionette.EventBinder.apply(this, arguments);
1413
+ _.extend(this, options);
1414
+ },
991
1415
 
992
- // Return the last module in the definition chain
993
- return module;
1416
+ // Override the `bindTo` method to ensure that the event aggregator
1417
+ // is used as the event binding storage
1418
+ bindTo: function(eventName, callback, context){
1419
+ return Marionette.EventBinder.prototype.bindTo.call(this, this, eventName, callback, context);
994
1420
  }
995
- };
1421
+ });
1422
+
1423
+ // Copy the basic Backbone.Events on to the event aggregator
1424
+ _.extend(Marionette.EventAggregator.prototype, Backbone.Events);
1425
+
1426
+ // Copy the `extend` function used by Backbone's classes
1427
+ Marionette.EventAggregator.extend = Backbone.View.extend;
1428
+
996
1429
 
997
1430
  // Helpers
998
1431
  // -------
999
1432
 
1000
1433
  // For slicing `arguments` in functions
1001
1434
  var slice = Array.prototype.slice;
1002
-
1003
- // Copy the `extend` function used by Backbone's classes
1004
- var extend = Marionette.View.extend;
1005
- Marionette.Region.extend = extend;
1006
- Marionette.Application.extend = extend;
1007
-
1008
- // Copy the `modules` feature on to the `Application` object
1009
- Marionette.Application.prototype.module = Marionette.Modules.module;
1010
-
1011
- // Copy the features of `BindTo` on to these objects
1012
- _.extend(Marionette.View.prototype, Marionette.BindTo);
1013
- _.extend(Marionette.Application.prototype, Marionette.BindTo);
1014
- _.extend(Marionette.Region.prototype, Marionette.BindTo);
1015
-
1016
- // A simple wrapper method for deferring a callback until
1017
- // after another method has been called, passing the
1018
- // results of the first method to the second. Uses jQuery's
1019
- // deferred / promise objects, and $.when/then to make it
1020
- // work.
1021
- var callDeferredMethod = function(fn, callback, context){
1022
- var promise;
1023
- if (fn) { promise = fn.call(context); }
1024
- $.when(promise).then(callback);
1025
- }
1026
1435
 
1027
1436
 
1028
- return Marionette;
1029
- })(Backbone, _, window.jQuery || window.Zepto || window.ender);
1437
+ return Marionette;
1438
+ })(Backbone, _, window.jQuery || window.Zepto || window.ender);
1030
1439
 
1031
- return Backbone.Marionette;
1440
+ return Backbone.Marionette;
1032
1441
 
1033
- }));
1442
+ }));