From 02d270f4550dcfc60e03699f3528fc36c3d3ecc2 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 18 Feb 2014 17:09:01 -0500 Subject: [PATCH 01/82] revert c1e62cda: add `View#make` back in for overriding --- backbone.js | 15 +++++++++++++-- test/view.js | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 67c290409..3a20c864a 100644 --- a/backbone.js +++ b/backbone.js @@ -1093,6 +1093,18 @@ return this; }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't + // needed, use **make** to manufacture elements, one at a time. + // + // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); + // + make: function(tagName, attributes, content) { + var $el = Backbone.$('<' + tagName + '>'); + if (attributes) $el.attr(attributes); + if (content != null) $el.html(content); + return $el[0]; + }, + // 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 @@ -1102,8 +1114,7 @@ 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, false); + this.setElement(this.make(_.result(this, 'tagName'), attrs), false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 76f0f1db1..5635c9376 100644 --- a/test/view.js +++ b/test/view.js @@ -26,6 +26,22 @@ strictEqual(view.$('a b').html(), 'test'); }); + test("make", 3, function() { + var div = view.make('div', {id: 'test-div'}, "one two three"); + + equal(div.tagName.toLowerCase(), 'div'); + equal(div.id, 'test-div'); + equal($(div).text(), 'one two three'); + }); + + test("make can take falsy values for content", 2, function() { + var div = view.make('div', {id: 'test-div'}, 0); + equal($(div).text(), '0'); + + div = view.make('div', {id: 'test-div'}, ''); + equal($(div).text(), ''); + }); + test("initialize", 1, function() { var View = Backbone.View.extend({ initialize: function() { From e9543ee59192a9ac42435345605e77571f17f4ed Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 18 Feb 2014 17:34:54 -0500 Subject: [PATCH 02/82] add `View#delegate` as a single event listener version of delegateEvents for easier overriding --- backbone.js | 23 ++++++++++++++++------- test/view.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index 3a20c864a..c6427b69a 100644 --- a/backbone.js +++ b/backbone.js @@ -1074,13 +1074,7 @@ 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(eventName, selector, method); } return this; }, @@ -1093,6 +1087,21 @@ return this; }, + // Add a single event listener to the element. + delegate: function(eventName, selector, method) { + if (_.isFunction(selector)) { + method = selector; + selector = undefined; + } + eventName += '.delegateEvents' + this.cid; + if (!selector) { + this.$el.on(eventName, _.bind(method, this)); + } else { + this.$el.on(eventName, selector, _.bind(method, this)); + } + return this; + }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't // needed, use **make** to manufacture elements, one at a time. // diff --git a/test/view.js b/test/view.js index 5635c9376..77f531c6e 100644 --- a/test/view.js +++ b/test/view.js @@ -76,6 +76,21 @@ equal(counter2, 3); }); + test("delegate", 2, function() { + var counter1 = 0, counter2 = 0; + + var view = new Backbone.View({el: '#testElement'}); + view.increment = function(){ counter1++; }; + view.$el.on('click', function(){ counter2++; }); + + view.delegate('click', 'h1', view.increment); + view.delegate('click', view.increment); + + view.$('h1').trigger('click'); + equal(counter1, 2); + equal(counter2, 1); + }) + test("delegateEvents allows functions for callbacks", 3, function() { var view = new Backbone.View({el: '

'}); view.counter = 0; From aacd0e1603538a84d00301dda2518878228cd8cd Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 19 Feb 2014 01:07:25 -0500 Subject: [PATCH 03/82] save two lines in View#make: jquery handles nulls nicely --- backbone.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index c6427b69a..1cce50acd 100644 --- a/backbone.js +++ b/backbone.js @@ -1108,9 +1108,7 @@ // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); // make: function(tagName, attributes, content) { - var $el = Backbone.$('<' + tagName + '>'); - if (attributes) $el.attr(attributes); - if (content != null) $el.html(content); + var $el = Backbone.$('<' + tagName + '>', attributes).html(content); return $el[0]; }, From 19d82b83385582ceeb9ae2a299e5b9a2ccf4aea5 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 01:21:39 +0800 Subject: [PATCH 04/82] Move delegate up and move check into undelegateEvents --- backbone.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index 1cce50acd..fb221dc60 100644 --- a/backbone.js +++ b/backbone.js @@ -1042,7 +1042,7 @@ // Change the view's element (`this.el` property), including event // re-delegation. setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); + this.undelegateEvents(); this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); this.el = this.$el[0]; if (delegate !== false) this.delegateEvents(); @@ -1079,13 +1079,6 @@ return this; }, - // Clears all callbacks previously bound to the view with `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); - return this; - }, // Add a single event listener to the element. delegate: function(eventName, selector, method) { @@ -1102,6 +1095,14 @@ return this; }, + // Clears all callbacks previously bound to the view with `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() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't // needed, use **make** to manufacture elements, one at a time. // From 846de91c88e04c90e2805f7fdea18e4012c94afc Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 01:23:30 +0800 Subject: [PATCH 05/82] Speed up method calls in delegateEvents and delegate --- backbone.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index fb221dc60..bd5afd1af 100644 --- a/backbone.js +++ b/backbone.js @@ -1067,6 +1067,7 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); + var delegate = this.delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1074,23 +1075,20 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; - this.delegate(eventName, selector, method); + method = method.bind && method.bind(this) || _.bind(method, this); + delegate.call(this, eventName, selector, method); } return this; }, - - // Add a single event listener to the element. + // Add a single event listener to the element responding only to the + // optional `selector` or catches all `eventName` events. delegate: function(eventName, selector, method) { - if (_.isFunction(selector)) { - method = selector; - selector = undefined; - } eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, _.bind(method, this)); + this.$el.on(eventName, method); } else { - this.$el.on(eventName, selector, _.bind(method, this)); + this.$el.on(eventName, selector, method); } return this; }, From 54252e618eac184c0f733e17fabb301408ce6f40 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 02:11:57 +0800 Subject: [PATCH 06/82] Further isolate $.remove by exposing removeElement in case subclasses forgot to clean up --- backbone.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index bd5afd1af..c2d1369f2 100644 --- a/backbone.js +++ b/backbone.js @@ -1031,14 +1031,23 @@ return this; }, - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. + // Remove this view by taking the element out of the document, remove all + // the DOM event listeners attached to it, and remove any applicable + // Backbone.Events listeners. remove: function() { - this.$el.remove(); + this.undelegateEvents(); + this.removeElement(); this.stopListening(); return this; }, + // Remove this view's element from the document and remove all the event + // listeners attached to it. + removeElement: function() { + this.$el.remove(); + return this; + }, + // Change the view's element (`this.el` property), including event // re-delegation. setElement: function(element, delegate) { From 33e23df577a48d2b06eda91d5c289fe9bd7c997e Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:11:11 +0800 Subject: [PATCH 07/82] Rename make to _createContext and have _ensureElement and setElement delegate to it --- backbone.js | 29 ++++++++++++++++++----------- test/view.js | 16 +++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/backbone.js b/backbone.js index c2d1369f2..6903da928 100644 --- a/backbone.js +++ b/backbone.js @@ -1052,8 +1052,7 @@ // re-delegation. setElement: function(element, delegate) { this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; + this._createContext(element); if (delegate !== false) this.delegateEvents(); return this; }, @@ -1110,14 +1109,21 @@ return this; }, - // For hooking into small amounts of DOM Elements, where a full-blown template isn't - // needed, use **make** to manufacture elements, one at a time. - // - // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); - // - make: function(tagName, attributes, content) { - var $el = Backbone.$('<' + tagName + '>', attributes).html(content); - return $el[0]; + // Creates the actual context for this view using the given `root` and a + // hash of `attributes`. `root` can be a CSS selector or an HTML string, a + // jQuery context or and Element. + _createContext: function(root, attributes) { + var $el; + if (typeof root == 'string') { + $el = Backbone.$(root); + } else if (root instanceof Backbone.$) { + $el = root; + } else { + $el = Backbone.$(root); + } + if (!_.isEmpty(attributes)) $el.attr(attributes); + this.$el = $el; + return (this.el = $el[0]); }, // Ensure that the View has a DOM element to render into. @@ -1129,7 +1135,8 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this.setElement(this.make(_.result(this, 'tagName'), attrs), false); + var el = document.createElement(_.result(this, 'tagName')); + this.setElement(this._createContext(el, attrs), false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 77f531c6e..cc896007a 100644 --- a/test/view.js +++ b/test/view.js @@ -26,20 +26,14 @@ strictEqual(view.$('a b').html(), 'test'); }); - test("make", 3, function() { - var div = view.make('div', {id: 'test-div'}, "one two three"); + test("_createContext", 5, function() { + var div = view._createContext('
', {id: 'test-div'}); equal(div.tagName.toLowerCase(), 'div'); equal(div.id, 'test-div'); - equal($(div).text(), 'one two three'); - }); - - test("make can take falsy values for content", 2, function() { - var div = view.make('div', {id: 'test-div'}, 0); - equal($(div).text(), '0'); - - div = view.make('div', {id: 'test-div'}, ''); - equal($(div).text(), ''); + equal(view.el, div); + ok(view.$el instanceof Backbone.$); + equal(view.$el[0], div); }); test("initialize", 1, function() { From 8ab8646361fb71fcea659ab0d0b88968227e869d Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:31:36 +0800 Subject: [PATCH 08/82] Prefix all hooks with _ and introduce _undelegate --- backbone.js | 37 ++++++++++++++++++++++++------------- test/view.js | 6 +++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/backbone.js b/backbone.js index 6903da928..ce873c19b 100644 --- a/backbone.js +++ b/backbone.js @@ -1036,14 +1036,15 @@ // Backbone.Events listeners. remove: function() { this.undelegateEvents(); - this.removeElement(); + this._removeElement(); this.stopListening(); return this; }, // Remove this view's element from the document and remove all the event - // listeners attached to it. - removeElement: function() { + // listeners attached to it. Useful for subclasses to override in order to + // utilize an alternative DOM manipulation API. + _removeElement: function() { this.$el.remove(); return this; }, @@ -1075,7 +1076,7 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); - var delegate = this.delegate; + var _delegate = this._delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1084,14 +1085,15 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; method = method.bind && method.bind(this) || _.bind(method, this); - delegate.call(this, eventName, selector, method); + _delegate.call(this, eventName, selector, method); } return this; }, // Add a single event listener to the element responding only to the - // optional `selector` or catches all `eventName` events. - delegate: function(eventName, selector, method) { + // optional `selector` or catches all `eventName` events. Subclasses can + // override this to utilize an alternative DOM event management API. + _delegate: function(eventName, selector, method) { eventName += '.delegateEvents' + this.cid; if (!selector) { this.$el.on(eventName, method); @@ -1101,17 +1103,26 @@ return this; }, - // Clears all callbacks previously bound to the view with `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. + // Undo what `delegateEvents` did. 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() { - if (this.$el) this.$el.off('.delegateEvents' + this.cid); + if (this.$el) this._undelegate(); + return this; + }, + + // Clears all callbacks previously bound to the view by + // `delegateEvents`. Subclasses can override this to utilize an alternative + // DOM event management API. + _undelegate: function() { + this.$el.off('.delegateEvents' + this.cid); return this; }, // Creates the actual context for this view using the given `root` and a - // hash of `attributes`. `root` can be a CSS selector or an HTML string, a - // jQuery context or and Element. + // hash of `attributes` and returns the created element. `root` can be a CSS + // selector or an HTML string, a jQuery context or and Element. Subclasses + // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { var $el; if (typeof root == 'string') { diff --git a/test/view.js b/test/view.js index cc896007a..6263dda07 100644 --- a/test/view.js +++ b/test/view.js @@ -70,15 +70,15 @@ equal(counter2, 3); }); - test("delegate", 2, function() { + test("_delegate", 2, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - view.delegate('click', 'h1', view.increment); - view.delegate('click', view.increment); + view._delegate('click', 'h1', view.increment); + view._delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); From 31bb4289b059d0c23527e175165a09cd7b1fc0f2 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:53:13 +0800 Subject: [PATCH 09/82] Revert introduction of _undelegate --- backbone.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/backbone.js b/backbone.js index ce873c19b..3239e5377 100644 --- a/backbone.js +++ b/backbone.js @@ -1103,19 +1103,11 @@ return this; }, - // Undo what `delegateEvents` did. You usually don't need to use this, but - // may wish to if you have multiple Backbone views attached to the same DOM - // element. + // 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() { - if (this.$el) this._undelegate(); - return this; - }, - - // Clears all callbacks previously bound to the view by - // `delegateEvents`. Subclasses can override this to utilize an alternative - // DOM event management API. - _undelegate: function() { - this.$el.off('.delegateEvents' + this.cid); + if (this.$el) this.$el.off('.delegateEvents' + this.cid); return this; }, From 15cb64ec8ba9fcf85bb5d861606cdee286c552e8 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 13:05:03 +0800 Subject: [PATCH 10/82] Shortened _createContext --- backbone.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index 3239e5377..eabeab817 100644 --- a/backbone.js +++ b/backbone.js @@ -1116,15 +1116,8 @@ // selector or an HTML string, a jQuery context or and Element. Subclasses // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { - var $el; - if (typeof root == 'string') { - $el = Backbone.$(root); - } else if (root instanceof Backbone.$) { - $el = root; - } else { - $el = Backbone.$(root); - } - if (!_.isEmpty(attributes)) $el.attr(attributes); + var $el = root instanceof Backbone.$ ? root : Backbone.$(root); + $el.attr(attributes || {}); this.$el = $el; return (this.el = $el[0]); }, From 09d376237be4036ae59a054030a2cd4bf926cea3 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 14:18:21 +0800 Subject: [PATCH 11/82] _createContext is side-effect only --- backbone.js | 6 +++--- test/view.js | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index eabeab817..68a1bfec9 100644 --- a/backbone.js +++ b/backbone.js @@ -1119,7 +1119,7 @@ var $el = root instanceof Backbone.$ ? root : Backbone.$(root); $el.attr(attributes || {}); this.$el = $el; - return (this.el = $el[0]); + this.el = $el[0]; }, // Ensure that the View has a DOM element to render into. @@ -1131,8 +1131,8 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - var el = document.createElement(_.result(this, 'tagName')); - this.setElement(this._createContext(el, attrs), false); + this._createContext(document.createElement(_.result(this, 'tagName')), attrs); + this.setElement(this.el, false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 6263dda07..8b507aaa2 100644 --- a/test/view.js +++ b/test/view.js @@ -26,14 +26,13 @@ strictEqual(view.$('a b').html(), 'test'); }); - test("_createContext", 5, function() { - var div = view._createContext('
', {id: 'test-div'}); + test("_createContext", 4, function() { + view._createContext('
', {id: 'test-div'}); - equal(div.tagName.toLowerCase(), 'div'); - equal(div.id, 'test-div'); - equal(view.el, div); + equal(view.el.tagName.toLowerCase(), 'div'); + equal(view.el.id, 'test-div'); ok(view.$el instanceof Backbone.$); - equal(view.$el[0], div); + equal(view.$el[0], view.el); }); test("initialize", 1, function() { From f872032b52afbba6e183922286ed3fed126367c3 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 15:22:38 +0800 Subject: [PATCH 12/82] Disallow chaining in hook calls --- backbone.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/backbone.js b/backbone.js index 68a1bfec9..66306d020 100644 --- a/backbone.js +++ b/backbone.js @@ -1046,7 +1046,6 @@ // utilize an alternative DOM manipulation API. _removeElement: function() { this.$el.remove(); - return this; }, // Change the view's element (`this.el` property), including event @@ -1100,7 +1099,6 @@ } else { this.$el.on(eventName, selector, method); } - return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. From 7c468561580993a287d3b1205ec97553cd006d23 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 02:46:20 +0800 Subject: [PATCH 13/82] Fix comment --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 66306d020..c49fff4f9 100644 --- a/backbone.js +++ b/backbone.js @@ -1111,7 +1111,7 @@ // Creates the actual context for this view using the given `root` and a // hash of `attributes` and returns the created element. `root` can be a CSS - // selector or an HTML string, a jQuery context or and Element. Subclasses + // selector or an HTML string, a jQuery context or an element. Subclasses // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { var $el = root instanceof Backbone.$ ? root : Backbone.$(root); From aca58b7659f4771d3aa74f9289b885723877be49 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:13:17 +0800 Subject: [PATCH 14/82] No reason jQuery can't be a little faster too --- backbone.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index c49fff4f9..9826cd179 100644 --- a/backbone.js +++ b/backbone.js @@ -1093,11 +1093,12 @@ // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. _delegate: function(eventName, selector, method) { + var $el = this.$el; eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, method); + $el.on(eventName, method); } else { - this.$el.on(eventName, selector, method); + $el.on(eventName, selector, method); } }, From e14a903020d8a10a8b98ff687eb6afc9dc79b89f Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:19:38 +0800 Subject: [PATCH 15/82] Fix typo --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 9826cd179..cb8548a39 100644 --- a/backbone.js +++ b/backbone.js @@ -1113,7 +1113,7 @@ // Creates the actual context for this view using the given `root` and a // hash of `attributes` and returns the created element. `root` can be a CSS // selector or an HTML string, a jQuery context or an element. Subclasses - // can override this to utilize and alternative DOM manipulation API. + // can override this to utilize an alternative DOM manipulation API. _createContext: function(root, attributes) { var $el = root instanceof Backbone.$ ? root : Backbone.$(root); $el.attr(attributes || {}); From be3a35428a0ebad1902cd1d987f7fe70e39beccb Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:31:08 +0800 Subject: [PATCH 16/82] Comment for 2 arg form inside _delegate --- backbone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backbone.js b/backbone.js index cb8548a39..42d77bd1a 100644 --- a/backbone.js +++ b/backbone.js @@ -1098,6 +1098,7 @@ if (!selector) { $el.on(eventName, method); } else { + // `selector` is actually `method` in 2 argument form. $el.on(eventName, selector, method); } }, From fc95eaefd2ebe2d953fdb213c358996a47a6ee6d Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 21 Feb 2014 10:00:48 -0500 Subject: [PATCH 17/82] Revert "Speed up method calls in delegateEvents and delegate" This reverts commit 846de91c88e04c90e2805f7fdea18e4012c94afc. Conflicts: backbone.js --- backbone.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index 42d77bd1a..1da3f1d87 100644 --- a/backbone.js +++ b/backbone.js @@ -1075,7 +1075,6 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); - var _delegate = this._delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1083,8 +1082,7 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; - method = method.bind && method.bind(this) || _.bind(method, this); - _delegate.call(this, eventName, selector, method); + this.delegate(eventName, selector, _.bind(method, this)); } return this; }, @@ -1092,14 +1090,14 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. - _delegate: function(eventName, selector, method) { - var $el = this.$el; + delegate: function(eventName, selector, method) { eventName += '.delegateEvents' + this.cid; if (!selector) { - $el.on(eventName, method); + this.$el.on(eventName, method); } else { - // `selector` is actually `method` in 2 argument form. - $el.on(eventName, selector, method); + // When `delegate` is called with two arguments, `selector` is actually + // the `method` + this.$el.on(eventName, selector, method); } }, From 3cef7fa7b6aac645aa7cd81dc81a54333938fdd7 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sat, 22 Feb 2014 00:02:37 +0800 Subject: [PATCH 18/82] Fix test for fc95eae --- test/view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/view.js b/test/view.js index 8b507aaa2..534a33285 100644 --- a/test/view.js +++ b/test/view.js @@ -69,20 +69,20 @@ equal(counter2, 3); }); - test("_delegate", 2, function() { + test("delegate", 2, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - view._delegate('click', 'h1', view.increment); - view._delegate('click', view.increment); + view.delegate('click', 'h1', view.increment); + view.delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 1); - }) + }); test("delegateEvents allows functions for callbacks", 3, function() { var view = new Backbone.View({el: '

