Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View hooks for native and non-jQuery libraries #3003

Merged
merged 92 commits into from
Mar 16, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
02d270f
revert c1e62cda: add `View#make` back in for overriding
akre54 Feb 18, 2014
e9543ee
add `View#delegate` as a single event listener version of delegateEve…
akre54 Feb 18, 2014
aacd0e1
save two lines in View#make: jquery handles nulls nicely
akre54 Feb 19, 2014
19d82b8
Move delegate up and move check into undelegateEvents
wyuenho Feb 19, 2014
846de91
Speed up method calls in delegateEvents and delegate
wyuenho Feb 19, 2014
54252e6
Further isolate $.remove by exposing removeElement in case subclasses…
wyuenho Feb 19, 2014
33e23df
Rename make to _createContext and have _ensureElement and setElement …
wyuenho Feb 19, 2014
8ab8646
Prefix all hooks with _ and introduce _undelegate
wyuenho Feb 19, 2014
31bb428
Revert introduction of _undelegate
wyuenho Feb 19, 2014
15cb64e
Shortened _createContext
wyuenho Feb 20, 2014
09d3762
_createContext is side-effect only
wyuenho Feb 20, 2014
f872032
Disallow chaining in hook calls
wyuenho Feb 20, 2014
7c46856
Fix comment
wyuenho Feb 20, 2014
aca58b7
No reason jQuery can't be a little faster too
wyuenho Feb 20, 2014
e14a903
Fix typo
wyuenho Feb 20, 2014
be3a354
Comment for 2 arg form inside _delegate
wyuenho Feb 20, 2014
bde634f
Merge pull request #5 from wyuenho/view-native-hooks
akre54 Feb 21, 2014
fc95eae
Revert "Speed up method calls in delegateEvents and delegate"
akre54 Feb 21, 2014
3cef7fa
Fix test for fc95eae
wyuenho Feb 21, 2014
2b19b00
Call _createContext only once during new View
wyuenho Feb 21, 2014
c80be41
Merge pull request #8 from wyuenho/view-native-hooks
akre54 Feb 21, 2014
557edc7
return `this` from `delegate`
akre54 Feb 22, 2014
933c02e
small cleanup in _createContext
akre54 Feb 27, 2014
6fec7a5
save a var in _createContext
akre54 Feb 27, 2014
6433043
Rename delegate's method param to listener to tell people it's ok to …
wyuenho Mar 2, 2014
7eedd4b
View conformance tests
wyuenho Mar 3, 2014
0cdb38b
revert c1e62cda: add `View#make` back in for overriding
akre54 Feb 18, 2014
90c36ca
add `View#delegate` as a single event listener version of delegateEve…
akre54 Feb 18, 2014
4e5b282
save two lines in View#make: jquery handles nulls nicely
akre54 Feb 19, 2014
be1cba3
Move delegate up and move check into undelegateEvents
wyuenho Feb 19, 2014
3369bf4
Speed up method calls in delegateEvents and delegate
wyuenho Feb 19, 2014
ad44cb4
Further isolate $.remove by exposing removeElement in case subclasses…
wyuenho Feb 19, 2014
5f66f9a
Rename make to _createContext and have _ensureElement and setElement …
wyuenho Feb 19, 2014
d78fb34
Prefix all hooks with _ and introduce _undelegate
wyuenho Feb 19, 2014
1e9cd4a
Revert introduction of _undelegate
wyuenho Feb 19, 2014
9028e33
Shortened _createContext
wyuenho Feb 20, 2014
6ebb6e1
_createContext is side-effect only
wyuenho Feb 20, 2014
18d17db
Disallow chaining in hook calls
wyuenho Feb 20, 2014
390275b
Fix comment
wyuenho Feb 20, 2014
9868bfb
No reason jQuery can't be a little faster too
wyuenho Feb 20, 2014
25c0bee
Fix typo
wyuenho Feb 20, 2014
12abe8e
Comment for 2 arg form inside _delegate
wyuenho Feb 20, 2014
72b88b5
Revert "Speed up method calls in delegateEvents and delegate"
akre54 Feb 21, 2014
20935c8
Fix test for fc95eae
wyuenho Feb 21, 2014
5dc2d09
Call _createContext only once during new View
wyuenho Feb 21, 2014
3edf74c
return `this` from `delegate`
akre54 Feb 22, 2014
e30e6b9
small cleanup in _createContext
akre54 Feb 27, 2014
446ee52
save a var in _createContext
akre54 Feb 27, 2014
fd64254
Rename delegate's method param to listener to tell people it's ok to …
wyuenho Mar 2, 2014
f13181f
rename _createContext to _setEl
akre54 Mar 4, 2014
01f794f
Merge remote-tracking branch 'wyuenho/view-native-hooks' into view-na…
akre54 Mar 4, 2014
a9d066a
test view.$ interface and rename _createContext to _setEl in view test
akre54 Mar 4, 2014
70933e5
fix merge conflict
akre54 Mar 4, 2014
0fca38c
view test cleanup
akre54 Mar 4, 2014
7339bed
Merge branch 'master' into view-native-hooks
akre54 Mar 4, 2014
fca75fb
Fix up global in view test
wyuenho Mar 4, 2014
6cef2f1
Check typeof length instead of instanceof
wyuenho Mar 4, 2014
81809e1
add global Element guard for IE8
akre54 Mar 4, 2014
7e95b9c
short-circuit on attributes
akre54 Mar 4, 2014
c5aa603
rephrase _setEl comment
akre54 Mar 4, 2014
986540f
swap argument ordering to setElement, boolean goes last
akre54 Mar 5, 2014
1cb19be
Test for view.remove and view._removeElement
wyuenho Mar 5, 2014
2711185
Merge pull request #16 from wyuenho/view-native-hooks
akre54 Mar 5, 2014
6d69999
test _removeElement
akre54 Mar 5, 2014
9eff496
Revert "View conformance tests"
akre54 Mar 6, 2014
bef78ef
test setEl and el.parentNode
akre54 Mar 6, 2014
c62708f
use notEqual body isntead of equal null
akre54 Mar 6, 2014
d9f6c64
remove undelegateEvents call from remove
akre54 Mar 9, 2014
58c9a2a
remove undelegateEvents call from remove
akre54 Mar 9, 2014
b840669
Rename _removeElement to _remove and test _remove for event undelegation
wyuenho Mar 8, 2014
393a209
Merge _remove and remove tests and remove _setEl tests
wyuenho Mar 9, 2014
b377e30
Merge remote-tracking branch 'wyuenho/view-native-hooks' into view-na…
akre54 Mar 11, 2014
20ed074
rename _setEl to _setElement for parity
akre54 Mar 11, 2014
be11f3a
add an undelegate method to View and pass back the listener from dele…
akre54 Mar 12, 2014
d21fd79
remove namespace test from custom events test.
akre54 Mar 12, 2014
d99e43b
test for undelegate with handler and selector, and use an element tha…
akre54 Mar 12, 2014
3850d37
add missing semi
akre54 Mar 12, 2014
b59b621
shorter delegate method thanks to jQuery empty selector checks
akre54 Mar 13, 2014
291f3bc
nicer jq test
akre54 Mar 13, 2014
764eef4
shorter undelegate
akre54 Mar 13, 2014
264bd9b
comments cleanup
akre54 Mar 13, 2014
c5c0d2e
Merge branch 'master' into view-native-hooks
akre54 Mar 14, 2014
06586f3
shorter comment
akre54 Mar 14, 2014
f67c7a1
test delegate returns listener
akre54 Mar 14, 2014
3f79ea8
fix leftover merge conflict
akre54 Mar 14, 2014
4e8caf4
no need for repeated comment on `remove`
akre54 Mar 14, 2014
ad39972
Return undefined from delegate
wyuenho Mar 14, 2014
b79bd78
Merge pull request #18 from wyuenho/view-native-hooks
akre54 Mar 14, 2014
72f7da4
A little clarification.
braddunbar Mar 14, 2014
155a738
Merge pull request #19 from braddunbar/view-hooks
akre54 Mar 14, 2014
7180d5c
Add #_setAttributes and clean up #setElement.
braddunbar Mar 14, 2014
1d6eb7a
Merge pull request #20 from braddunbar/view-hooks
akre54 Mar 15, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 46 additions & 21 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -1065,21 +1065,37 @@
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this._removeElement();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive me if I'm rehashing something that's already been discussed, but #undelegateEvents is covered by #_removeElement, no?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems right to me. @akre54?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if the DOM lib is jQuery. You'll need to undelegate events explicitly for everyone else so _removeElement can just focus on just detaching the node. Maybe we should probably renamed it to _detach or _detachElement because that's what it's really doing.

