diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..4a8ee11f2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig: http://EditorConfig.org +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore index bb763e1f9..b38e3e71f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ raw *.sw? .DS_Store +.idea node_modules bower_components diff --git a/backbone.js b/backbone.js index 55ccb22bd..8ee889ce4 100644 --- a/backbone.js +++ b/backbone.js @@ -157,6 +157,17 @@ return events; }; + // An overridable callback to allow event objects to know when something starts + // listening to it when previously it had no listeners. Useful for knowing this + // object needs to start listening to something else. + // (which might be expensive to run all the time) + Events.onFirstListener = function() {}; + + // An overridable callback to allow event objects to know when the last listener + // stops listening to it. Useful to know this object can stop listening to it's + // sources. + Events.onNoListeners = function() {}; + // Bind an event to a `callback` function. Passing `"all"` will bind // the callback to all events fired. Events.on = function(name, callback, context) { @@ -165,6 +176,10 @@ // Guard the `listening` argument from the public API. var internalOn = function(obj, name, callback, context, listening) { + if (!obj._events || _.isEmpty(obj._events)) { + obj.onFirstListener(); + } + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { context: context, ctx: obj, @@ -222,6 +237,11 @@ context: context, listeners: this._listeners }); + + if (!this._events || _.isEmpty(this._events)) { + this.onNoListeners(); + } + return this; }; diff --git a/test/events.js b/test/events.js index 544b39a19..772397ebf 100644 --- a/test/events.js +++ b/test/events.js @@ -703,4 +703,44 @@ two.trigger('y', 2); }); + QUnit.test('onFirstListener & onNoListeners are triggered', function(assert) { + assert.expect(6); + var first = 1; + var none = 1; + + var expectedFirsts = 0; + var expectedNones = 0; + + var obj = _.extend({}, Backbone.Events, { + onFirstListener: function() { assert.ok(first++ === expectedFirsts); }, + onNoListeners: function() { assert.ok(none++ === expectedNones); } + }); + var obj2 = _.extend({}, Backbone.Events, { + onFirstListener: function() { assert.ok(false, 'Should not have been called'); }, + onNoListeners: function() { assert.ok(false, 'Should not have been called'); } + }); + var fn = function() {}; + + expectedFirsts = 1; + obj.on('a', fn); // expects a call to onFirstListener + obj.on('b', fn); + + obj.off('a', fn); // no call + expectedNones = 1; + obj.off('b', fn); // expects a call to onNoListeners + + expectedFirsts = 2; + obj2.listenTo(obj, 'a', fn); + obj2.listenTo(obj, 'b', fn); + + obj2.stopListening(obj, 'a', fn); + expectedNones = 2; + obj2.stopListening(obj, 'b', fn); + + expectedFirsts = 3; + obj2.listenToOnce(obj, 'a', fn); + expectedNones = 3; + obj.trigger('a'); + }) + })();