'}); From 2b19b00d7d5ae0cc7a755bd12288fcf3dfec5d43 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sat, 22 Feb 2014 01:13:53 +0800 Subject: [PATCH 19/82] Call _createContext only once during new View --- backbone.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 1da3f1d87..bd44aad4a 100644 --- a/backbone.js +++ b/backbone.js @@ -1050,9 +1050,9 @@ // Change the view's element (`this.el` property), including event // re-delegation. - setElement: function(element, delegate) { + setElement: function(element, delegate, attributes) { this.undelegateEvents(); - this._createContext(element); + this._createContext(element, attributes); if (delegate !== false) this.delegateEvents(); return this; }, @@ -1129,8 +1129,7 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this._createContext(document.createElement(_.result(this, 'tagName')), attrs); - this.setElement(this.el, false); + this.setElement(document.createElement(_.result(this, 'tagName')), false, attrs); } else { this.setElement(_.result(this, 'el'), false); } From 557edc7fc00c83b12c0c45bebbcc41e2c7b46314 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Sat, 22 Feb 2014 09:44:47 -0500 Subject: [PATCH 20/82] return `this` from `delegate` --- backbone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backbone.js b/backbone.js index bd44aad4a..5ce4245f8 100644 --- a/backbone.js +++ b/backbone.js @@ -1099,6 +1099,7 @@ // the `method` this.$el.on(eventName, selector, method); } + return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. From 933c02e6a140522b42155be38f089f583734ccc7 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 27 Feb 2014 18:36:15 +0200 Subject: [PATCH 21/82] small cleanup in _createContext --- backbone.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backbone.js b/backbone.js index 5ce4245f8..cf55e9fd2 100644 --- a/backbone.js +++ b/backbone.js @@ -1114,11 +1114,10 @@ // hash of `attributes` and returns the created element. `root` 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. - _createContext: function(root, attributes) { - var $el = root instanceof Backbone.$ ? root : Backbone.$(root); - $el.attr(attributes || {}); - this.$el = $el; - this.el = $el[0]; + _createContext: function(el, attributes) { + var $el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.$el = $el.attr(attributes || {}); + this.el = this.$el[0]; }, // Ensure that the View has a DOM element to render into. From 6fec7a5772991f2bf44ffac544546a19a0b14687 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 27 Feb 2014 23:24:20 +0200 Subject: [PATCH 22/82] save a var in _createContext --- backbone.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index cf55e9fd2..31086e218 100644 --- a/backbone.js +++ b/backbone.js @@ -1115,8 +1115,8 @@ // selector or an HTML string, a jQuery context or an element. Subclasses // can override this to utilize an alternative DOM manipulation API. _createContext: function(el, attributes) { - var $el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.$el = $el.attr(attributes || {}); + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.$el.attr(attributes || {}); this.el = this.$el[0]; }, From 6433043649b301c7d67c7dac4b74709da1f9da29 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Mon, 3 Mar 2014 05:07:28 +0800 Subject: [PATCH 23/82] Rename delegate's method param to listener to tell people it's ok to attach an unbound function as handler --- backbone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 31086e218..d4402ea88 100644 --- a/backbone.js +++ b/backbone.js @@ -1090,14 +1090,14 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. - delegate: function(eventName, selector, method) { + delegate: function(eventName, selector, listener) { eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, method); + this.$el.on(eventName, listener); } else { // When `delegate` is called with two arguments, `selector` is actually - // the `method` - this.$el.on(eventName, selector, method); + // the `listener` + this.$el.on(eventName, selector, listener); } return this; }, From 7eedd4bc0a5f7d336304d868f5215d868b609966 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Mon, 3 Mar 2014 23:50:04 +0800 Subject: [PATCH 24/82] View conformance tests --- test/view.js | 217 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 138 insertions(+), 79 deletions(-) diff --git a/test/view.js b/test/view.js index 534a33285..560dd66cd 100644 --- a/test/view.js +++ b/test/view.js @@ -1,11 +1,46 @@ (function() { + var ViewUnderTest = Backbone.View; + var view; + var addEventListener = function(obj, eventName, listener, useCapture) { + if (obj.addEventListener) { + obj.addEventListener(eventName, listener, useCapture); + } else { + return obj.attachEvent('on' + eventName, listener); + } + }; + + function click(element) { + var event; + if (document.createEvent) { + event = document.createEvent('MouseEvent'); + var args = [ + 'click', true, true, + // IE 10+ and Firefox require these + event.view, event.detail, event.screenX, event.screenY, event.clientX, + event.clientY, event.ctrlKey, event.altKey, event.shiftKey, + event.metaKey, event.button, event.relatedTarget + ]; + (event.initMouseEvent || event.initEvent).apply(event, args); + } else { + event = document.createEventObject(); + event.type = 'click'; + event.bubbles = true; + event.cancelable = true; + } + + if (element.dispatchEvent) { + return element.dispatchEvent(event); + } + element.fireEvent('onclick', event); + } + module("Backbone.View", { setup: function() { - view = new Backbone.View({ + view = new ViewUnderTest({ id : 'test-view', className : 'test-view', other : 'non-special-option' @@ -20,23 +55,21 @@ equal(view.el.other, void 0); }); - test("jQuery", 1, function() { - var view = new Backbone.View; + test("$", 1, function() { + var view = new ViewUnderTest; view.setElement('

test

'); - strictEqual(view.$('a b').html(), 'test'); + strictEqual(view.$('a b')[0].innerHTML, 'test'); }); - test("_createContext", 4, function() { + test("_createContext", 2, function() { view._createContext('
', {id: 'test-div'}); equal(view.el.tagName.toLowerCase(), 'div'); equal(view.el.id, 'test-div'); - ok(view.$el instanceof Backbone.$); - equal(view.$el[0], view.el); }); test("initialize", 1, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ initialize: function() { this.one = 1; } @@ -48,23 +81,23 @@ test("delegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - var view = new Backbone.View({el: '#testElement'}); + var view = new ViewUnderTest({el: '#testElement'}); view.increment = function(){ counter1++; }; - view.$el.on('click', function(){ counter2++; }); + addEventListener(view.el, 'click', function(){ counter2++; }); var events = {'click h1': 'increment'}; view.delegateEvents(events); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 1); equal(counter2, 1); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 2); equal(counter2, 2); view.delegateEvents(events); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 3); equal(counter2, 3); }); @@ -72,20 +105,20 @@ test("delegate", 2, function() { var counter1 = 0, counter2 = 0; - var view = new Backbone.View({el: '#testElement'}); + var view = new ViewUnderTest({el: '#testElement'}); view.increment = function(){ counter1++; }; - view.$el.on('click', function(){ counter2++; }); + view.delegate('click', function(){ counter2++; }); view.delegate('click', 'h1', view.increment); view.delegate('click', view.increment); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 2); equal(counter2, 1); }); test("delegateEvents allows functions for callbacks", 3, function() { - var view = new Backbone.View({el: '

'}); + var view = new ViewUnderTest({el: '

'}); view.counter = 0; var events = { @@ -94,52 +127,61 @@ } }; + document.body.appendChild(view.el); + view.delegateEvents(events); - view.$el.trigger('click'); + click(view.el); equal(view.counter, 1); - view.$el.trigger('click'); + click(view.el); equal(view.counter, 2); view.delegateEvents(events); - view.$el.trigger('click'); + click(view.el); equal(view.counter, 3); + + document.body.removeChild(view.el); }); test("delegateEvents ignore undefined methods", 0, function() { - var view = new Backbone.View({el: '

'}); + var view = new ViewUnderTest({el: '