Sent from my iPhone

On 6 Mar, 2014, at 12:43 am, brad dunbar notifications@github.com wrote:

In backbone.js:

 remove: function() {
  •  this.$el.remove();
    
  •  this.undelegateEvents();
    
  •  this._removeElement();
    
    Forgive me if I'm rehashing something that's already been discussed, but #undelegateEvents is covered by _removeElement, no?


Reply to this email directly or view it on GitHub.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DOM lib is jQuery. Everyone else can just call undelegateEvents in their overridden copy of _removeElement, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DOM lib is jQuery. Everyone else can just call undelegateEvents in their overridden copy of _removeElement, right?

Right, this. In the default case we'd be removing events twice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the debate, but if the argument is about this.$el.remove() vs this.$el.detach() I'm in the remove camp.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akre54: @jashkenas and @braddunbar want to remove that line of this.undelegateEvents() inside remove, but I'm saying I put it there for a reason because basically every other implementation will have to call undelegateEvents to clean up at the minimum the handlers View attached. @jashkenas and @braddunbar are saying we should just use $.fn.remove for GC.

I slept on this and I still think that you just can't guarantee View#remove will clean up everything immediately. Here's why:

view.el.addEventListener('click', clickHandler);
view.remove(); // jQuery can't clean up clickHandler
var clickEvent = document.createEvent();
...
view.dispatchEvent(clickEvent);

While the above will still GC el and clickHandler in sane browsers, you can't guarantee that event handlers attached outside of delegateEvents will be detached even today and you can still trigger clickHandler before the whole thing gets GC'ed. So I think the tests for remove shouldn't even try to guarantee that either. You can keep $.fn.remove for backward compatibility, but the tests shouldn't try to guarantee that all event handlers will be detached immediately because it's actually not possible, especially for implementations where jQuery isn't available.

Since we can only guarantee cleaning up the event handlers delegate attached, calling undelegateEvent inside remove becomes a "necessary convenience".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to clean up all event handlers, just the ones set up using Backbone.$ on el and it's descendants. Regardless of what's in #remove, this will be the contract for #_removeElement and thus the extra call is just redundant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you insist, what do you think if I removed undelegateEvents from #remove, renamed _removeElement to _remove and fixed up its test to test for the removal of delegated handlers? I prefer not because removing 2 + 2 handlers separately = removing 4 handlers in one go, so it's just added convenience. I have no strong feeling against removing that undelegateEvents line otherwise.

I think we should document that if you want to be forward compatible with other View classes, you should use delegate instead of $.fn.on, but that's another ticket when we agree that's actually what everyone wants.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Then in that case I agree with Brad and Jeremy. I argued for this point before and I still believe it should be the responsibility of remove.

this.stopListening();
return this;
},

