Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release-0.2'

  • Loading branch information...
commit ad5e3eb4d81e3ebb6b7081bc913338de411180ab 2 parents ce5106e + e6e4893
@millermedeiros authored
View
34 CHANGELOG.markdown
@@ -0,0 +1,34 @@
+# JS-Signals Changelog #
+
+
+## v0.2 (2010/11/26) ##
+
+### API changes ###
+
+ - Added:
+ - `Signal.prototype.pause()`
+ - `Signal.prototype.resume()`
+ - `Signal.prototype.isPaused()`
+ - `Signal.prototype.stopPropagation()`
+
+### Fixes ###
+
+ - `SignalBinding.prototype.isPaused()`
+
+### Test Changes ###
+
+ - Increased test coverage a lot.
+ - Tests added:
+ - pause/resume (for individual bindings and signal)
+ - stopPropagation (using `return false` and `Signal.prototype.stopPropagation()`)
+ - `SignalBindings.prototype.isOnce()`
+ - if same listener added twice returns same binding
+
+### Other ###
+
+Small refactoring and code cleaning.
+
+
+## v0.1 (2010/11/26) ##
+
+ - initial release, support of basic features.
View
115 README.markdown
@@ -3,15 +3,95 @@
Custom event/messaging system for JavaScript inspired by [AS3-Signals](https://github.com/robertpenner/as3-signals).
+
## Introduction ##
A Signal is similar to a EventTarget/EventDispatcher or a pub/sub system, the main difference is that each event kind has it's own controller and doesn't rely on strings to call proper callbacks.
Another advantage is that you can pass arbitrary parameters to callbacks and it also have some convenience methods that aren't present on other *observer pattern* implementations.
+This implementation is heavily inspired by Robert Penner's AS3-Signals but it has a different set of features/classes, the main target of this implementation is *custom events* and not replacing *native DOM events*.
+
+
+## Comparisson between *observers* ##
+
+The comparisson below is just about the basic features of subscribing to an event type, dispatching and removing an event listener. It isn't based on available features but on differences between each concept and pros and cons of using each one.
+
+### Event Target / Event Dispatcher ###
+
+ - Each Object that dispatches custom events needs to inherit from an EventTarget/EventDispatcher object or implement the proper interface.
+ - Use strings to identify the event type.
+ - DOM2/DOM3 Events are based on this principle.
+
+#### Code sample ####
+
+ myObject.addEventListener('myCustomEventTypeString', handler);
+ myObject.dispatch(new Event('myCustomEventTypeString'));
+ myObject.removeEventListener('myCustomEventTypeString', handler);
+
+#### Pros ####
+
+ - You have total control of the *target object* and make sure you are listening only to the events dispatched by the specific target.
+ - Can dispatch arbitrary event types without modifying the target object.
+
+#### Cons ####
+
+ - Favors inheritance over composition.
+ - Uses strings to identify event types, prone to typo errors and autocomplete doens't work properly.
+
+
+### Publish / Subscribe ###
+
+ - Uses a single object to *broadcast messages* to multiple *subscribers*.
+ - Use strings to identify the event type.
+
+#### Code sample ####
+
+ globalBroadcaster.subscribe('myCustomEventTypeString', handler);
+ globalBroadcaster.publish('myCustomEventTypeString', paramsArray);
+ globalBroadcaster.unsubscribe('myCustomEventTypeString', handler);
+
+#### Pros ####
+
+ - Any object can publish/subscribe to any event type.
+ - Light-weigth.
+ - Easy to use.
+
+#### Cons ####
+
+ - Any object can publish/subscribe to any event type. *(yeap, it's pro and con at the same time)*
+ - Uses strings to identify event types (error prone and no auto-complete).
+
+
+### Signals ###
+
+ - Each event type has it's own controller.
+ - Doesn't rely on strings for event types.
+
+#### Code sample ####
+
+ myObject.myCustomEventType.add(handler);
+ myObject.myCustomEventType.dispatch(param1, param2, ...);
+ myObject.myCustomEventType.remove(handler);
+
+#### Pros ####
+
+ - Doesn't rely on strings.
+ - auto-complete works properly.
+ - easy do identify wich *signals* the object dispatch.
+ - less error prone.
+ - Granular control over each listener and event type (easily).
+
+#### Cons ####
+
+ - Can't dispatch arbitrary events. *(which is also a pro in most cases)*
+ - Each event-type is an object member. *(which is also a pro in most cases)*
+
## Examples ##
+### Setup code (required for all examples) ###
+
//store local reference for brevity
var Signal = signals.Signal;
@@ -20,7 +100,10 @@ Another advantage is that you can pass arbitrary parameters to callbacks and it
started : new Signal(),
stopped : new Signal()
};
-
+
+
+### Single Listener ###
+
function onStarted(param1, param2){
alert(param1 + param2);
}
@@ -33,7 +116,10 @@ Another advantage is that you can pass arbitrary parameters to callbacks and it
//remove a single listener
myObject.started.remove(onStarted);
-
+
+
+### Multiple Listeners ###
+
function onStopped(){
alert('stopped');
}
@@ -51,3 +137,28 @@ Another advantage is that you can pass arbitrary parameters to callbacks and it
//remove all listeners of the `stopped` signal
myObject.stopped.removeAll();
+
+
+### Stop Propagation (method 1) ###
+
+ myObject.started.add(function(){
+ myObject.started.stopPropagation();
+ });
+ myObject.started.add(function(){
+ //won't be called since first listener stops propagation
+ alert('second listener');
+ });
+ myObject.started.dispatch();
+
+
+### Stop Propagation (method 2) ###
+
+ myObject.started.add(function(){
+ return false; //stop propagation
+ });
+ myObject.started.add(function(){
+ //won't be called since first listener stops propagation
+ alert('second listener');
+ });
+ myObject.started.dispatch();
+
View
4 dev/build/build.number
@@ -1,3 +1,3 @@
#Build Number for ANT. Do not edit!
-#Fri Nov 26 03:47:05 EST 2010
-build.number=30
+#Fri Nov 26 19:15:08 EST 2010
+build.number=47
View
2  dev/build/build.properties
@@ -4,6 +4,6 @@ build.dir = ${dev.dir}/build
dist.dir = dist
yuicompressor.jar = ${build.dir}/yuicompressor/yuicompressor-2.4.2.jar
product.name = js-signals
-version.number = 0.0.1
+version.number = 0.2
dist.name = ${product.name}.js
dist.min.name = ${product.name}.min.js
View
52 dev/src/Signal.js
@@ -16,21 +16,23 @@
signals.Signal.prototype = {
- _registerListener : function(listener, isOnce, scope){
- var prevBinding,
- prevIndex = this._indexOfListener(listener),
+ _shouldPropagate : true,
+
+ _isPaused : false,
+
+ _registerListener : function _registerListener(listener, isOnce, scope){
+ var prevIndex = this._indexOfListener(listener),
binding;
- if(prevIndex != -1){ //avoid creating a new Binding if already added to list
- prevBinding = this._bindings[prevIndex];
+ if(prevIndex !== -1){ //avoid creating a new Binding for same listener if already added to list
+ binding = this._bindings[prevIndex];
- if(prevBinding.isOnce() && !isOnce){
+ if(binding.isOnce() && !isOnce){
throw new Error('You cannot addOnce() then add() the same listener without removing the relationship first.');
- }else if(!prevBinding.isOnce() && isOnce){
+ }else if(!binding.isOnce() && isOnce){
throw new Error('You cannot add() then addOnce() the same listener without removing the relationship first.');
}
- binding = prevBinding;
} else {
binding = new signals.SignalBinding(listener, isOnce, scope, this);
this._bindings.push(binding);
@@ -47,17 +49,17 @@
return -1;
},
- add : function(listener, scope){
+ add : function add(listener, scope){
return this._registerListener(listener, false, scope);
},
- addOnce : function(listener, scope){
+ addOnce : function addOnce(listener, scope){
return this._registerListener(listener, true, scope);
},
remove : function remove(listener){
var i = this._indexOfListener(listener);
- if(i != -1){
+ if(i !== -1){
this._bindings.splice(i, 1);
}
return listener;
@@ -71,19 +73,39 @@
return this._bindings.length;
},
- dispatch : function(){
+ pause : function pause(){
+ this._isPaused = true;
+ },
+
+ resume : function resume(){
+ this._isPaused = false;
+ },
+
+ isPaused : function isPaused(){
+ return this._isPaused;
+ },
+
+ stopPropagation : function stopPropagation(){
+ this._shouldPropagate = false;
+ },
+
+ dispatch : function dispatch(params){
+ if(this._isPaused) return;
+
var paramsArr = Array.prototype.slice.call(arguments),
- i = 0,
bindings = this._bindings.slice(), //clone array in case add/remove items during dispatch
+ i = 0,
cur;
+
while(cur = bindings[i++]){
- cur.execute(paramsArr);
+ if(cur.execute(paramsArr) === false || !this._shouldPropagate) break; //execute all callbacks until end of the list or until a callback returns `false`
}
+ this._shouldPropagate = true;
},
toString : function toString(){
- return '[Signal numListeners: '+ this.getNumListeners() +']';
+ return '[Signal isPaused: '+ this._isPaused +' numListeners: '+ this.getNumListeners() +']';
}
};
View
4 dev/src/SignalBinding.js
@@ -20,7 +20,7 @@
execute : function execute(paramsArr){
if(! this._isPaused){
if(this._isOnce) this._signal.remove(this.listener);
- this.listener.apply(this.listenerScope, paramsArr);
+ return this.listener.apply(this.listenerScope, paramsArr);
}
},
@@ -33,7 +33,7 @@
},
isPaused : function isPaused(){
- return this._paused;
+ return this._isPaused;
},
isOnce : function isOnce(){
View
172 dev/tests/src/01.js
@@ -649,6 +649,178 @@ YUI().use('node', 'console', 'test', function (Y){
Y.Assert.areSame(35, scope.n);
},
+ //-------------------- Stop Propagation -----------------------------//
+
+ testStopPropagation : function(){
+ var s = this.signal;
+
+ var n = 0;
+ var l1 = function(){n++};
+ var l2 = function(){return false};
+ var l3 = function(){n++};
+
+ s.add(l1);
+ s.add(l2);
+ s.add(l3);
+ s.dispatch();
+
+ Y.Assert.areSame(1, n);
+ },
+
+ testStopPropagation2 : function(){
+ var s = this.signal;
+
+ var n = 0;
+ var l1 = function(){n++};
+ var l2 = function(){s.stopPropagation()};
+ var l3 = function(){n++};
+
+ s.add(l1);
+ s.add(l2);
+ s.add(l3);
+ s.dispatch();
+
+ Y.Assert.areSame(1, n);
+ },
+
+ //--------------------------- Pause/Resume -------------------------------//
+
+ testPauseSignal : function(){
+ var s = this.signal;
+
+ var n = 0;
+ var l1 = function(){n++};
+ var l2 = function(){n++};
+ var l3 = function(){n++};
+
+ s.add(l1);
+ s.add(l2);
+ s.add(l3);
+
+ Y.Assert.areSame(false, s.isPaused());
+ s.dispatch();
+
+ s.pause();
+ Y.Assert.areSame(true, s.isPaused());
+ s.dispatch();
+
+ Y.Assert.areSame(3, n);
+ },
+
+ testResumeSignal : function(){
+ var s = this.signal;
+
+ var n = 0;
+ var l1 = function(){n++};
+ var l2 = function(){n++};
+ var l3 = function(){n++};
+
+ s.add(l1);
+ s.add(l2);
+ s.add(l3);
+
+ Y.Assert.areSame(false, s.isPaused());
+ s.dispatch();
+
+ s.pause();
+ Y.Assert.areSame(true, s.isPaused());
+ s.dispatch();
+
+ s.resume();
+ Y.Assert.areSame(false, s.isPaused());
+ s.dispatch();
+
+ Y.Assert.areSame(6, n);
+ },
+
+ testPauseBinding : function(){
+ var s = this.signal;
+
+ var n = 0;
+ var l1 = function(){n++};
+ var l2 = function(){n++};
+ var l3 = function(){n++};
+
+ var b1 = s.add(l1);
+ var b2 = s.add(l2);
+ var b3 = s.add(l3);
+
+ Y.Assert.areSame(false, s.isPaused());
+ Y.Assert.areSame(false, b2.isPaused());
+ s.dispatch();
+
+ b2.pause();
+ Y.Assert.areSame(false, s.isPaused());
+ Y.Assert.areSame(true, b2.isPaused());
+ s.dispatch();
+
+ b2.resume();
+ Y.Assert.areSame(false, s.isPaused());
+ Y.Assert.areSame(false, b2.isPaused());
+ s.dispatch();
+
+ Y.Assert.areSame(8, n);
+ },
+
+ //------------------------ Bindings ----------------------------------//
+
+ testBindingsIsOnce : function(){
+ var s = this.signal;
+ var b1 = s.addOnce(function(){});
+ Y.Assert.areSame(1, s.getNumListeners());
+ Y.Assert.areSame(true, b1.isOnce());
+ },
+
+ testBindingsIsOnce2 : function(){
+ var s = this.signal;
+ var b1 = s.addOnce(function(){});
+ var b2 = s.addOnce(function(){});
+ Y.Assert.areSame(2, s.getNumListeners());
+ Y.Assert.areSame(true, b1.isOnce());
+ Y.Assert.areSame(true, b2.isOnce());
+ Y.Assert.areNotSame(b1, b2);
+ },
+
+ testBindingsIsOnce3 : function(){
+ var s = this.signal;
+ var l = function(){};
+ var b1 = s.addOnce(l);
+ var b2 = s.addOnce(l);
+ Y.Assert.areSame(1, s.getNumListeners());
+ Y.Assert.areSame(true, b1.isOnce());
+ Y.Assert.areSame(true, b2.isOnce());
+ Y.Assert.areSame(b1, b2);
+ },
+
+ testBindingsIsNotOnce : function(){
+ var s = this.signal;
+ var b1 = s.add(function(){});
+ Y.Assert.areSame(1, s.getNumListeners());
+ Y.Assert.areSame(false, b1.isOnce());
+ },
+
+ testBindingsIsNotOnce2 : function(){
+ var s = this.signal;
+ var b1 = s.add(function(){});
+ var b2 = s.add(function(){});
+ Y.Assert.areSame(2, s.getNumListeners());
+ Y.Assert.areSame(false, b1.isOnce());
+ Y.Assert.areSame(false, b2.isOnce());
+ Y.Assert.areNotSame(b1, b2);
+ },
+
+ testBindingsIsNotOnce3 : function(){
+ var s = this.signal;
+ var l = function(){};
+ var b1 = s.add(l);
+ var b2 = s.add(l);
+ Y.Assert.areSame(1, s.getNumListeners());
+ Y.Assert.areSame(false, b1.isOnce());
+ Y.Assert.areSame(false, b2.isOnce());
+ Y.Assert.areSame(b1, b2);
+ },
+
+
//------------------------ Remove ----------------------------------//
testRemoveSingle : function(){
View
60 dist/js-signals.js
@@ -2,8 +2,8 @@
* JS Signals <https://github.com/millermedeiros/js-signals>
* Released under the MIT license (http://www.opensource.org/licenses/mit-license.php)
* @author Miller Medeiros <http://millermedeiros.com>
- * @version 0.0.1
- * @build 29 11/26/2010 03:47 AM
+ * @version 0.2
+ * @build 46 11/26/2010 07:15 PM
*/
(function(){
@@ -29,21 +29,23 @@
signals.Signal.prototype = {
- _registerListener : function(listener, isOnce, scope){
- var prevBinding,
- prevIndex = this._indexOfListener(listener),
+ _shouldPropagate : true,
+
+ _isPaused : false,
+
+ _registerListener : function _registerListener(listener, isOnce, scope){
+ var prevIndex = this._indexOfListener(listener),
binding;
- if(prevIndex != -1){ //avoid creating a new Binding if already added to list
- prevBinding = this._bindings[prevIndex];
+ if(prevIndex !== -1){ //avoid creating a new Binding for same listener if already added to list
+ binding = this._bindings[prevIndex];
- if(prevBinding.isOnce() && !isOnce){
+ if(binding.isOnce() && !isOnce){
throw new Error('You cannot addOnce() then add() the same listener without removing the relationship first.');
- }else if(!prevBinding.isOnce() && isOnce){
+ }else if(!binding.isOnce() && isOnce){
throw new Error('You cannot add() then addOnce() the same listener without removing the relationship first.');
}
- binding = prevBinding;
} else {
binding = new signals.SignalBinding(listener, isOnce, scope, this);
this._bindings.push(binding);
@@ -60,17 +62,17 @@
return -1;
},
- add : function(listener, scope){
+ add : function add(listener, scope){
return this._registerListener(listener, false, scope);
},
- addOnce : function(listener, scope){
+ addOnce : function addOnce(listener, scope){
return this._registerListener(listener, true, scope);
},
remove : function remove(listener){
var i = this._indexOfListener(listener);
- if(i != -1){
+ if(i !== -1){
this._bindings.splice(i, 1);
}
return listener;
@@ -84,19 +86,39 @@
return this._bindings.length;
},
- dispatch : function(){
+ pause : function pause(){
+ this._isPaused = true;
+ },
+
+ resume : function resume(){
+ this._isPaused = false;
+ },
+
+ isPaused : function isPaused(){
+ return this._isPaused;
+ },
+
+ stopPropagation : function stopPropagation(){
+ this._shouldPropagate = false;
+ },
+
+ dispatch : function dispatch(params){
+ if(this._isPaused) return;
+
var paramsArr = Array.prototype.slice.call(arguments),
- i = 0,
bindings = this._bindings.slice(), //clone array in case add/remove items during dispatch
+ i = 0,
cur;
+
while(cur = bindings[i++]){
- cur.execute(paramsArr);
+ if(cur.execute(paramsArr) === false || !this._shouldPropagate) break; //execute all callbacks until end of the list or until a callback returns `false`
}
+ this._shouldPropagate = true;
},
toString : function toString(){
- return '[Signal numListeners: '+ this.getNumListeners() +']';
+ return '[Signal isPaused: '+ this._isPaused +' numListeners: '+ this.getNumListeners() +']';
}
};
@@ -122,7 +144,7 @@
execute : function execute(paramsArr){
if(! this._isPaused){
if(this._isOnce) this._signal.remove(this.listener);
- this.listener.apply(this.listenerScope, paramsArr);
+ return this.listener.apply(this.listenerScope, paramsArr);
}
},
@@ -135,7 +157,7 @@
},
isPaused : function isPaused(){
- return this._paused;
+ return this._isPaused;
},
isOnce : function isOnce(){
View
6 dist/js-signals.min.js
@@ -2,7 +2,7 @@
* JS Signals <https://github.com/millermedeiros/js-signals>
* Released under the MIT license (http://www.opensource.org/licenses/mit-license.php)
* @author Miller Medeiros <http://millermedeiros.com>
- * @version 0.0.1
- * @build 29 11/26/2010 03:47 AM
+ * @version 0.2
+ * @build 46 11/26/2010 07:15 PM
*/
-(function(){var d=window.signals={};d.Signal=function(){this._bindings=[]};d.Signal.prototype={_registerListener:function(p,o,n){var q,m=this._indexOfListener(p),r;if(m!=-1){q=this._bindings[m];if(q.isOnce()&&!o){throw new Error("You cannot addOnce() then add() the same listener without removing the relationship first.")}else{if(!q.isOnce()&&o){throw new Error("You cannot add() then addOnce() the same listener without removing the relationship first.")}}r=q}else{r=new d.SignalBinding(p,o,n,this);this._bindings.push(r)}return r},_indexOfListener:function a(m){var o=this._bindings.length;while(o--){if(this._bindings[o].listener===m){return o}}return -1},add:function(n,m){return this._registerListener(n,false,m)},addOnce:function(n,m){return this._registerListener(n,true,m)},remove:function e(n){var m=this._indexOfListener(n);if(m!=-1){this._bindings.splice(m,1)}return n},removeAll:function g(){this._bindings.length=0},getNumListeners:function j(){return this._bindings.length},dispatch:function(){var m=Array.prototype.slice.call(arguments),n=0,p=this._bindings.slice(),o;while(o=p[n++]){o.execute(m)}},toString:function b(){return"[Signal numListeners: "+this.getNumListeners()+"]"}};d.SignalBinding=function k(p,n,m,o){this.listener=p;this._isOnce=n;this._signal=o;this.listenerScope=m};d.SignalBinding.prototype={_isPaused:false,execute:function i(m){if(!this._isPaused){if(this._isOnce){this._signal.remove(this.listener)}this.listener.apply(this.listenerScope,m)}},pause:function l(){this._isPaused=true},resume:function c(){this._isPaused=false},isPaused:function f(){return this._paused},isOnce:function h(){return this._isOnce},toString:function b(){return"[SignalBinding listener: "+this.listener+", isOnce: "+this._isOnce+", isPaused: "+this._isPaused+", listenerScope: "+this.listenerScope+"]"}}}());
+(function(){var e=window.signals={};e.Signal=function(){this._bindings=[]};e.Signal.prototype={_shouldPropagate:true,_isPaused:false,_registerListener:function f(u,t,s){var r=this._indexOfListener(u),v;if(r!==-1){v=this._bindings[r];if(v.isOnce()&&!t){throw new Error("You cannot addOnce() then add() the same listener without removing the relationship first.")}else{if(!v.isOnce()&&t){throw new Error("You cannot add() then addOnce() the same listener without removing the relationship first.")}}}else{v=new e.SignalBinding(u,t,s,this);this._bindings.push(v)}return v},_indexOfListener:function a(r){var s=this._bindings.length;while(s--){if(this._bindings[s].listener===r){return s}}return -1},add:function p(s,r){return this._registerListener(s,false,r)},addOnce:function b(s,r){return this._registerListener(s,true,r)},remove:function g(s){var r=this._indexOfListener(s);if(r!==-1){this._bindings.splice(r,1)}return s},removeAll:function j(){this._bindings.length=0},getNumListeners:function n(){return this._bindings.length},pause:function q(){this._isPaused=true},resume:function d(){this._isPaused=false},isPaused:function h(){return this._isPaused},stopPropagation:function m(){this._shouldPropagate=false},dispatch:function i(u){if(this._isPaused){return}var r=Array.prototype.slice.call(arguments),v=this._bindings.slice(),s=0,t;while(t=v[s++]){if(t.execute(r)===false||!this._shouldPropagate){break}}this._shouldPropagate=true},toString:function c(){return"[Signal isPaused: "+this._isPaused+" numListeners: "+this.getNumListeners()+"]"}};e.SignalBinding=function o(u,s,r,t){this.listener=u;this._isOnce=s;this._signal=t;this.listenerScope=r};e.SignalBinding.prototype={_isPaused:false,execute:function l(r){if(!this._isPaused){if(this._isOnce){this._signal.remove(this.listener)}return this.listener.apply(this.listenerScope,r)}},pause:function q(){this._isPaused=true},resume:function d(){this._isPaused=false},isPaused:function h(){return this._isPaused},isOnce:function k(){return this._isOnce},toString:function c(){return"[SignalBinding listener: "+this.listener+", isOnce: "+this._isOnce+", isPaused: "+this._isPaused+", listenerScope: "+this.listenerScope+"]"}}}());
Please sign in to comment.
Something went wrong with that request. Please try again.