'}); + + document.body.appendChild(view.el); + view.delegateEvents({'click': 'undefinedMethod'}); - view.$el.trigger('click'); + click(view.el); + + document.body.removeChild(view.el); }); test("undelegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - var view = new Backbone.View({el: '#testElement'}); + var view = new ViewUnderTest({el: '#testElement'}); view.increment = function(){ counter1++; }; - view.$el.on('click', function(){ counter2++; }); + addEventListener(view.el, 'click', function(){ counter2++; }); var events = {'click h1': 'increment'}; view.delegateEvents(events); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 1); equal(counter2, 1); view.undelegateEvents(); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 1); equal(counter2, 2); view.delegateEvents(events); - view.$('h1').trigger('click'); + click(view.$('h1')[0]); equal(counter1, 2); equal(counter2, 3); }); test("_ensureElement with DOM node el", 1, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ el: document.body }); @@ -147,24 +189,27 @@ }); test("_ensureElement with string el", 3, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ el: "body" }); strictEqual(new View().el, document.body); - View = Backbone.View.extend({ + View = ViewUnderTest.extend({ el: "#testElement > h1" }); - strictEqual(new View().el, $("#testElement > h1").get(0)); + var h1 = _.filter(document.getElementById('testElement').childNodes, function(node) { + return node.nodeType == 1; + })[0]; + strictEqual(new View().el, h1); - View = Backbone.View.extend({ + View = ViewUnderTest.extend({ el: "#nonexistent" }); ok(!new View().el); }); test("with className and id functions", 2, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ className: function() { return 'className'; }, @@ -178,7 +223,7 @@ }); test("with attributes", 2, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ attributes: { id: 'id', 'class': 'class' @@ -190,7 +235,7 @@ }); test("with attributes as a function", 1, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ attributes: function() { return {'class': 'dynamic'}; } @@ -201,10 +246,10 @@ test("multiple views per element", 3, function() { var count = 0; - var $el = $('

'); + var el = document.createElement('p'); - var View = Backbone.View.extend({ - el: $el, + var View = ViewUnderTest.extend({ + el: el, events: { click: function() { count++; @@ -212,59 +257,69 @@ } }); + document.body.appendChild(el); + var view1 = new View; - $el.trigger("click"); + click(el); equal(1, count); var view2 = new View; - $el.trigger("click"); + click(el); equal(3, count); view1.delegateEvents(); - $el.trigger("click"); + click(el); equal(5, count); + + document.body.removeChild(el); }); - test("custom events, with namespaces", 2, function() { - var count = 0; + if (typeof jQuery != 'undefined') { + test("custom events, with namespaces", 2, function() { + var count = 0; - var View = Backbone.View.extend({ - el: $('body'), - events: function() { - return {"fake$event.namespaced": "run"}; - }, - run: function() { - count++; - } - }); + var View = ViewUnderTest.extend({ + el: $('body'), + events: function() { + return {"fake$event.namespaced": "run"}; + }, + run: function() { + count++; + } + }); - var view = new View; - $('body').trigger('fake$event').trigger('fake$event'); - equal(count, 2); + var view = new View; + $('body').trigger('fake$event').trigger('fake$event'); + equal(count, 2); - $('body').off('.namespaced'); - $('body').trigger('fake$event'); - equal(count, 2); - }); + $('body').off('.namespaced'); + $('body').trigger('fake$event'); + equal(count, 2); + + }); + } test("#1048 - setElement uses provided object.", 2, function() { - var $el = $('body'); + var el = document.body; - var view = new Backbone.View({el: $el}); - ok(view.$el === $el); + var view = new ViewUnderTest({el: el}); + ok(view.el === el); - view.setElement($el = $($el)); - ok(view.$el === $el); + view.setElement(el = document.body); + ok(view.el === el); }); test("#986 - Undelegate before changing element.", 1, function() { - var button1 = $(''); - var button2 = $(''); + var button1 = document.createElement('button'); + var button2 = document.createElement('button'); + + document.body.appendChild(button1); + document.body.appendChild(button2); - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ events: { click: function(e) { - ok(view.el === e.target); + ok(view.el === e.target || e.srcElement); } } }); @@ -272,12 +327,15 @@ var view = new View({el: button1}); view.setElement(button2); - button1.trigger('click'); - button2.trigger('click'); + click(button1); + click(button2); + + document.body.removeChild(button1); + document.body.removeChild(button2); }); test("#1172 - Clone attributes object", 2, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ attributes: {foo: 'bar'} }); @@ -289,17 +347,17 @@ }); test("#1228 - tagName can be provided as a function", 1, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ tagName: function() { return 'p'; } }); - ok(new View().$el.is('p')); + ok(new View().el.tagName.toLowerCase() == 'p'); }); test("views stopListening", 0, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ initialize: function() { this.listenTo(this.model, 'all x', function(){ ok(false); }, this); this.listenTo(this.collection, 'all x', function(){ ok(false); }, this); @@ -317,21 +375,21 @@ }); test("Provide function for el.", 2, function() { - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ el: function() { return "

"; } }); var view = new View; - ok(view.$el.is('p')); - ok(view.$el.has('a')); + ok(view.el.tagName.toLowerCase() == 'p'); + ok(view.$('a').length != 0); }); test("events passed in options", 1, function() { var counter = 0; - var View = Backbone.View.extend({ + var View = ViewUnderTest.extend({ el: '#testElement', increment: function() { counter++; @@ -344,7 +402,8 @@ } }); - view.$('h1').trigger('click').trigger('click'); + click(view.$('h1')[0]); + click(view.$('h1')[0]); equal(counter, 2); }); From 0cdb38b03f603a81fa5174ff30a8c81d019ba5b7 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 18 Feb 2014 17:09:01 -0500 Subject: [PATCH 25/82] revert c1e62cda: add `View#make` back in for overriding --- backbone.js | 15 +++++++++++++-- test/view.js | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 99713a8b9..dd6f01950 100644 --- a/backbone.js +++ b/backbone.js @@ -1124,6 +1124,18 @@ return this; }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't + // needed, use **make** to manufacture elements, one at a time. + // + // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); + // + make: function(tagName, attributes, content) { + var $el = Backbone.$('<' + tagName + '>'); + if (attributes) $el.attr(attributes); + if (content != null) $el.html(content); + return $el[0]; + }, + // 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 @@ -1133,8 +1145,7 @@ 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, false); + this.setElement(this.make(_.result(this, 'tagName'), attrs), false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 76f0f1db1..5635c9376 100644 --- a/test/view.js +++ b/test/view.js @@ -26,6 +26,22 @@ strictEqual(view.$('a b').html(), 'test'); }); + test("make", 3, function() { + var div = view.make('div', {id: 'test-div'}, "one two three"); + + equal(div.tagName.toLowerCase(), 'div'); + equal(div.id, 'test-div'); + equal($(div).text(), 'one two three'); + }); + + test("make can take falsy values for content", 2, function() { + var div = view.make('div', {id: 'test-div'}, 0); + equal($(div).text(), '0'); + + div = view.make('div', {id: 'test-div'}, ''); + equal($(div).text(), ''); + }); + test("initialize", 1, function() { var View = Backbone.View.extend({ initialize: function() { From 90c36cadacacee7f54a750725ff777b5ee7f1a6b Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 18 Feb 2014 17:34:54 -0500 Subject: [PATCH 26/82] add `View#delegate` as a single event listener version of delegateEvents for easier overriding --- backbone.js | 23 ++++++++++++++++------- test/view.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index dd6f01950..450196a4a 100644 --- a/backbone.js +++ b/backbone.js @@ -1105,13 +1105,7 @@ 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(eventName, selector, method); } return this; }, @@ -1124,6 +1118,21 @@ return this; }, + // Add a single event listener to the element. + delegate: function(eventName, selector, method) { + if (_.isFunction(selector)) { + method = selector; + selector = undefined; + } + eventName += '.delegateEvents' + this.cid; + if (!selector) { + this.$el.on(eventName, _.bind(method, this)); + } else { + this.$el.on(eventName, selector, _.bind(method, this)); + } + return this; + }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't // needed, use **make** to manufacture elements, one at a time. // diff --git a/test/view.js b/test/view.js index 5635c9376..77f531c6e 100644 --- a/test/view.js +++ b/test/view.js @@ -76,6 +76,21 @@ equal(counter2, 3); }); + test("delegate", 2, function() { + var counter1 = 0, counter2 = 0; + + var view = new Backbone.View({el: '#testElement'}); + view.increment = function(){ counter1++; }; + view.$el.on('click', function(){ counter2++; }); + + view.delegate('click', 'h1', view.increment); + view.delegate('click', view.increment); + + view.$('h1').trigger('click'); + equal(counter1, 2); + equal(counter2, 1); + }) + test("delegateEvents allows functions for callbacks", 3, function() { var view = new Backbone.View({el: '

