Skip to content

Commit

Permalink
onBeforeAttach and onAttach for CollectionView child views
Browse files Browse the repository at this point in the history
  • Loading branch information
ianmstew committed Apr 27, 2015
1 parent 94012e8 commit 17a4870
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 42 deletions.
1 change: 1 addition & 0 deletions SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<script src="test/unit/item-view.spec.js"></script>
<script src="test/unit/layout-view.dynamic-regions.spec.js"></script>
<script src="test/unit/layout-view.spec.js"></script>
<script src="test/unit/merge-options.spec.js"></script>
<script src="test/unit/mixin-underscore-collection.spec.js"></script>
<script src="test/unit/module.spec.js"></script>
<script src="test/unit/module.stop.spec.js"></script>
Expand Down
111 changes: 87 additions & 24 deletions src/collection-view.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* jshint maxstatements: 14 */
/* jshint maxstatements: 20, maxcomplexity: 7 */

// Collection View
// ---------------
Expand All @@ -22,15 +22,17 @@ Marionette.CollectionView = Marionette.View.extend({
// option to pass `{comparator: compFunction()}` to allow the `CollectionView`
// to use a custom sort order for the collection.
constructor: function(options) {

this.once('render', this._initialEvents);
this._initChildViewStorage();

Marionette.View.apply(this, arguments);

this.on('before:show', this._onBeforeShowCalled);
this.on('show', this._onShowCalled);

this.on({
'before:show': this._onBeforeShowCalled,
'show': this._onShowCalled,
'before:attach': this._onBeforeAttachCalled,
'attach': this._onAttachCalled
});
this.initRenderBuffer();
},

Expand All @@ -47,23 +49,38 @@ Marionette.CollectionView = Marionette.View.extend({
},

endBuffering: function() {
// Only trigger attach if already shown and attached, otherwise Region#show() handles this.
var canTriggerAttach = this._isShown && Marionette.isNodeAttached(this.el);
var nestedViews;

this.isBuffering = false;

if (this._isShown) {
this._triggerShowMultiple(this._bufferedChildren, 'before:');
this._triggerMethodMany(this._bufferedChildren, this, 'before:show');
}
if (canTriggerAttach && this._triggerBeforeAttach) {
nestedViews = this._getNestedViews();
this._triggerMethodMany(nestedViews, this, 'before:attach');
}

this.attachBuffer(this, this._createBuffer());

if (canTriggerAttach && this._triggerAttach) {
nestedViews = this._getNestedViews();
this._triggerMethodMany(nestedViews, this, 'attach');
}
if (this._isShown) {
this._triggerShowMultiple(this._bufferedChildren);
this._triggerMethodMany(this._bufferedChildren, this, 'show');
}
this.initRenderBuffer();
},

_triggerShowMultiple: function(views, prefix) {
var eventName = (prefix || '') + 'show';
_.each(views, function(view) {
Marionette.triggerMethodOn(view, eventName, view);
}, this);
_triggerMethodMany: function(targets, source, eventName) {
var args = _.drop(arguments, 3);

_.each(targets, function(target) {
Marionette.triggerMethodOn.apply(target, [target, eventName, target, source].concat(args));
});
},

// Configured the initial events that the collection view
Expand Down Expand Up @@ -104,6 +121,9 @@ Marionette.CollectionView = Marionette.View.extend({
},

_onBeforeShowCalled: function() {
// Reset attach event flags at the top of the Region#show() event lifecycle; if the Region's
// show() options permit onBeforeAttach/onAttach events, these flags will be set true again.
this._triggerBeforeAttach = this._triggerAttach = false;
this.children.each(function(childView) {
Marionette.triggerMethodOn(childView, 'before:show', childView);
});
Expand All @@ -115,6 +135,16 @@ Marionette.CollectionView = Marionette.View.extend({
});
},

// If during Region#show() onBeforeAttach was fired, continue firing it for child views
_onBeforeAttachCalled: function() {
this._triggerBeforeAttach = true;
},

// If during Region#show() onAttach was fired, continue firing it for child views
_onAttachCalled: function() {
this._triggerAttach = true;
},

// Render children views. Override this method to
// provide your own implementation of a render function for
// the collection view.
Expand Down Expand Up @@ -295,6 +325,10 @@ Marionette.CollectionView = Marionette.View.extend({
// but "add:child" events are not fired, and the event from
// emptyView are not forwarded
addEmptyView: function(child, EmptyView) {
// Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
// Region#show() handles this.
var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
var nestedViews;

// get the emptyViewOptions, falling back to childViewOptions
var emptyViewOptions = this.getOption('emptyViewOptions') ||
Expand All @@ -312,8 +346,7 @@ Marionette.CollectionView = Marionette.View.extend({
// Proxy emptyView events
this.proxyChildEvents(view);

// trigger the 'before:show' event on `view` if the collection view
// has already been shown
// trigger the 'before:show' event on `view` if the collection view has already been shown
if (this._isShown) {
Marionette.triggerMethodOn(view, 'before:show', view);
}
Expand All @@ -322,11 +355,24 @@ Marionette.CollectionView = Marionette.View.extend({
// remove and/or close it later
this.children.add(view);

// Trigger `before:attach` following `render` to avoid adding logic and event triggers
// to public method `renderChildView()`.
if (canTriggerAttach && this._triggerBeforeAttach) {
nestedViews = [view].concat(view._getNestedViews());
view.once('render', function() {
this._triggerMethodMany(nestedViews, this, 'before:attach');
}, this);
}

// Render it and show it
this.renderChildView(view, this._emptyViewIndex);

// call the 'show' method if the collection view
// has already been shown
// Trigger `attach`
if (canTriggerAttach && this._triggerAttach) {
nestedViews = [view].concat(view._getNestedViews());
this._triggerMethodMany(nestedViews, this, 'attach');
}
// call the 'show' method if the collection view has already been shown
if (this._isShown) {
Marionette.triggerMethodOn(view, 'show', view);
}
Expand Down Expand Up @@ -364,7 +410,9 @@ Marionette.CollectionView = Marionette.View.extend({
// increment indices of views after this one
this._updateIndices(view, true, index);

this.triggerMethod('before:add:child', view);
this._addChildView(view, index);
this.triggerMethod('add:child', view);

view._parent = this;

Expand Down Expand Up @@ -394,27 +442,42 @@ Marionette.CollectionView = Marionette.View.extend({
// Internal Method. Add the view to children and render it at
// the given index.
_addChildView: function(view, index) {
// Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
// Region#show() handles this.
var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
var nestedViews;

// set up the child view event forwarding
this.proxyChildEvents(view);

this.triggerMethod('before:add:child', view);

// trigger the 'before:show' event on `view` if the collection view
// has already been shown
// trigger the 'before:show' event on `view` if the collection view has already been shown
if (this._isShown && !this.isBuffering) {
Marionette.triggerMethodOn(view, 'before:show', view);
}

// Store the child view itself so we can properly
// remove and/or destroy it later
// Store the child view itself so we can properly remove and/or destroy it later
this.children.add(view);

// Trigger `before:attach` following `render` to avoid adding logic and event triggers
// to public method `renderChildView()`.
if (canTriggerAttach && this._triggerBeforeAttach) {
nestedViews = [view].concat(view._getNestedViews());
view.once('render', function() {
this._triggerMethodMany(nestedViews, this, 'before:attach');
}, this);
}

this.renderChildView(view, index);

// Trigger `attach`
if (canTriggerAttach && this._triggerAttach) {
nestedViews = [view].concat(view._getNestedViews());
this._triggerMethodMany(nestedViews, this, 'attach');
}
// Trigger `show`
if (this._isShown && !this.isBuffering) {
Marionette.triggerMethodOn(view, 'show', view);
}

this.triggerMethod('add:child', view);
},

// render the child view
Expand Down
46 changes: 28 additions & 18 deletions test/unit/collection-view.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ describe('collection view', function() {
onDomRefresh: function() {},
});

this.CollectionView = Backbone.Marionette.CollectionView.extend({
childView: this.ChildView
this.DeepEqualChildView = this.ChildView.extend({
// Init region manager creates a circular reference, which explodes Sinon's deep equals
// assertion. Some tests do not care if the view has a region manager or not, but do care
// about deep equality.
_initializeRegions: function() {},
// The View's destroy method tries to destroy the RegionManager, which, from the above,
// does not exist.
destroy: Marionette.View.prototype.destroy
});

this.MockCollectionView = Backbone.Marionette.CollectionView.extend({
this.CollectionView = Marionette.CollectionView.extend({
childView: this.ChildView,
onBeforeRender: function() {
return this.isRendered;
Expand All @@ -53,6 +59,10 @@ describe('collection view', function() {
onRenderCollection: function() {},
onBeforeRenderCollection: function() {}
});

this.DeepEqualCollectionView = this.CollectionView.extend({
childView: this.DeepEqualChildView,
});
});

// Collection View Specs
Expand All @@ -61,7 +71,7 @@ describe('collection view', function() {
describe('before rendering a collection view', function() {
beforeEach(function() {
this.collection = new Backbone.Collection([]);
this.CollectionView = this.MockCollectionView.extend({
this.CollectionView = this.DeepEqualCollectionView.extend({
sort: function() { return 1; }
});

Expand Down Expand Up @@ -123,7 +133,7 @@ describe('collection view', function() {

this.childViewRender = this.sinon.stub();

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
collection: this.collection
});

Expand Down Expand Up @@ -262,7 +272,7 @@ describe('collection view', function() {

this.collection = new Backbone.Collection([{foo: 'bar'}, {foo: 'baz'}]);

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
collection: this.collection
});

Expand All @@ -280,7 +290,7 @@ describe('collection view', function() {

describe('when rendering a collection view without a collection', function() {
beforeEach(function() {
this.collectionView = new this.MockCollectionView();
this.collectionView = new this.DeepEqualCollectionView();

this.sinon.spy(this.collectionView, 'onRender');
this.sinon.spy(this.collectionView, 'onBeforeRender');
Expand Down Expand Up @@ -383,7 +393,7 @@ describe('collection view', function() {
describe('when a model is added to the collection', function() {
beforeEach(function() {
this.collection = new Backbone.Collection();
this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
childView: this.ChildView,
collection: this.collection
});
Expand Down Expand Up @@ -419,7 +429,7 @@ describe('collection view', function() {
beforeEach(function() {
this.collection = new Backbone.Collection({foo: 'bar'});

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
childView: this.ChildView,
collection: this.collection
});
Expand Down Expand Up @@ -517,7 +527,7 @@ describe('collection view', function() {
this.collection = new Backbone.Collection();
this.collection.add(this.model);

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
childView: this.ChildView,
collection: this.collection
});
Expand Down Expand Up @@ -801,7 +811,7 @@ describe('collection view', function() {
this.model = new Backbone.Model({foo: 'bar'});
this.collection = new Backbone.Collection([this.model]);

this.collectionView = new this.MockCollectionView({collection: this.collection});
this.collectionView = new this.DeepEqualCollectionView({collection: this.collection});
this.collectionView.on('childview:some:event', this.someEventSpy);
this.collectionView.render();

Expand All @@ -821,7 +831,7 @@ describe('collection view', function() {

describe('when configuring a custom childViewEventPrefix', function() {
beforeEach(function() {
this.CollectionView = this.MockCollectionView.extend({
this.CollectionView = this.DeepEqualCollectionView.extend({
childViewEventPrefix: 'myPrefix'
});

Expand Down Expand Up @@ -853,7 +863,7 @@ describe('collection view', function() {
this.model = new Backbone.Model({foo: 'bar'});
this.collection = new Backbone.Collection([this.model]);

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
childView: Backbone.Marionette.ItemView.extend({
template: function() { return '<%= foo %>'; }
}),
Expand Down Expand Up @@ -908,7 +918,7 @@ describe('collection view', function() {
this.model = new Backbone.Model({foo: 'bar'});
this.collection = new Backbone.Collection([this.model]);

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
template: '#itemTemplate',
collection: this.collection
});
Expand Down Expand Up @@ -942,7 +952,7 @@ describe('collection view', function() {
this.model = new Backbone.Model({foo: 'bar'});
this.collection = new Backbone.Collection([this.model]);

this.collectionView = new this.MockCollectionView({
this.collectionView = new this.DeepEqualCollectionView({
template: '#itemTemplate',
collection: this.collection
});
Expand Down Expand Up @@ -1057,7 +1067,7 @@ describe('collection view', function() {

describe('when calling childEvents via a childEvents method', function() {
beforeEach(function() {
this.CollectionView = this.MockCollectionView.extend({
this.CollectionView = this.DeepEqualCollectionView.extend({
childEvents: function() {
return {
'some:event': 'someEvent'
Expand Down Expand Up @@ -1092,7 +1102,7 @@ describe('collection view', function() {
beforeEach(function() {
this.onSomeEventSpy = this.sinon.stub();

this.CollectionView = this.MockCollectionView.extend({
this.CollectionView = this.DeepEqualCollectionView.extend({
childEvents: {
'some:event': this.onSomeEventSpy
}
Expand Down Expand Up @@ -1120,7 +1130,7 @@ describe('collection view', function() {

describe('when calling childEvents via the childEvents hash with a string of the function name', function() {
beforeEach(function() {
this.CollectionView = this.MockCollectionView.extend({
this.CollectionView = this.DeepEqualCollectionView.extend({
childEvents: {
'some:event': 'someEvent'
}
Expand Down
Loading

0 comments on commit 17a4870

Please sign in to comment.