// Change the view's element (`this.el` property), including event
// re-delegation.
// Remove this view's element from the document and all event listeners
// attached to it. Exposed for subclasses using an alternative DOM
// manipulation API.
_removeElement: function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this check was removed due to the dislike of this.$el but can't we just check this.el instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for this.el failed some tests before the test refactoring.

Sent from my iPhone

On 6 Mar, 2014, at 12:47 am, brad dunbar notifications@github.com wrote:

In backbone.js:

 // Change the view's element (`this.el` property), including event
  • // re-delegation.
  • setElement: function(element, delegate) {
  •  if (this.$el) this.undelegateEvents();
    
    I realize this check was removed due to the dislike of this.$el but can't we just check this.el instead?


Reply to this email directly or view it on GitHub.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see now. this.el isn't necessarily a DOM element. Thanks!

this.$el.remove();
},

// Change the view's element (`this.el` property) and re-delegate the
// view's events on the new element.
setElement: function(element) {
if (this.$el) this.undelegateEvents();
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
this.undelegateEvents();
this._setElement(element);
this.delegateEvents();
return this;
},

// Creates the `this.el` and `this.$el` references for this view using the
// given `el` and a hash of `attributes`. `el` can be a CSS selector or an
// HTML string, a jQuery context or an element. Subclasses can override
// this to utilize an alternative DOM manipulation API and are only required
// to set the `this.el` property.
_setElement: function(el) {
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
this.el = this.$el[0];
},

// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
Expand All @@ -1093,37 +1109,40 @@
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return this;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;

var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
this.delegate(match[1], match[2], _.bind(method, this));
}
return this;
},

// Clears all callbacks previously bound to the view with `delegateEvents`.
// Add a single event listener to the view's element (or a child element
// using `selector`). This only works for delegate-able events: not `focus`,
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
delegate: function(eventName, selector, listener) {
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
},

// Clears all callbacks previously bound to the view by `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid);
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},

// A finer-grained `undelegateEvents` for removing a single delegated event.
// `selector` and `listener` are both optional.
undelegate: function(eventName, selector, listener) {
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why include this if it's never called? If you're not using jQuery you'd have to override anyway, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to complement delegate. See #3003 (comment).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. Providing delegate doesn't mean we have to provide undelegate, especially if it's unused.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unused currently because jQuery provides a way to implement undelegateEvents in a single line. It may be best to think of it and undelegate as follows:

{
    undelegateEvents: function() {
        this.undelegate('.delegateEvents' + this.cid);
        return this;
    },
    undelegate: function(eventName, selector, listener) {
        if (this.$el) this.$el.off(eventName, selector, listener);
    }
}

Though we're still going back and forth between namespaces( #3003 (comment) , #3003 (comment) ) . If we end up not using them any longer, I imagine undelegateEvents will more closely reflect delegateEvents and just loop over events, calling undelegate for each.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but that's not the way it's implemented here. And further, I don't think it needs to be. The reason for delegate is to save subclasses from re-implementing the logic for picking apart #events. There's no such requirement for undelegateEvents and as such it can be overwritten in a forward compatible manner without an extra method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you remove just one handler that was added by delegate then?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to? Events are cleaned up when you remove the view via undelegateEvents.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For plugins. Stickit has stickit/unstickit for removing bindings from the view. Stickit needs a way to track its own handlers and remove them when you request for the view to be "unstuck".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @braddunbar is saying you can already track down the reference by looping the event-selector-handler combos. This is fine by me, thought it prevents efficient and direct removal with removeEventListener for native implementations. Price to pay for removing one line.

Sent from my iPhone

On 14 Mar, 2014, at 4:56 pm, Adam Krebs notifications@github.com wrote:

In backbone.js:

   return this;
 },
  • // Remove a single event from the delegated events. selector and listener
  • // are both optional.
  • undelegate: function(eventName, selector, listener) {
  •  eventName += '.delegateEvents' + this.cid;
    
  •  this.$el.off(eventName, selector, listener);
    
  • },
    For plugins. Stickit has stickit/unstickit for removing bindings from the view. Stickit needs a way to track its own handlers and remove them when you request for the view to be "unstuck".


Reply to this email directly or view it on GitHub.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's fair enough. Thanks for sticking with me here @wyuenho, @akre54!


// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
Expand All @@ -1133,11 +1152,17 @@
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el);
this.setElement(document.createElement(_.result(this, 'tagName')));
this._setAttributes(attrs);
} else {
this.setElement(_.result(this, 'el'));
}
},

// Set attributes from a hash on this view's element. Exposed for
// subclasses using an alternative DOM manipulation API.
_setAttributes: function(attributes) {
this.$el.attr(attributes);
}

});
Expand Down
100 changes: 85 additions & 15 deletions test/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@
equal(view.el.other, void 0);
});