'}); view.counter = 0; From 4e5b28221cf7ff2ff561cf641e254ce8665e529b Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 19 Feb 2014 01:07:25 -0500 Subject: [PATCH 27/82] save two lines in View#make: jquery handles nulls nicely --- backbone.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index 450196a4a..88bdd1c6e 100644 --- a/backbone.js +++ b/backbone.js @@ -1139,9 +1139,7 @@ // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); // make: function(tagName, attributes, content) { - var $el = Backbone.$('<' + tagName + '>'); - if (attributes) $el.attr(attributes); - if (content != null) $el.html(content); + var $el = Backbone.$('<' + tagName + '>', attributes).html(content); return $el[0]; }, From be1cba33314ab5d6997a7c660478e808d8494d9a Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 01:21:39 +0800 Subject: [PATCH 28/82] Move delegate up and move check into undelegateEvents --- backbone.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index 88bdd1c6e..e30566652 100644 --- a/backbone.js +++ b/backbone.js @@ -1073,7 +1073,7 @@ // Change the view's element (`this.el` property), including event // re-delegation. setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); + this.undelegateEvents(); this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); this.el = this.$el[0]; if (delegate !== false) this.delegateEvents(); @@ -1110,13 +1110,6 @@ return this; }, - // Clears all callbacks previously bound to the view with `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); - return this; - }, // Add a single event listener to the element. delegate: function(eventName, selector, method) { @@ -1133,6 +1126,14 @@ return this; }, + // Clears all callbacks previously bound to the view with `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() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + // For hooking into small amounts of DOM Elements, where a full-blown template isn't // needed, use **make** to manufacture elements, one at a time. // From 3369bf486b1bcb46bd6fb4b34b37145219cc2937 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 01:23:30 +0800 Subject: [PATCH 29/82] Speed up method calls in delegateEvents and delegate --- backbone.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index e30566652..b97f34a9a 100644 --- a/backbone.js +++ b/backbone.js @@ -1098,6 +1098,7 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); + var delegate = this.delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1105,23 +1106,20 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; - this.delegate(eventName, selector, method); + method = method.bind && method.bind(this) || _.bind(method, this); + delegate.call(this, eventName, selector, method); } return this; }, - - // Add a single event listener to the element. + // Add a single event listener to the element responding only to the + // optional `selector` or catches all `eventName` events. delegate: function(eventName, selector, method) { - if (_.isFunction(selector)) { - method = selector; - selector = undefined; - } eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, _.bind(method, this)); + this.$el.on(eventName, method); } else { - this.$el.on(eventName, selector, _.bind(method, this)); + this.$el.on(eventName, selector, method); } return this; }, From ad44cb41b6b1d3fa72efbf5931623cfd09d96405 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 02:11:57 +0800 Subject: [PATCH 30/82] Further isolate $.remove by exposing removeElement in case subclasses forgot to clean up --- backbone.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index b97f34a9a..5d09a6813 100644 --- a/backbone.js +++ b/backbone.js @@ -1062,14 +1062,23 @@ return this; }, - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. + // Remove this view by taking the element out of the document, remove all + // the DOM event listeners attached to it, and remove any applicable + // Backbone.Events listeners. remove: function() { - this.$el.remove(); + this.undelegateEvents(); + this.removeElement(); this.stopListening(); return this; }, + // Remove this view's element from the document and remove all the event + // listeners attached to it. + removeElement: function() { + this.$el.remove(); + return this; + }, + // Change the view's element (`this.el` property), including event // re-delegation. setElement: function(element, delegate) { From 5f66f9af18d6ef460c69ee31d46b60b487a99a0f Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:11:11 +0800 Subject: [PATCH 31/82] Rename make to _createContext and have _ensureElement and setElement delegate to it --- backbone.js | 29 ++++++++++++++++++----------- test/view.js | 16 +++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/backbone.js b/backbone.js index 5d09a6813..5a64b328f 100644 --- a/backbone.js +++ b/backbone.js @@ -1083,8 +1083,7 @@ // re-delegation. setElement: function(element, delegate) { this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; + this._createContext(element); if (delegate !== false) this.delegateEvents(); return this; }, @@ -1141,14 +1140,21 @@ return this; }, - // For hooking into small amounts of DOM Elements, where a full-blown template isn't - // needed, use **make** to manufacture elements, one at a time. - // - // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); - // - make: function(tagName, attributes, content) { - var $el = Backbone.$('<' + tagName + '>', attributes).html(content); - return $el[0]; + // Creates the actual context for this view using the given `root` and a + // hash of `attributes`. `root` can be a CSS selector or an HTML string, a + // jQuery context or and Element. + _createContext: function(root, attributes) { + var $el; + if (typeof root == 'string') { + $el = Backbone.$(root); + } else if (root instanceof Backbone.$) { + $el = root; + } else { + $el = Backbone.$(root); + } + if (!_.isEmpty(attributes)) $el.attr(attributes); + this.$el = $el; + return (this.el = $el[0]); }, // Ensure that the View has a DOM element to render into. @@ -1160,7 +1166,8 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this.setElement(this.make(_.result(this, 'tagName'), attrs), false); + var el = document.createElement(_.result(this, 'tagName')); + this.setElement(this._createContext(el, attrs), false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 77f531c6e..cc896007a 100644 --- a/test/view.js +++ b/test/view.js @@ -26,20 +26,14 @@ strictEqual(view.$('a b').html(), 'test'); }); - test("make", 3, function() { - var div = view.make('div', {id: 'test-div'}, "one two three"); + test("_createContext", 5, function() { + var div = view._createContext('
', {id: 'test-div'}); equal(div.tagName.toLowerCase(), 'div'); equal(div.id, 'test-div'); - equal($(div).text(), 'one two three'); - }); - - test("make can take falsy values for content", 2, function() { - var div = view.make('div', {id: 'test-div'}, 0); - equal($(div).text(), '0'); - - div = view.make('div', {id: 'test-div'}, ''); - equal($(div).text(), ''); + equal(view.el, div); + ok(view.$el instanceof Backbone.$); + equal(view.$el[0], div); }); test("initialize", 1, function() { From d78fb34aff53430cbac0ee151bac6d8a0bddf885 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:31:36 +0800 Subject: [PATCH 32/82] Prefix all hooks with _ and introduce _undelegate --- backbone.js | 37 ++++++++++++++++++++++++------------- test/view.js | 6 +++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/backbone.js b/backbone.js index 5a64b328f..3734c53c6 100644 --- a/backbone.js +++ b/backbone.js @@ -1067,14 +1067,15 @@ // Backbone.Events listeners. remove: function() { this.undelegateEvents(); - this.removeElement(); + this._removeElement(); this.stopListening(); return this; }, // Remove this view's element from the document and remove all the event - // listeners attached to it. - removeElement: function() { + // listeners attached to it. Useful for subclasses to override in order to + // utilize an alternative DOM manipulation API. + _removeElement: function() { this.$el.remove(); return this; }, @@ -1106,7 +1107,7 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); - var delegate = this.delegate; + var _delegate = this._delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1115,14 +1116,15 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; method = method.bind && method.bind(this) || _.bind(method, this); - delegate.call(this, eventName, selector, method); + _delegate.call(this, eventName, selector, method); } return this; }, // Add a single event listener to the element responding only to the - // optional `selector` or catches all `eventName` events. - delegate: function(eventName, selector, method) { + // optional `selector` or catches all `eventName` events. Subclasses can + // override this to utilize an alternative DOM event management API. + _delegate: function(eventName, selector, method) { eventName += '.delegateEvents' + this.cid; if (!selector) { this.$el.on(eventName, method); @@ -1132,17 +1134,26 @@ return this; }, - // Clears all callbacks previously bound to the view with `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. + // Undo what `delegateEvents` did. 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() { - if (this.$el) this.$el.off('.delegateEvents' + this.cid); + if (this.$el) this._undelegate(); + return this; + }, + + // Clears all callbacks previously bound to the view by + // `delegateEvents`. Subclasses can override this to utilize an alternative + // DOM event management API. + _undelegate: function() { + this.$el.off('.delegateEvents' + this.cid); return this; }, // Creates the actual context for this view using the given `root` and a - // hash of `attributes`. `root` can be a CSS selector or an HTML string, a - // jQuery context or and Element. + // hash of `attributes` and returns the created element. `root` can be a CSS + // selector or an HTML string, a jQuery context or and Element. Subclasses + // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { var $el; if (typeof root == 'string') { diff --git a/test/view.js b/test/view.js index cc896007a..6263dda07 100644 --- a/test/view.js +++ b/test/view.js @@ -70,15 +70,15 @@ equal(counter2, 3); }); - test("delegate", 2, function() { + test("_delegate", 2, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - view.delegate('click', 'h1', view.increment); - view.delegate('click', view.increment); + view._delegate('click', 'h1', view.increment); + view._delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); From 1e9cd4a35055b70e6c98034e3f882368ce6fd1db Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 05:53:13 +0800 Subject: [PATCH 33/82] Revert introduction of _undelegate --- backbone.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/backbone.js b/backbone.js index 3734c53c6..fec69efd0 100644 --- a/backbone.js +++ b/backbone.js @@ -1134,19 +1134,11 @@ return this; }, - // Undo what `delegateEvents` did. You usually don't need to use this, but - // may wish to if you have multiple Backbone views attached to the same DOM - // element. + // 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() { - if (this.$el) this._undelegate(); - return this; - }, - - // Clears all callbacks previously bound to the view by - // `delegateEvents`. Subclasses can override this to utilize an alternative - // DOM event management API. - _undelegate: function() { - this.$el.off('.delegateEvents' + this.cid); + if (this.$el) this.$el.off('.delegateEvents' + this.cid); return this; }, From 9028e338c3b39252bcb8df94ec341bdf9467540d Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 13:05:03 +0800 Subject: [PATCH 34/82] Shortened _createContext --- backbone.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index fec69efd0..485266aef 100644 --- a/backbone.js +++ b/backbone.js @@ -1147,15 +1147,8 @@ // selector or an HTML string, a jQuery context or and Element. Subclasses // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { - var $el; - if (typeof root == 'string') { - $el = Backbone.$(root); - } else if (root instanceof Backbone.$) { - $el = root; - } else { - $el = Backbone.$(root); - } - if (!_.isEmpty(attributes)) $el.attr(attributes); + var $el = root instanceof Backbone.$ ? root : Backbone.$(root); + $el.attr(attributes || {}); this.$el = $el; return (this.el = $el[0]); }, From 6ebb6e1752e6dac63042b4f060d1660f4a203b96 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 14:18:21 +0800 Subject: [PATCH 35/82] _createContext is side-effect only --- backbone.js | 6 +++--- test/view.js | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index 485266aef..a47d1cf40 100644 --- a/backbone.js +++ b/backbone.js @@ -1150,7 +1150,7 @@ var $el = root instanceof Backbone.$ ? root : Backbone.$(root); $el.attr(attributes || {}); this.$el = $el; - return (this.el = $el[0]); + this.el = $el[0]; }, // Ensure that the View has a DOM element to render into. @@ -1162,8 +1162,8 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - var el = document.createElement(_.result(this, 'tagName')); - this.setElement(this._createContext(el, attrs), false); + this._createContext(document.createElement(_.result(this, 'tagName')), attrs); + this.setElement(this.el, false); } else { this.setElement(_.result(this, 'el'), false); } diff --git a/test/view.js b/test/view.js index 6263dda07..8b507aaa2 100644 --- a/test/view.js +++ b/test/view.js @@ -26,14 +26,13 @@ strictEqual(view.$('a b').html(), 'test'); }); - test("_createContext", 5, function() { - var div = view._createContext('
', {id: 'test-div'}); + test("_createContext", 4, function() { + view._createContext('
', {id: 'test-div'}); - equal(div.tagName.toLowerCase(), 'div'); - equal(div.id, 'test-div'); - equal(view.el, div); + equal(view.el.tagName.toLowerCase(), 'div'); + equal(view.el.id, 'test-div'); ok(view.$el instanceof Backbone.$); - equal(view.$el[0], div); + equal(view.$el[0], view.el); }); test("initialize", 1, function() { From 18d17db5545b01f9210204d7b4ce9253f304a42e Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Thu, 20 Feb 2014 15:22:38 +0800 Subject: [PATCH 36/82] Disallow chaining in hook calls --- backbone.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/backbone.js b/backbone.js index a47d1cf40..a0b26c97a 100644 --- a/backbone.js +++ b/backbone.js @@ -1077,7 +1077,6 @@ // utilize an alternative DOM manipulation API. _removeElement: function() { this.$el.remove(); - return this; }, // Change the view's element (`this.el` property), including event @@ -1131,7 +1130,6 @@ } else { this.$el.on(eventName, selector, method); } - return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. From 390275b82f23de2ce8ab40a0827fd1e425c261c8 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 02:46:20 +0800 Subject: [PATCH 37/82] Fix comment --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index a0b26c97a..adf65e57c 100644 --- a/backbone.js +++ b/backbone.js @@ -1142,7 +1142,7 @@ // Creates the actual context for this view using the given `root` and a // hash of `attributes` and returns the created element. `root` can be a CSS - // selector or an HTML string, a jQuery context or and Element. Subclasses + // selector or an HTML string, a jQuery context or an element. Subclasses // can override this to utilize and alternative DOM manipulation API. _createContext: function(root, attributes) { var $el = root instanceof Backbone.$ ? root : Backbone.$(root); From 9868bfb86263da9fa17ae306af0f0b56e7e30fdf Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:13:17 +0800 Subject: [PATCH 38/82] No reason jQuery can't be a little faster too --- backbone.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index adf65e57c..acb6f9492 100644 --- a/backbone.js +++ b/backbone.js @@ -1124,11 +1124,12 @@ // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. _delegate: function(eventName, selector, method) { + var $el = this.$el; eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, method); + $el.on(eventName, method); } else { - this.$el.on(eventName, selector, method); + $el.on(eventName, selector, method); } }, From 25c0beed0f698d97d4857d479e470e26d23f2cfb Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:19:38 +0800 Subject: [PATCH 39/82] Fix typo --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index acb6f9492..e49e9487a 100644 --- a/backbone.js +++ b/backbone.js @@ -1144,7 +1144,7 @@ // Creates the actual context for this view using the given `root` and a // hash of `attributes` and returns the created element. `root` can be a CSS // selector or an HTML string, a jQuery context or an element. Subclasses - // can override this to utilize and alternative DOM manipulation API. + // can override this to utilize an alternative DOM manipulation API. _createContext: function(root, attributes) { var $el = root instanceof Backbone.$ ? root : Backbone.$(root); $el.attr(attributes || {}); From 12abe8eef60e8611859c856308d671c24a5b1a24 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 21 Feb 2014 03:31:08 +0800 Subject: [PATCH 40/82] Comment for 2 arg form inside _delegate --- backbone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backbone.js b/backbone.js index e49e9487a..36dfc7522 100644 --- a/backbone.js +++ b/backbone.js @@ -1129,6 +1129,7 @@ if (!selector) { $el.on(eventName, method); } else { + // `selector` is actually `method` in 2 argument form. $el.on(eventName, selector, method); } }, From 72b88b5835ce27ad7dc9ad901d3dae844157c3c2 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 21 Feb 2014 10:00:48 -0500 Subject: [PATCH 41/82] Revert "Speed up method calls in delegateEvents and delegate" This reverts commit 846de91c88e04c90e2805f7fdea18e4012c94afc. Conflicts: backbone.js --- backbone.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index 36dfc7522..f80bddc90 100644 --- a/backbone.js +++ b/backbone.js @@ -1106,7 +1106,6 @@ delegateEvents: function(events) { if (!(events || (events = _.result(this, 'events')))) return this; this.undelegateEvents(); - var _delegate = this._delegate; for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; @@ -1114,8 +1113,7 @@ var match = key.match(delegateEventSplitter); var eventName = match[1], selector = match[2]; - method = method.bind && method.bind(this) || _.bind(method, this); - _delegate.call(this, eventName, selector, method); + this.delegate(eventName, selector, _.bind(method, this)); } return this; }, @@ -1123,14 +1121,14 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. - _delegate: function(eventName, selector, method) { - var $el = this.$el; + delegate: function(eventName, selector, method) { eventName += '.delegateEvents' + this.cid; if (!selector) { - $el.on(eventName, method); + this.$el.on(eventName, method); } else { - // `selector` is actually `method` in 2 argument form. - $el.on(eventName, selector, method); + // When `delegate` is called with two arguments, `selector` is actually + // the `method` + this.$el.on(eventName, selector, method); } }, From 20935c85eee0defaa528bf19f76008b185b3ef8f Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sat, 22 Feb 2014 00:02:37 +0800 Subject: [PATCH 42/82] Fix test for fc95eae --- test/view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/view.js b/test/view.js index 8b507aaa2..534a33285 100644 --- a/test/view.js +++ b/test/view.js @@ -69,20 +69,20 @@ equal(counter2, 3); }); - test("_delegate", 2, function() { + test("delegate", 2, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - view._delegate('click', 'h1', view.increment); - view._delegate('click', view.increment); + view.delegate('click', 'h1', view.increment); + view.delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 1); - }) + }); test("delegateEvents allows functions for callbacks", 3, function() { var view = new Backbone.View({el: '

