Skip to content

Commit

Permalink
Merge pull request jashkenas#3003 from akre54/view-native-hooks
Browse files Browse the repository at this point in the history
View hooks for native and non-jQuery libraries
  • Loading branch information
jashkenas committed Mar 16, 2014
2 parents 2543be2 + 1d6eb7a commit 3994c1c
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 36 deletions.
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();
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() {
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);
},

// 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);
});

})();

0 comments on commit 3994c1c

Please sign in to comment.