test("jQuery", 1, function() {
test("$", 2, function() {
var view = new Backbone.View;
view.setElement('<p><a><b>test</b></a></p>');
strictEqual(view.$('a b').html(), 'test');
var result = view.$('a b');

strictEqual(result[0].innerHTML, 'test');
ok(result.length === +result.length);
});

test("$el", 3, function() {
var view = new Backbone.View;
view.setElement('<p><a><b>test</b></a></p>');
strictEqual(view.el.nodeType, 1);

ok(view.$el instanceof Backbone.$);
strictEqual(view.$el[0], view.el);
});

test("initialize", 1, function() {
Expand Down Expand Up @@ -60,6 +72,17 @@
equal(counter2, 3);
});

test("delegate", 2, function() {
var view = new Backbone.View({el: '#testElement'});
view.delegate('click', 'h1', function() {
ok(true);
});
view.delegate('click', function() {
ok(true);
});
view.$('h1').trigger('click');
});

test("delegateEvents allows functions for callbacks", 3, function() {
var view = new Backbone.View({el: '<p></p>'});
view.counter = 0;
Expand Down Expand Up @@ -114,6 +137,45 @@
equal(counter2, 3);
});

test("undelegate", 0, function() {
view = new Backbone.View({el: '#testElement'});
view.delegate('click', function() { ok(false); });
view.delegate('click', 'h1', function() { ok(false); });

view.undelegate('click');

view.$('h1').trigger('click');
view.$el.trigger('click');
});

test("undelegate with passed handler", 1, function() {
view = new Backbone.View({el: '#testElement'});
var listener = function() { ok(false); };
view.delegate('click', listener);
view.delegate('click', function() { ok(true); });
view.undelegate('click', listener);
view.$el.trigger('click');
});

test("undelegate with selector", 2, function() {
view = new Backbone.View({el: '#testElement'});
view.delegate('click', function() { ok(true); });
view.delegate('click', 'h1', function() { ok(false); });
view.undelegate('click', 'h1');
view.$('h1').trigger('click');
view.$el.trigger('click');
});

test("undelegate with handler and selector", 2, function() {
view = new Backbone.View({el: '#testElement'});
view.delegate('click', function() { ok(true); });
var handler = function(){ ok(false); };
view.delegate('click', 'h1', handler);
view.undelegate('click', 'h1', handler);
view.$('h1').trigger('click');
view.$el.trigger('click');
});

test("_ensureElement with DOM node el", 1, function() {
var View = Backbone.View.extend({
el: document.body
Expand Down Expand Up @@ -201,26 +263,19 @@
equal(5, count);
});

test("custom events, with namespaces", 2, function() {
var count = 0;

test("custom events", 2, function() {
var View = Backbone.View.extend({
el: $('body'),
events: function() {
return {"fake$event.namespaced": "run"};
},
run: function() {
count++;
events: {
"fake$event": function() { ok(true); }
}
});

var view = new View;
$('body').trigger('fake$event').trigger('fake$event');
equal(count, 2);

$('body').off('.namespaced');
$('body').off('fake$event');
$('body').trigger('fake$event');
equal(count, 2);
});

test("#1048 - setElement uses provided object.", 2, function() {
Expand Down Expand Up @@ -277,8 +332,8 @@
test("views stopListening", 0, function() {
var View = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, 'all x', function(){ ok(false); }, this);
this.listenTo(this.collection, 'all x', function(){ ok(false); }, this);
this.listenTo(this.model, 'all x', function(){ ok(false); });
this.listenTo(this.collection, 'all x', function(){ ok(false); });
}
});

Expand Down Expand Up @@ -324,4 +379,19 @@
equal(counter, 2);
});

test("remove", 1, function() {
var view = new Backbone.View;
document.body.appendChild(view.el);

view.delegate('click', function() { ok(false); });
view.listenTo(view, 'all x', function() { ok(false); });

view.remove();
view.$el.trigger('click');
view.trigger('x');

// In IE8 and below, parentNode still exists but is not document.body.
notEqual(view.el.parentNode, document.body);
});

})();