'}); From 5dc2d093fb43fd1b76f134470f5fa3e1052731d6 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sat, 22 Feb 2014 01:13:53 +0800 Subject: [PATCH 43/82] Call _createContext only once during new View --- backbone.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index f80bddc90..cd185c666 100644 --- a/backbone.js +++ b/backbone.js @@ -1081,9 +1081,9 @@ // Change the view's element (`this.el` property), including event // re-delegation. - setElement: function(element, delegate) { + setElement: function(element, delegate, attributes) { this.undelegateEvents(); - this._createContext(element); + this._createContext(element, attributes); if (delegate !== false) this.delegateEvents(); return this; }, @@ -1160,8 +1160,7 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this._createContext(document.createElement(_.result(this, 'tagName')), attrs); - this.setElement(this.el, false); + this.setElement(document.createElement(_.result(this, 'tagName')), false, attrs); } else { this.setElement(_.result(this, 'el'), false); } From 3edf74c37bad199d81add5152207e20ed7c4e7d0 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Sat, 22 Feb 2014 09:44:47 -0500 Subject: [PATCH 44/82] return `this` from `delegate` --- backbone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backbone.js b/backbone.js index cd185c666..8fd958319 100644 --- a/backbone.js +++ b/backbone.js @@ -1130,6 +1130,7 @@ // the `method` this.$el.on(eventName, selector, method); } + return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. From e30e6b9344f11c3219e70009ff7374b5c7bab61b Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 27 Feb 2014 18:36:15 +0200 Subject: [PATCH 45/82] small cleanup in _createContext --- backbone.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backbone.js b/backbone.js index 8fd958319..50bb6481b 100644 --- a/backbone.js +++ b/backbone.js @@ -1145,11 +1145,10 @@ // hash of `attributes` and returns the created element. `root` 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. - _createContext: function(root, attributes) { - var $el = root instanceof Backbone.$ ? root : Backbone.$(root); - $el.attr(attributes || {}); - this.$el = $el; - this.el = $el[0]; + _createContext: function(el, attributes) { + var $el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.$el = $el.attr(attributes || {}); + this.el = this.$el[0]; }, // Ensure that the View has a DOM element to render into. From 446ee523faca61c437167ff45b6a73acc83dd108 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 27 Feb 2014 23:24:20 +0200 Subject: [PATCH 46/82] save a var in _createContext --- backbone.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 50bb6481b..4367bd116 100644 --- a/backbone.js +++ b/backbone.js @@ -1146,8 +1146,8 @@ // selector or an HTML string, a jQuery context or an element. Subclasses // can override this to utilize an alternative DOM manipulation API. _createContext: function(el, attributes) { - var $el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.$el = $el.attr(attributes || {}); + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.$el.attr(attributes || {}); this.el = this.$el[0]; }, From fd6425441d207e401fbd8132325df7249afed614 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Mon, 3 Mar 2014 05:07:28 +0800 Subject: [PATCH 47/82] Rename delegate's method param to listener to tell people it's ok to attach an unbound function as handler --- backbone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 4367bd116..6c6e52ac9 100644 --- a/backbone.js +++ b/backbone.js @@ -1121,14 +1121,14 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can // override this to utilize an alternative DOM event management API. - delegate: function(eventName, selector, method) { + delegate: function(eventName, selector, listener) { eventName += '.delegateEvents' + this.cid; if (!selector) { - this.$el.on(eventName, method); + this.$el.on(eventName, listener); } else { // When `delegate` is called with two arguments, `selector` is actually - // the `method` - this.$el.on(eventName, selector, method); + // the `listener` + this.$el.on(eventName, selector, listener); } return this; }, From f13181fbc010ef614321cea4e1d0a9d5720b3196 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 4 Mar 2014 13:35:24 +0200 Subject: [PATCH 48/82] rename _createContext to _setEl --- backbone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 6c6e52ac9..7b383e9f2 100644 --- a/backbone.js +++ b/backbone.js @@ -1083,7 +1083,7 @@ // re-delegation. setElement: function(element, delegate, attributes) { this.undelegateEvents(); - this._createContext(element, attributes); + this._setEl(element, attributes); if (delegate !== false) this.delegateEvents(); return this; }, @@ -1141,11 +1141,11 @@ return this; }, - // Creates the actual context for this view using the given `root` and a - // hash of `attributes` and returns the created element. `root` can be a CSS + // Creates the actual context for this view using the given `el` and a + // hash of `attributes` and returns the created element. `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. - _createContext: function(el, attributes) { + _setEl: function(el, attributes) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); this.$el.attr(attributes || {}); this.el = this.$el[0]; From a9d066a84eda5a827b0d05be1d357d4cd447f149 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 4 Mar 2014 14:17:42 +0200 Subject: [PATCH 49/82] test view.$ interface and rename _createContext to _setEl in view test --- test/view.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/view.js b/test/view.js index 4472f6222..8f190e3d7 100644 --- a/test/view.js +++ b/test/view.js @@ -55,21 +55,24 @@ equal(view.el.other, void 0); }); - test("$", 1, function() { + test("$", 2, function() { var view = new ViewUnderTest; view.setElement('

test

'); - strictEqual(view.$('a b')[0].innerHTML, 'test'); + var result = view.$('a b'); + + strictEqual(result[0].innerHTML, 'test'); + ok(result instanceof NodeList || result instanceof Array || result instanceof Backbone.$); }); - test("_createContext", 2, function() { - view._createContext('
', {id: 'test-div'}); + test("_setEl", 2, function() { + view._setEl('
', {id: 'test-div'}); equal(view.el.tagName.toLowerCase(), 'div'); equal(view.el.id, 'test-div'); }); - test("_createContext", 4, function() { - view._createContext('
', {id: 'test-div'}); + test("_setEl", 4, function() { + view._setEl('
', {id: 'test-div'}); equal(view.el.tagName.toLowerCase(), 'div'); equal(view.el.id, 'test-div'); From 70933e51cf42b5102ffa1f62a3ee1dcebd476868 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 4 Mar 2014 14:19:35 +0200 Subject: [PATCH 50/82] fix merge conflict --- test/view.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/view.js b/test/view.js index 8f190e3d7..963df62a0 100644 --- a/test/view.js +++ b/test/view.js @@ -71,15 +71,6 @@ equal(view.el.id, 'test-div'); }); - test("_setEl", 4, function() { - view._setEl('
', {id: 'test-div'}); - - equal(view.el.tagName.toLowerCase(), 'div'); - equal(view.el.id, 'test-div'); - ok(view.$el instanceof Backbone.$); - equal(view.$el[0], view.el); - }); - test("initialize", 1, function() { var View = ViewUnderTest.extend({ initialize: function() { From 0fca38cd820a1eba179529e70f6412de9ba73984 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 4 Mar 2014 14:45:22 +0200 Subject: [PATCH 51/82] view test cleanup --- test/view.js | 160 +++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 81 deletions(-) diff --git a/test/view.js b/test/view.js index 963df62a0..d18b40af1 100644 --- a/test/view.js +++ b/test/view.js @@ -1,46 +1,14 @@ (function() { - var ViewUnderTest = Backbone.View; + // When testing alternative View implementations, change this varaible. + var View = Backbone.View; var view; - var addEventListener = function(obj, eventName, listener, useCapture) { - if (obj.addEventListener) { - obj.addEventListener(eventName, listener, useCapture); - } else { - return obj.attachEvent('on' + eventName, listener); - } - }; - - function click(element) { - var event; - if (document.createEvent) { - event = document.createEvent('MouseEvent'); - var args = [ - 'click', true, true, - // IE 10+ and Firefox require these - event.view, event.detail, event.screenX, event.screenY, event.clientX, - event.clientY, event.ctrlKey, event.altKey, event.shiftKey, - event.metaKey, event.button, event.relatedTarget - ]; - (event.initMouseEvent || event.initEvent).apply(event, args); - } else { - event = document.createEventObject(); - event.type = 'click'; - event.bubbles = true; - event.cancelable = true; - } - - if (element.dispatchEvent) { - return element.dispatchEvent(event); - } - element.fireEvent('onclick', event); - } - module("Backbone.View", { setup: function() { - view = new ViewUnderTest({ + view = new View({ id : 'test-view', className : 'test-view', other : 'non-special-option' @@ -56,7 +24,7 @@ }); test("$", 2, function() { - var view = new ViewUnderTest; + var view = new View; view.setElement('

test

'); var result = view.$('a b'); @@ -72,21 +40,21 @@ }); test("initialize", 1, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ initialize: function() { this.one = 1; } }); - strictEqual(new View().one, 1); + strictEqual(new Test().one, 1); }); test("delegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - var view = new ViewUnderTest({el: '#testElement'}); + view = new View({el: '#testElement'}); view.increment = function(){ counter1++; }; - addEventListener(view.el, 'click', function(){ counter2++; }); + addEventListener.call(view.el, 'click', function(){ counter2++; }); var events = {'click h1': 'increment'}; @@ -108,7 +76,7 @@ test("delegate", 2, function() { var counter1 = 0, counter2 = 0; - var view = new ViewUnderTest({el: '#testElement'}); + view = new View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.delegate('click', function(){ counter2++; }); @@ -121,7 +89,7 @@ }); test("delegateEvents allows functions for callbacks", 3, function() { - var view = new ViewUnderTest({el: '

'}); + view = new View({el: '

'}); view.counter = 0; var events = { @@ -148,7 +116,7 @@ test("delegateEvents ignore undefined methods", 0, function() { - var view = new ViewUnderTest({el: '

'}); + view = new View({el: '

'}); document.body.appendChild(view.el); @@ -161,9 +129,9 @@ test("undelegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - var view = new ViewUnderTest({el: '#testElement'}); + view = new View({el: '#testElement'}); view.increment = function(){ counter1++; }; - addEventListener(view.el, 'click', function(){ counter2++; }); + addEventListener.call(view.el, 'click', function(){ counter2++; }); var events = {'click h1': 'increment'}; @@ -184,35 +152,35 @@ }); test("_ensureElement with DOM node el", 1, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: document.body }); - equal(new View().el, document.body); + equal(new Test().el, document.body); }); test("_ensureElement with string el", 3, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: "body" }); - strictEqual(new View().el, document.body); + strictEqual(new Test().el, document.body); - View = ViewUnderTest.extend({ + Test = View.extend({ el: "#testElement > h1" }); var h1 = _.filter(document.getElementById('testElement').childNodes, function(node) { return node.nodeType == 1; })[0]; - strictEqual(new View().el, h1); + strictEqual(new Test().el, h1); - View = ViewUnderTest.extend({ + Test = View.extend({ el: "#nonexistent" }); - ok(!new View().el); + ok(!new Test().el); }); test("with className and id functions", 2, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ className: function() { return 'className'; }, @@ -221,37 +189,37 @@ } }); - strictEqual(new View().el.className, 'className'); - strictEqual(new View().el.id, 'id'); + strictEqual(new Test().el.className, 'className'); + strictEqual(new Test().el.id, 'id'); }); test("with attributes", 2, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ attributes: { id: 'id', 'class': 'class' } }); - strictEqual(new View().el.className, 'class'); - strictEqual(new View().el.id, 'id'); + strictEqual(new Test().el.className, 'class'); + strictEqual(new Test().el.id, 'id'); }); test("with attributes as a function", 1, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ attributes: function() { return {'class': 'dynamic'}; } }); - strictEqual(new View().el.className, 'dynamic'); + strictEqual(new Test().el.className, 'dynamic'); }); test("multiple views per element", 3, function() { var count = 0; var el = document.createElement('p'); - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: el, events: { click: function() { @@ -262,11 +230,11 @@ document.body.appendChild(el); - var view1 = new View; + var view1 = new Test; click(el); equal(1, count); - var view2 = new View; + var view2 = new Test; click(el); equal(3, count); @@ -277,11 +245,11 @@ document.body.removeChild(el); }); - if (typeof jQuery != 'undefined') { + if (typeof Backbone.$ != 'undefined') { test("custom events, with namespaces", 2, function() { var count = 0; - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: $('body'), events: function() { return {"fake$event.namespaced": "run"}; @@ -291,7 +259,7 @@ } }); - var view = new View; + var view = new Test; $('body').trigger('fake$event').trigger('fake$event'); equal(count, 2); @@ -305,7 +273,7 @@ test("#1048 - setElement uses provided object.", 2, function() { var el = document.body; - var view = new ViewUnderTest({el: el}); + view = new View({el: el}); ok(view.el === el); view.setElement(el = document.body); @@ -319,7 +287,7 @@ document.body.appendChild(button1); document.body.appendChild(button2); - var View = ViewUnderTest.extend({ + Test = View.extend({ events: { click: function(e) { ok(view.el === e.target || e.srcElement); @@ -327,7 +295,7 @@ } }); - var view = new View({el: button1}); + var view = new Test({el: button1}); view.setElement(button2); click(button1); @@ -338,36 +306,36 @@ }); test("#1172 - Clone attributes object", 2, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ attributes: {foo: 'bar'} }); - var view1 = new View({id: 'foo'}); + var view1 = new Test({id: 'foo'}); strictEqual(view1.el.id, 'foo'); - var view2 = new View(); + var view2 = new Test(); ok(!view2.el.id); }); test("#1228 - tagName can be provided as a function", 1, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ tagName: function() { return 'p'; } }); - ok(new View().el.tagName.toLowerCase() == 'p'); + ok(new Test().el.tagName.toLowerCase() == 'p'); }); test("views stopListening", 0, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ initialize: function() { this.listenTo(this.model, 'all x', function(){ ok(false); }, this); this.listenTo(this.collection, 'all x', function(){ ok(false); }, this); } }); - var view = new View({ + var view = new Test({ model: new Backbone.Model, collection: new Backbone.Collection }); @@ -378,13 +346,13 @@ }); test("Provide function for el.", 2, function() { - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: function() { return "

