marionette-amd-rails 0.8.4.1 → 0.10.2.1

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.
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
+ }));