"; } }); - var view = new View; + var view = new Test; ok(view.el.tagName.toLowerCase() == 'p'); ok(view.$('a').length != 0); }); @@ -392,14 +360,14 @@ test("events passed in options", 1, function() { var counter = 0; - var View = ViewUnderTest.extend({ + var Test = View.extend({ el: '#testElement', increment: function() { counter++; } }); - var view = new View({ + var view = new Test({ events: { 'click h1': 'increment' } @@ -410,4 +378,34 @@ equal(counter, 2); }); + // Cross-browser helpers + var addEventListener = Element.prototype.addEventListener || function(eventName, listener) { + return this.attachEvent('on' + eventName, listener); + }; + + function click(element) { + var event; + if (document.createEvent) { + event = document.createEvent('MouseEvent'); + var args = [ + 'click', true, true, + // IE 10+ and Firefox require these + event.view, event.detail, event.screenX, event.screenY, event.clientX, + event.clientY, event.ctrlKey, event.altKey, event.shiftKey, + event.metaKey, event.button, event.relatedTarget + ]; + (event.initMouseEvent || event.initEvent).apply(event, args); + } else { + event = document.createEventObject(); + event.type = 'click'; + event.bubbles = true; + event.cancelable = true; + } + + if (element.dispatchEvent) { + return element.dispatchEvent(event); + } + element.fireEvent('onclick', event); + } + })(); From fca75fb6bdcb74d3cf71be8a5c1c86eff60a2bd6 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Tue, 4 Mar 2014 23:22:55 +0800 Subject: [PATCH 52/82] Fix up global in view test --- test/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index d18b40af1..bb79cbb5f 100644 --- a/test/view.js +++ b/test/view.js @@ -287,7 +287,7 @@ document.body.appendChild(button1); document.body.appendChild(button2); - Test = View.extend({ + var Test = View.extend({ events: { click: function(e) { ok(view.el === e.target || e.srcElement); From 6cef2f12e5e8fefe575be8e551124b829c709b8f Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Tue, 4 Mar 2014 23:27:08 +0800 Subject: [PATCH 53/82] Check typeof length instead of instanceof --- test/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index bb79cbb5f..6dac73b43 100644 --- a/test/view.js +++ b/test/view.js @@ -29,7 +29,7 @@ var result = view.$('a b'); strictEqual(result[0].innerHTML, 'test'); - ok(result instanceof NodeList || result instanceof Array || result instanceof Backbone.$); + ok(result.length === +result.length); }); test("_setEl", 2, function() { From 81809e188faecfe5af4618c44a22e2a71f5d6146 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 4 Mar 2014 23:07:28 +0200 Subject: [PATCH 54/82] add global Element guard for IE8 --- test/view.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/view.js b/test/view.js index 6dac73b43..0afff3bd0 100644 --- a/test/view.js +++ b/test/view.js @@ -379,9 +379,10 @@ }); // Cross-browser helpers - var addEventListener = Element.prototype.addEventListener || function(eventName, listener) { - return this.attachEvent('on' + eventName, listener); - }; + var addEventListener = + typeof Element != 'undefined' && Element.prototype.addEventListener || function(eventName, listener) { + return this.attachEvent('on' + eventName, listener); + }; function click(element) { var event; From 7e95b9cc2b2b3cbc1da39d1e58d46d61e0d32ead Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 5 Mar 2014 01:07:24 +0200 Subject: [PATCH 55/82] short-circuit on attributes --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 01d1ecddf..53096f1e4 100644 --- a/backbone.js +++ b/backbone.js @@ -1147,7 +1147,7 @@ // can override this to utilize an alternative DOM manipulation API. _setEl: function(el, attributes) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.$el.attr(attributes || {}); + if (attributes) this.$el.attr(attributes); this.el = this.$el[0]; }, From c5aa603b4808c48f161ace8b2ec15b27e5788d31 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 5 Mar 2014 01:13:56 +0200 Subject: [PATCH 56/82] rephrase _setEl comment --- backbone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 53096f1e4..a5f02ec00 100644 --- a/backbone.js +++ b/backbone.js @@ -1141,10 +1141,10 @@ return this; }, - // Creates the actual context for this view using the given `el` and a - // hash of `attributes` and returns the created element. `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. + // Creates the `this.el` and `this.$el` references for the 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. _setEl: function(el, attributes) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); if (attributes) this.$el.attr(attributes); From 986540f46f07d53257a293013938863f7b083a48 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 5 Mar 2014 09:07:35 +0200 Subject: [PATCH 57/82] swap argument ordering to setElement, boolean goes last --- backbone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index a5f02ec00..a1f0d58d8 100644 --- a/backbone.js +++ b/backbone.js @@ -1080,8 +1080,8 @@ }, // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate, attributes) { + // re-delegation. Pass along a set of attributes to be applied to the element. + setElement: function(element, attributes, delegate) { this.undelegateEvents(); this._setEl(element, attributes); if (delegate !== false) this.delegateEvents(); @@ -1160,9 +1160,9 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this.setElement(document.createElement(_.result(this, 'tagName')), false, attrs); + this.setElement(document.createElement(_.result(this, 'tagName')), attrs, false); } else { - this.setElement(_.result(this, 'el'), false); + this.setElement(_.result(this, 'el'), null, false); } } From 1cb19be4e93d7a0510fb7f8f3d870298cd831be4 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Wed, 5 Mar 2014 18:25:25 +0800 Subject: [PATCH 58/82] Test for view.remove and view._removeElement --- test/view.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/view.js b/test/view.js index 0afff3bd0..24caaf7d1 100644 --- a/test/view.js +++ b/test/view.js @@ -330,8 +330,8 @@ test("views stopListening", 0, function() { var Test = 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); }); } }); @@ -378,6 +378,27 @@ equal(counter, 2); }); + test("remove", 2, function() { + document.body.appendChild(view.el); + + addEventListener.call(view.el, 'click', function() { ok(true); }); + view.delegate('click', function() { ok(false); }); + view.listenTo(view, 'all x', function() { ok(false); }); + + view.remove(); + + notEqual(view.el.parentNode, document.body); + + click(view.el); + view.trigger('x'); + }); + + test("_removeElement", 1, function() { + document.body.appendChild(view.el); + view._removeElement(); + notEqual(view.el.parentNode, document.body); + }); + // Cross-browser helpers var addEventListener = typeof Element != 'undefined' && Element.prototype.addEventListener || function(eventName, listener) { From 6d6999976ab9b03f4f02e5b54ca39098d7415d14 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 5 Mar 2014 09:45:00 +0200 Subject: [PATCH 59/82] test _removeElement --- test/view.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/view.js b/test/view.js index 24caaf7d1..5ec9d9d41 100644 --- a/test/view.js +++ b/test/view.js @@ -393,10 +393,12 @@ view.trigger('x'); }); - test("_removeElement", 1, function() { + test('_removeElement', 2, function() { + var el = view.el; document.body.appendChild(view.el); view._removeElement(); - notEqual(view.el.parentNode, document.body); + strictEqual(view.el, el); + ok(!document.body.contains(view.el)); }); // Cross-browser helpers From 9eff496762171221a8f020b428dc199459a8aa87 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 6 Mar 2014 11:14:16 +0400 Subject: [PATCH 60/82] Revert "View conformance tests" This reverts commit 7eedd4bc0a5f7d336304d868f5215d868b609966. Conflicts: test/view.js --- test/view.js | 257 ++++++++++++++++++++------------------------------- 1 file changed, 99 insertions(+), 158 deletions(-) diff --git a/test/view.js b/test/view.js index 5ec9d9d41..dde1022e7 100644 --- a/test/view.js +++ b/test/view.js @@ -1,14 +1,11 @@ (function() { - // When testing alternative View implementations, change this varaible. - var View = Backbone.View; - var view; module("Backbone.View", { setup: function() { - view = new View({ + view = new Backbone.View({ id : 'test-view', className : 'test-view', other : 'non-special-option' @@ -24,7 +21,7 @@ }); test("$", 2, function() { - var view = new View; + var view = new Backbone.View; view.setElement('

test

'); var result = view.$('a b'); @@ -32,43 +29,47 @@ ok(result.length === +result.length); }); - test("_setEl", 2, function() { - view._setEl('
', {id: 'test-div'}); - equal(view.el.tagName.toLowerCase(), 'div'); - equal(view.el.id, 'test-div'); + test("jQuery", function() { + var view = new Backbone.View; + view.setElement('

test

'); + + if (Backbone.$) { + ok(view.$el instanceof Backbone.$); + equal(view.$el[0], view.el); + } }); test("initialize", 1, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ initialize: function() { this.one = 1; } }); - strictEqual(new Test().one, 1); + strictEqual(new View().one, 1); }); test("delegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - view = new View({el: '#testElement'}); + var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; - addEventListener.call(view.el, 'click', function(){ counter2++; }); + view.$el.on('click', function(){ counter2++; }); var events = {'click h1': 'increment'}; view.delegateEvents(events); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 1); equal(counter2, 1); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 2); view.delegateEvents(events); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 3); equal(counter2, 3); }); @@ -76,20 +77,20 @@ test("delegate", 2, function() { var counter1 = 0, counter2 = 0; - view = new View({el: '#testElement'}); + var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; - view.delegate('click', function(){ counter2++; }); + view.$el.on('click', function(){ counter2++; }); view.delegate('click', 'h1', view.increment); view.delegate('click', view.increment); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 1); }); test("delegateEvents allows functions for callbacks", 3, function() { - view = new View({el: '

'}); + var view = new Backbone.View({el: '

'}); view.counter = 0; var events = { @@ -98,89 +99,77 @@ } }; - document.body.appendChild(view.el); - view.delegateEvents(events); - click(view.el); + view.$el.trigger('click'); equal(view.counter, 1); - click(view.el); + view.$el.trigger('click'); equal(view.counter, 2); view.delegateEvents(events); - click(view.el); + view.$el.trigger('click'); equal(view.counter, 3); - - document.body.removeChild(view.el); }); test("delegateEvents ignore undefined methods", 0, function() { - view = new View({el: '

'}); - - document.body.appendChild(view.el); - + var view = new Backbone.View({el: '

'}); view.delegateEvents({'click': 'undefinedMethod'}); - click(view.el); - - document.body.removeChild(view.el); + view.$el.trigger('click'); }); test("undelegateEvents", 6, function() { var counter1 = 0, counter2 = 0; - view = new View({el: '#testElement'}); + var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; - addEventListener.call(view.el, 'click', function(){ counter2++; }); + view.$el.on('click', function(){ counter2++; }); var events = {'click h1': 'increment'}; view.delegateEvents(events); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 1); equal(counter2, 1); view.undelegateEvents(); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 1); equal(counter2, 2); view.delegateEvents(events); - click(view.$('h1')[0]); + view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 3); }); test("_ensureElement with DOM node el", 1, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ el: document.body }); - equal(new Test().el, document.body); + equal(new View().el, document.body); }); test("_ensureElement with string el", 3, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ el: "body" }); - strictEqual(new Test().el, document.body); + strictEqual(new View().el, document.body); - Test = View.extend({ + View = Backbone.View.extend({ el: "#testElement > h1" }); - var h1 = _.filter(document.getElementById('testElement').childNodes, function(node) { - return node.nodeType == 1; - })[0]; - strictEqual(new Test().el, h1); + strictEqual(new View().el, $("#testElement > h1").get(0)); - Test = View.extend({ + View = Backbone.View.extend({ el: "#nonexistent" }); - ok(!new Test().el); + ok(!new View().el); }); test("with className and id functions", 2, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ className: function() { return 'className'; }, @@ -189,38 +178,38 @@ } }); - strictEqual(new Test().el.className, 'className'); - strictEqual(new Test().el.id, 'id'); + strictEqual(new View().el.className, 'className'); + strictEqual(new View().el.id, 'id'); }); test("with attributes", 2, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ attributes: { id: 'id', 'class': 'class' } }); - strictEqual(new Test().el.className, 'class'); - strictEqual(new Test().el.id, 'id'); + strictEqual(new View().el.className, 'class'); + strictEqual(new View().el.id, 'id'); }); test("with attributes as a function", 1, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ attributes: function() { return {'class': 'dynamic'}; } }); - strictEqual(new Test().el.className, 'dynamic'); + strictEqual(new View().el.className, 'dynamic'); }); test("multiple views per element", 3, function() { var count = 0; - var el = document.createElement('p'); + var $el = $('

'); - var Test = View.extend({ - el: el, + var View = Backbone.View.extend({ + el: $el, events: { click: function() { count++; @@ -228,114 +217,101 @@ } }); - document.body.appendChild(el); - - var view1 = new Test; - click(el); + var view1 = new View; + $el.trigger("click"); equal(1, count); - var view2 = new Test; - click(el); + var view2 = new View; + $el.trigger("click"); equal(3, count); view1.delegateEvents(); - click(el); + $el.trigger("click"); equal(5, count); - - document.body.removeChild(el); }); - if (typeof Backbone.$ != 'undefined') { - test("custom events, with namespaces", 2, function() { - var count = 0; - - var Test = View.extend({ - el: $('body'), - events: function() { - return {"fake$event.namespaced": "run"}; - }, - run: function() { - count++; - } - }); + test("custom events, with namespaces", 2, function() { + var count = 0; - var view = new Test; - $('body').trigger('fake$event').trigger('fake$event'); - equal(count, 2); + var View = Backbone.View.extend({ + el: $('body'), + events: function() { + return {"fake$event.namespaced": "run"}; + }, + run: function() { + count++; + } + }); - $('body').off('.namespaced'); - $('body').trigger('fake$event'); - equal(count, 2); + var view = new View; + $('body').trigger('fake$event').trigger('fake$event'); + equal(count, 2); - }); - } + $('body').off('.namespaced'); + $('body').trigger('fake$event'); + equal(count, 2); + }); test("#1048 - setElement uses provided object.", 2, function() { - var el = document.body; + var $el = $('body'); - view = new View({el: el}); - ok(view.el === el); + var view = new Backbone.View({el: $el}); + ok(view.$el === $el); - view.setElement(el = document.body); - ok(view.el === el); + view.setElement($el = $($el)); + ok(view.$el === $el); }); test("#986 - Undelegate before changing element.", 1, function() { - var button1 = document.createElement('button'); - var button2 = document.createElement('button'); + var button1 = $(''); + var button2 = $(''); - document.body.appendChild(button1); - document.body.appendChild(button2); - - var Test = View.extend({ + var View = Backbone.View.extend({ events: { click: function(e) { - ok(view.el === e.target || e.srcElement); + ok(view.el === e.target); } } }); - var view = new Test({el: button1}); + var view = new View({el: button1}); view.setElement(button2); - click(button1); - click(button2); - - document.body.removeChild(button1); - document.body.removeChild(button2); + button1.trigger('click'); + button2.trigger('click'); }); test("#1172 - Clone attributes object", 2, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ attributes: {foo: 'bar'} }); - var view1 = new Test({id: 'foo'}); + var view1 = new View({id: 'foo'}); strictEqual(view1.el.id, 'foo'); - var view2 = new Test(); + var view2 = new View(); ok(!view2.el.id); }); test("#1228 - tagName can be provided as a function", 1, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ tagName: function() { return 'p'; } }); - ok(new Test().el.tagName.toLowerCase() == 'p'); + ok(new View().el.tagName.toLowerCase() == 'p'); }); test("views stopListening", 0, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ initialize: function() { this.listenTo(this.model, 'all x', function(){ ok(false); }); this.listenTo(this.collection, 'all x', function(){ ok(false); }); } }); - var view = new Test({ + var view = new View({ model: new Backbone.Model, collection: new Backbone.Collection }); @@ -346,13 +322,13 @@ }); test("Provide function for el.", 2, function() { - var Test = View.extend({ + var View = Backbone.View.extend({ el: function() { return "

"; } }); - var view = new Test; + var view = new View; ok(view.el.tagName.toLowerCase() == 'p'); ok(view.$('a').length != 0); }); @@ -360,36 +336,32 @@ test("events passed in options", 1, function() { var counter = 0; - var Test = View.extend({ + var View = Backbone.View.extend({ el: '#testElement', increment: function() { counter++; } }); - var view = new Test({ + var view = new View({ events: { 'click h1': 'increment' } }); - click(view.$('h1')[0]); - click(view.$('h1')[0]); + view.$('h1').trigger('click').trigger('click'); equal(counter, 2); }); - test("remove", 2, function() { + test("remove", 0, function() { document.body.appendChild(view.el); - addEventListener.call(view.el, 'click', function() { ok(true); }); view.delegate('click', function() { ok(false); }); view.listenTo(view, 'all x', function() { ok(false); }); view.remove(); - notEqual(view.el.parentNode, document.body); - - click(view.el); + view.$el.trigger('x'); view.trigger('x'); }); @@ -401,35 +373,4 @@ ok(!document.body.contains(view.el)); }); - // Cross-browser helpers - var addEventListener = - typeof Element != 'undefined' && Element.prototype.addEventListener || function(eventName, listener) { - return this.attachEvent('on' + eventName, listener); - }; - - function click(element) { - var event; - if (document.createEvent) { - event = document.createEvent('MouseEvent'); - var args = [ - 'click', true, true, - // IE 10+ and Firefox require these - event.view, event.detail, event.screenX, event.screenY, event.clientX, - event.clientY, event.ctrlKey, event.altKey, event.shiftKey, - event.metaKey, event.button, event.relatedTarget - ]; - (event.initMouseEvent || event.initEvent).apply(event, args); - } else { - event = document.createEventObject(); - event.type = 'click'; - event.bubbles = true; - event.cancelable = true; - } - - if (element.dispatchEvent) { - return element.dispatchEvent(event); - } - element.fireEvent('onclick', event); - } - })(); From bef78ef4964e9979b4724383ab5b2feab79fd706 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 6 Mar 2014 15:22:13 +0400 Subject: [PATCH 61/82] test setEl and el.parentNode --- test/view.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index dde1022e7..f4ed4504d 100644 --- a/test/view.js +++ b/test/view.js @@ -29,6 +29,11 @@ ok(result.length === +result.length); }); + test("_setEl", 2, function() { + view._setEl('
', {id: 'test-div'}); + strictEqual(view.el.tagName.toLowerCase(), 'div'); + strictEqual(view.el.id, 'test-div'); + }); test("jQuery", function() { var view = new Backbone.View; @@ -37,6 +42,8 @@ if (Backbone.$) { ok(view.$el instanceof Backbone.$); equal(view.$el[0], view.el); + } else { + ok(view.el.nodeType === 1); } }); @@ -370,7 +377,7 @@ document.body.appendChild(view.el); view._removeElement(); strictEqual(view.el, el); - ok(!document.body.contains(view.el)); + strictEqual(view.el.parentNode, null); }); })(); From c62708f04b1f38c8d9e1f353c8173183f3040b10 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 6 Mar 2014 15:26:44 +0400 Subject: [PATCH 62/82] use notEqual body isntead of equal null --- test/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index f4ed4504d..8b4225660 100644 --- a/test/view.js +++ b/test/view.js @@ -377,7 +377,7 @@ document.body.appendChild(view.el); view._removeElement(); strictEqual(view.el, el); - strictEqual(view.el.parentNode, null); + notEqual(view.el.parentNode, document.body); }); })(); From d9f6c64719089062afb1bebbc68af49cefcad755 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Sun, 9 Mar 2014 11:31:40 +0400 Subject: [PATCH 63/82] remove undelegateEvents call from remove --- backbone.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backbone.js b/backbone.js index a1f0d58d8..36f7cbc13 100644 --- a/backbone.js +++ b/backbone.js @@ -1066,7 +1066,6 @@ // the DOM event listeners attached to it, and remove any applicable // Backbone.Events listeners. remove: function() { - this.undelegateEvents(); this._removeElement(); this.stopListening(); return this; From 58c9a2abc6a5a5d1189ed0e645c08ab883a9e37c Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Sun, 9 Mar 2014 11:31:40 +0400 Subject: [PATCH 64/82] remove undelegateEvents call from remove --- backbone.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backbone.js b/backbone.js index a1f0d58d8..36f7cbc13 100644 --- a/backbone.js +++ b/backbone.js @@ -1066,7 +1066,6 @@ // the DOM event listeners attached to it, and remove any applicable // Backbone.Events listeners. remove: function() { - this.undelegateEvents(); this._removeElement(); this.stopListening(); return this; From b840669fa77f9a27b1e8a46bc60ca80062f8d82f Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sun, 9 Mar 2014 02:19:11 +0800 Subject: [PATCH 65/82] Rename _removeElement to _remove and test _remove for event undelegation --- backbone.js | 4 ++-- test/view.js | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backbone.js b/backbone.js index 36f7cbc13..a3828521b 100644 --- a/backbone.js +++ b/backbone.js @@ -1066,7 +1066,7 @@ // the DOM event listeners attached to it, and remove any applicable // Backbone.Events listeners. remove: function() { - this._removeElement(); + this._remove(); this.stopListening(); return this; }, @@ -1074,7 +1074,7 @@ // Remove this view's element from the document and remove all the event // listeners attached to it. Useful for subclasses to override in order to // utilize an alternative DOM manipulation API. - _removeElement: function() { + _remove: function() { this.$el.remove(); }, diff --git a/test/view.js b/test/view.js index 8b4225660..fc8203982 100644 --- a/test/view.js +++ b/test/view.js @@ -363,21 +363,25 @@ test("remove", 0, function() { document.body.appendChild(view.el); - view.delegate('click', function() { ok(false); }); view.listenTo(view, 'all x', function() { ok(false); }); view.remove(); view.$el.trigger('x'); - view.trigger('x'); }); - test('_removeElement', 2, function() { + test('_remove', 2, function() { var el = view.el; document.body.appendChild(view.el); - view._removeElement(); + + view.delegate('click', function() { ok(false); }); + + view._remove(); + strictEqual(view.el, el); notEqual(view.el.parentNode, document.body); + + view.trigger('x'); }); })(); From 393a209ca8c053829b7bc1abcd0321dcdb5e2e6a Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Sun, 9 Mar 2014 17:05:20 +0800 Subject: [PATCH 66/82] Merge _remove and remove tests and remove _setEl tests --- test/view.js | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/test/view.js b/test/view.js index fc8203982..d1f377f79 100644 --- a/test/view.js +++ b/test/view.js @@ -29,12 +29,6 @@ ok(result.length === +result.length); }); - test("_setEl", 2, function() { - view._setEl('
', {id: 'test-div'}); - strictEqual(view.el.tagName.toLowerCase(), 'div'); - strictEqual(view.el.id, 'test-div'); - }); - test("jQuery", function() { var view = new Backbone.View; view.setElement('

test

'); @@ -360,27 +354,19 @@ equal(counter, 2); }); - test("remove", 0, function() { - document.body.appendChild(view.el); - - view.listenTo(view, 'all x', function() { ok(false); }); - - view.remove(); - - view.$el.trigger('x'); - }); - - test('_remove', 2, function() { + test("remove", 2, function() { var el = view.el; document.body.appendChild(view.el); view.delegate('click', function() { ok(false); }); + view.listenTo(view, 'all x', function() { ok(false); }); - view._remove(); + view.remove(); strictEqual(view.el, el); notEqual(view.el.parentNode, document.body); + view.$el.trigger('click'); view.trigger('x'); }); From 20ed07489eb875999b2c2dbe11644cf4b315a856 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 11 Mar 2014 11:29:07 +0400 Subject: [PATCH 67/82] rename _setEl to _setElement for parity --- backbone.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backbone.js b/backbone.js index a3828521b..1d06671f8 100644 --- a/backbone.js +++ b/backbone.js @@ -1082,11 +1082,21 @@ // re-delegation. Pass along a set of attributes to be applied to the element. setElement: function(element, attributes, delegate) { this.undelegateEvents(); - this._setEl(element, attributes); + this._setElement(element, attributes); if (delegate !== false) this.delegateEvents(); return this; }, + // Creates the `this.el` and `this.$el` references for the 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. + _setElement: function(el, attributes) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + if (attributes) this.$el.attr(attributes); + this.el = this.$el[0]; + }, + // Set callbacks, where `this.events` is a hash of // // *{"event selector": "callback"}* @@ -1140,16 +1150,6 @@ return this; }, - // Creates the `this.el` and `this.$el` references for the 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. - _setEl: function(el, attributes) { - this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - if (attributes) this.$el.attr(attributes); - this.el = this.$el[0]; - }, - // 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 From be11f3a0105c3f750fa3e4598b8e8f2592e5f9fd Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 12 Mar 2014 10:10:37 +0400 Subject: [PATCH 68/82] add an undelegate method to View and pass back the listener from delegate --- backbone.js | 19 ++++++++++++++----- test/view.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backbone.js b/backbone.js index 1d06671f8..149d95f3e 100644 --- a/backbone.js +++ b/backbone.js @@ -1129,17 +1129,19 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can - // override this to utilize an alternative DOM event management API. + // override this to utilize an alternative DOM event management API. Returns + // the listener for easy undelegation with `undelegate`. delegate: function(eventName, selector, listener) { eventName += '.delegateEvents' + this.cid; - if (!selector) { + // When `delegate` is called with two arguments, `selector` is actually + // the `listener` + var listener = _.isFunction(selector) ? selector : listener; + if (selector === '') { this.$el.on(eventName, listener); } else { - // When `delegate` is called with two arguments, `selector` is actually - // the `listener` this.$el.on(eventName, selector, listener); } - return this; + return listener; }, // Clears all callbacks previously bound to the view by `delegateEvents`. @@ -1150,6 +1152,13 @@ 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); + }, + // 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 diff --git a/test/view.js b/test/view.js index d1f377f79..237e075f6 100644 --- a/test/view.js +++ b/test/view.js @@ -144,6 +144,37 @@ equal(counter2, 3); }); + test("undelegate", 0, function() { + 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() { + var listener = view.delegate('click', function() { ok(false); }); + view.delegate('click', function() { ok(true); }); + view.undelegate('click', listener); + view.$el.trigger('click'); + }); + + test("undelegate with selector", 2, function() { + var counter1 = 0, counter2 = 0; + view.delegate('click', function() { counter1++; }); + view.delegate('click', 'h1', function() { counter2++; }); + + view.undelegate('click', 'h1'); + + view.$('h1').trigger('click'); + view.$el.trigger('click'); + + equal(counter1, 1); + equal(counter2, 0); + }); + test("_ensureElement with DOM node el", 1, function() { var View = Backbone.View.extend({ el: document.body From d21fd79d8f5734f02585d2984a99aa90a0c5295d Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 12 Mar 2014 15:16:33 +0400 Subject: [PATCH 69/82] remove namespace test from custom events test. --- test/view.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/view.js b/test/view.js index 237e075f6..c347d5cff 100644 --- a/test/view.js +++ b/test/view.js @@ -262,13 +262,13 @@ equal(5, count); }); - test("custom events, with namespaces", 2, function() { + test("custom events", 2, function() { var count = 0; var View = Backbone.View.extend({ el: $('body'), events: function() { - return {"fake$event.namespaced": "run"}; + return {"fake$event": "run"}; }, run: function() { count++; @@ -279,7 +279,7 @@ $('body').trigger('fake$event').trigger('fake$event'); equal(count, 2); - $('body').off('.namespaced'); + $('body').off('fake$event'); $('body').trigger('fake$event'); equal(count, 2); }); From d99e43b5647b9637a302d953c398cae711299016 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 12 Mar 2014 15:20:40 +0400 Subject: [PATCH 70/82] test for undelegate with handler and selector, and use an element that exists in the DOM for bubbling --- test/view.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index c347d5cff..ea535b525 100644 --- a/test/view.js +++ b/test/view.js @@ -145,6 +145,7 @@ }); test("undelegate", 0, function() { + view = new Backbone.View({el: '#testElement'}); view.delegate('click', function() { ok(false); }); view.delegate('click', 'h1', function() { ok(false); }); @@ -155,6 +156,7 @@ }) test("undelegate with passed handler", 1, function() { + view = new Backbone.View({el: '#testElement'}); var listener = view.delegate('click', function() { ok(false); }); view.delegate('click', function() { ok(true); }); view.undelegate('click', listener); @@ -163,6 +165,7 @@ test("undelegate with selector", 2, function() { var counter1 = 0, counter2 = 0; + view = new Backbone.View({el: '#testElement'}); view.delegate('click', function() { counter1++; }); view.delegate('click', 'h1', function() { counter2++; }); @@ -171,7 +174,22 @@ view.$('h1').trigger('click'); view.$el.trigger('click'); - equal(counter1, 1); + equal(counter1, 2); + equal(counter2, 0); + }); + + test("undelegate with handler and selector", 2, function() { + var counter1 = 0, counter2 = 0; + view = new Backbone.View({el: '#testElement'}); + view.delegate('click', function() { counter1++; }); + var handler = view.delegate('click', 'h1', function() { counter2++; }); + + view.undelegate('click', 'h1', handler); + + view.$('h1').trigger('click'); + view.$el.trigger('click'); + + equal(counter1, 2); equal(counter2, 0); }); From 3850d37def7d4b9386d94f8708cab36985dbb308 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Wed, 12 Mar 2014 19:55:08 +0400 Subject: [PATCH 71/82] add missing semi --- test/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view.js b/test/view.js index ea535b525..f985e3369 100644 --- a/test/view.js +++ b/test/view.js @@ -153,7 +153,7 @@ view.$('h1').trigger('click'); view.$el.trigger('click'); - }) + }); test("undelegate with passed handler", 1, function() { view = new Backbone.View({el: '#testElement'}); From b59b621b29064183718e3c504a4887223345afea Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 13 Mar 2014 12:20:13 +0400 Subject: [PATCH 72/82] shorter delegate method thanks to jQuery empty selector checks --- backbone.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/backbone.js b/backbone.js index 149d95f3e..ef8df7acc 100644 --- a/backbone.js +++ b/backbone.js @@ -1129,19 +1129,14 @@ // Add a single event listener to the element responding only to the // optional `selector` or catches all `eventName` events. Subclasses can - // override this to utilize an alternative DOM event management API. Returns - // the listener for easy undelegation with `undelegate`. + // override this to utilize an alternative DOM event management API. delegate: function(eventName, selector, listener) { - eventName += '.delegateEvents' + this.cid; - // When `delegate` is called with two arguments, `selector` is actually - // the `listener` - var listener = _.isFunction(selector) ? selector : listener; - if (selector === '') { - this.$el.on(eventName, listener); - } else { - this.$el.on(eventName, selector, listener); - } - return listener; + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + + // Return the listener for easy undelegation with `undelegate`. When + // `delegate` is called with two arguments, `selector` is actually the + // `listener`. + return _.isFunction(selector) ? selector : listener; }, // Clears all callbacks previously bound to the view by `delegateEvents`. From 291f3bcde4bcb3771d4f23a0657de9e34093c019 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 13 Mar 2014 12:59:36 +0400 Subject: [PATCH 73/82] nicer jq test --- test/view.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/view.js b/test/view.js index f985e3369..763e6d2d1 100644 --- a/test/view.js +++ b/test/view.js @@ -33,11 +33,13 @@ var view = new Backbone.View; view.setElement('

test

'); + strictEqual(view.el.nodeType, 1); + if (Backbone.$) { ok(view.$el instanceof Backbone.$); - equal(view.$el[0], view.el); + strictEqual(view.$el[0], view.el); } else { - ok(view.el.nodeType === 1); + strictEqual(view.$el, undefined); } }); From 764eef407bd499554228c4205ff9cce82fee707e Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 13 Mar 2014 15:27:35 +0400 Subject: [PATCH 74/82] shorter undelegate --- backbone.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index ef8df7acc..f125579a1 100644 --- a/backbone.js +++ b/backbone.js @@ -1150,8 +1150,7 @@ // 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); + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); }, // Ensure that the View has a DOM element to render into. From 264bd9b3da59eda920a8168a405926463881b6b0 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Thu, 13 Mar 2014 20:33:48 +0400 Subject: [PATCH 75/82] comments cleanup --- backbone.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/backbone.js b/backbone.js index f125579a1..28712a4a0 100644 --- a/backbone.js +++ b/backbone.js @@ -1071,15 +1071,15 @@ return this; }, - // Remove this view's element from the document and remove all the event - // listeners attached to it. Useful for subclasses to override in order to - // utilize an alternative DOM manipulation API. + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. _remove: function() { this.$el.remove(); }, - // Change the view's element (`this.el` property), including event - // re-delegation. Pass along a set of attributes to be applied to the element. + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. setElement: function(element, attributes, delegate) { this.undelegateEvents(); this._setElement(element, attributes); @@ -1087,10 +1087,11 @@ return this; }, - // Creates the `this.el` and `this.$el` references for the View using the + // 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. + // this to utilize an alternative DOM manipulation API and are only required + // to set the `this.el` property. _setElement: function(el, attributes) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); if (attributes) this.$el.attr(attributes); @@ -1110,8 +1111,6 @@ // 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(); @@ -1127,9 +1126,9 @@ return this; }, - // Add a single event listener to the element responding only to the - // optional `selector` or catches all `eventName` events. Subclasses can - // override this to utilize an alternative DOM event management API. + // 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); @@ -1147,8 +1146,8 @@ return this; }, - // Remove a single event from the delegated events. `selector` and `listener` - // are both optional. + // 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); }, From 06586f3283505fc3aea63383bd07ddc3ef9cce54 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 14 Mar 2014 06:23:24 +0400 Subject: [PATCH 76/82] shorter comment --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 045d2bd5b..b33c1ea69 100644 --- a/backbone.js +++ b/backbone.js @@ -1063,7 +1063,7 @@ }, // Remove this view by taking the element out of the document, remove all - // the DOM event listeners attached to it, and remove any applicable + // the DOM event listeners attached to it and any applicable // Backbone.Events listeners. remove: function() { this._remove(); From f67c7a11c3ea8c0e0a0a0ff96db03fd6f9eb2769 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 14 Mar 2014 06:35:20 +0400 Subject: [PATCH 77/82] test delegate returns listener --- test/view.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/view.js b/test/view.js index 763e6d2d1..e3b009c90 100644 --- a/test/view.js +++ b/test/view.js @@ -77,19 +77,21 @@ equal(counter2, 3); }); - test("delegate", 2, function() { + test("delegate", 4, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - view.delegate('click', 'h1', view.increment); - view.delegate('click', view.increment); + var listener1 = view.delegate('click', 'h1', view.increment); + var listener2 = view.delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 1); + strictEqual(listener1, view.increment); + strictEqual(listener2, view.increment); }); test("delegateEvents allows functions for callbacks", 3, function() { From 3f79ea88bf6f96af4284098ecb602e56afc956a2 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 14 Mar 2014 06:36:20 +0400 Subject: [PATCH 78/82] fix leftover merge conflict --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index b33c1ea69..088adb167 100644 --- a/backbone.js +++ b/backbone.js @@ -1163,7 +1163,7 @@ if (this.className) attrs['class'] = _.result(this, 'className'); this.setElement(document.createElement(_.result(this, 'tagName')), attrs); } else { - this.setElement(_.result(this, 'el'), null, false); + this.setElement(_.result(this, 'el')); } } From 4e8caf447120a951d389eaa373217cdc3a270325 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Fri, 14 Mar 2014 06:38:18 +0400 Subject: [PATCH 79/82] no need for repeated comment on `remove` --- backbone.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index 088adb167..0928625f4 100644 --- a/backbone.js +++ b/backbone.js @@ -1062,9 +1062,8 @@ return this; }, - // Remove this view by taking the element out of the document, remove all - // the DOM event listeners attached to it and any applicable - // Backbone.Events listeners. + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. remove: function() { this._remove(); this.stopListening(); From ad39972d5d666ac15e4b77c0d489821218236d18 Mon Sep 17 00:00:00 2001 From: Jimmy Yuen Ho Wong Date: Fri, 14 Mar 2014 18:04:18 +0800 Subject: [PATCH 80/82] Return undefined from delegate --- backbone.js | 5 ----- test/view.js | 11 +++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/backbone.js b/backbone.js index 0928625f4..357211ed9 100644 --- a/backbone.js +++ b/backbone.js @@ -1130,11 +1130,6 @@ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. delegate: function(eventName, selector, listener) { this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); - - // Return the listener for easy undelegation with `undelegate`. When - // `delegate` is called with two arguments, `selector` is actually the - // `listener`. - return _.isFunction(selector) ? selector : listener; }, // Clears all callbacks previously bound to the view by `delegateEvents`. diff --git a/test/view.js b/test/view.js index e3b009c90..50f267d8e 100644 --- a/test/view.js +++ b/test/view.js @@ -77,21 +77,19 @@ equal(counter2, 3); }); - test("delegate", 4, function() { + test("delegate", 2, function() { var counter1 = 0, counter2 = 0; var view = new Backbone.View({el: '#testElement'}); view.increment = function(){ counter1++; }; view.$el.on('click', function(){ counter2++; }); - var listener1 = view.delegate('click', 'h1', view.increment); - var listener2 = view.delegate('click', view.increment); + view.delegate('click', 'h1', view.increment); + view.delegate('click', view.increment); view.$('h1').trigger('click'); equal(counter1, 2); equal(counter2, 1); - strictEqual(listener1, view.increment); - strictEqual(listener2, view.increment); }); test("delegateEvents allows functions for callbacks", 3, function() { @@ -161,7 +159,8 @@ test("undelegate with passed handler", 1, function() { view = new Backbone.View({el: '#testElement'}); - var listener = view.delegate('click', function() { ok(false); }); + var listener = function() { ok(false); }; + view.delegate('click', listener); view.delegate('click', function() { ok(true); }); view.undelegate('click', listener); view.$el.trigger('click'); From 72f7da420891092d271556a21d1807bc6811c064 Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Fri, 14 Mar 2014 07:10:40 -0400 Subject: [PATCH 81/82] A little clarification. * Rename #_remove as #_removeElement. * Add #_setAttributes instead of using #setElement. * Slim down tests a bit. * #delegate returns this. --- backbone.js | 8 +++--- test/view.js | 76 +++++++++++++++++----------------------------------- 2 files changed, 27 insertions(+), 57 deletions(-) diff --git a/backbone.js b/backbone.js index 357211ed9..ede19bc18 100644 --- a/backbone.js +++ b/backbone.js @@ -1065,7 +1065,7 @@ // Remove this view by taking the element out of the DOM, and removing any // applicable Backbone.Events listeners. remove: function() { - this._remove(); + this._removeElement(); this.stopListening(); return this; }, @@ -1073,7 +1073,7 @@ // Remove this view's element from the document and all event listeners // attached to it. Exposed for subclasses using an alternative DOM // manipulation API. - _remove: function() { + _removeElement: function() { this.$el.remove(); }, @@ -1117,10 +1117,8 @@ 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]; - this.delegate(eventName, selector, _.bind(method, this)); + this.delegate(match[1], match[2], _.bind(method, this)); } return this; }, diff --git a/test/view.js b/test/view.js index 50f267d8e..b8275dd5a 100644 --- a/test/view.js +++ b/test/view.js @@ -29,18 +29,13 @@ ok(result.length === +result.length); }); - test("jQuery", function() { + test("$el", 3, function() { var view = new Backbone.View; view.setElement('

test

'); - strictEqual(view.el.nodeType, 1); - if (Backbone.$) { - ok(view.$el instanceof Backbone.$); - strictEqual(view.$el[0], view.el); - } else { - strictEqual(view.$el, undefined); - } + ok(view.$el instanceof Backbone.$); + strictEqual(view.$el[0], view.el); }); test("initialize", 1, function() { @@ -78,18 +73,14 @@ }); test("delegate", 2, function() { - var counter1 = 0, counter2 = 0; - var view = new Backbone.View({el: '#testElement'}); - view.increment = function(){ counter1++; }; - view.$el.on('click', function(){ counter2++; }); - - view.delegate('click', 'h1', view.increment); - view.delegate('click', view.increment); - + view.delegate('click', 'h1', function() { + ok(true); + }); + view.delegate('click', function() { + ok(true); + }); view.$('h1').trigger('click'); - equal(counter1, 2); - equal(counter2, 1); }); test("delegateEvents allows functions for callbacks", 3, function() { @@ -167,33 +158,22 @@ }); test("undelegate with selector", 2, function() { - var counter1 = 0, counter2 = 0; view = new Backbone.View({el: '#testElement'}); - view.delegate('click', function() { counter1++; }); - view.delegate('click', 'h1', function() { counter2++; }); - + 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'); - - equal(counter1, 2); - equal(counter2, 0); }); test("undelegate with handler and selector", 2, function() { - var counter1 = 0, counter2 = 0; view = new Backbone.View({el: '#testElement'}); - view.delegate('click', function() { counter1++; }); - var handler = view.delegate('click', 'h1', function() { counter2++; }); - + 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'); - - equal(counter1, 2); - equal(counter2, 0); }); test("_ensureElement with DOM node el", 1, function() { @@ -284,25 +264,18 @@ }); test("custom events", 2, function() { - var count = 0; - var View = Backbone.View.extend({ el: $('body'), - events: function() { - return {"fake$event": "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('fake$event'); $('body').trigger('fake$event'); - equal(count, 2); }); test("#1048 - setElement uses provided object.", 2, function() { @@ -353,7 +326,7 @@ } }); - ok(new View().el.tagName.toLowerCase() == 'p'); + ok(new View().$el.is('p')); }); test("views stopListening", 0, function() { @@ -382,8 +355,8 @@ }); var view = new View; - ok(view.el.tagName.toLowerCase() == 'p'); - ok(view.$('a').length != 0); + ok(view.$el.is('p')); + ok(view.$el.has('a')); }); test("events passed in options", 1, function() { @@ -406,20 +379,19 @@ equal(counter, 2); }); - test("remove", 2, function() { - var el = view.el; + 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(); - - strictEqual(view.el, el); - notEqual(view.el.parentNode, document.body); - 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); }); })(); From 7180d5ce0ae68fa538ea0d1f278a2636bd403bc4 Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Fri, 14 Mar 2014 12:27:16 -0400 Subject: [PATCH 82/82] Add #_setAttributes and clean up #setElement. --- backbone.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backbone.js b/backbone.js index ede19bc18..ae6c7f6f4 100644 --- a/backbone.js +++ b/backbone.js @@ -1079,9 +1079,9 @@ // Change the view's element (`this.el` property) and re-delegate the // view's events on the new element. - setElement: function(element, attributes) { + setElement: function(element) { this.undelegateEvents(); - this._setElement(element, attributes); + this._setElement(element); this.delegateEvents(); return this; }, @@ -1091,9 +1091,8 @@ // 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, attributes) { + _setElement: function(el) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - if (attributes) this.$el.attr(attributes); this.el = this.$el[0]; }, @@ -1153,10 +1152,17 @@ var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); - this.setElement(document.createElement(_.result(this, 'tagName')), attrs); + 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); } });