Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active September 20, 2017 18:25
Show Gist options
  • Save serradura/81601ca614533099d395660e94a84f81 to your computer and use it in GitHub Desktop.
Save serradura/81601ca614533099d395660e94a84f81 to your computer and use it in GitHub Desktop.
jQuery Signal + EventBus
const forEach = applier => ($model, args) => args[1].forEach( fn => applier( $model, [ args[0], fn ] ) );
const iterateIf = (isAnArrayHandler, args) => isAnArrayHandler && $.isArray( args[1] );
const fetchArgsWith = handler => args => handler ? handler.apply( null, args ) : args;
const featureApplier = (feature, fetch) => ($model, args) => feature.apply( $model, fetch( args ) );
const featureMounter = (feature, argsHandler, isAnArrayHandler) => {
const applyFeature = featureApplier( feature, fetchArgsWith( argsHandler ) );
return $model => (function() {
( iterateIf(isAnArrayHandler, arguments) ? forEach(applyFeature) : applyFeature )( $model, arguments );
return this
});
};
const ArgumentsHandler = {
toEmit: (...args) => [ args[0], args.slice( 1 ) ]
, toUnsubscribe: (...args) => args[1] ? [ args[0], '', args[1] ] : args
};
const Features = ((acceptArray, { toSubscribe, toUnsubscribe, toEmit }) => ({
on: featureMounter( $.prototype.on, toSubscribe, acceptArray )
, one: featureMounter( $.prototype.one, toSubscribe, acceptArray )
, off: featureMounter( $.prototype.off, toUnsubscribe, acceptArray )
, emit: featureMounter( $.prototype.trigger, toEmit, !acceptArray )
}))( true, ArgumentsHandler );
const composeEventBus = (list, ref = {}) => list.reduce((object, feature) => $.extend({}, object, feature), ref)
const mapFeature = $model => (feature, name) => ({ [name]: feature( $model ) });
const mapFeatures = $model => [ $model, composeEventBus( $.map(Features, mapFeature( $model )) ) ];
const mapPlugin = ($model, eventBus) => plugin => typeof plugin === 'function' ? plugin( $model, eventBus ) : plugin;
const mapPlugins = (plugins, [ $model, eventBus ]) => (
!$.isArray( plugins ) ? eventBus : composeEventBus( $.map(plugins, mapPlugin($model, eventBus)), eventBus )
);
const createEventBus = (plugins = []) => mapPlugins( plugins, mapFeatures($({})) );
// Usage
// Create basic instances
const EventBus = createEventBus();
EventBus
.on('event.foo', (_, arg) => console.log('FOO', arg))
.on('event.bar', (_, arg) => console.log('BAR', arg))
.one('test-one', (_, arg) => console.log('ONE ONE', arg))
.one('test-one', (_, arg) => console.log('ONE TWO', arg))
EventBus.emit('event.foo') // log: FOO
EventBus.emit('event.bar') // log: BAR
EventBus.emit('event') // log: FOO; BAR
EventBus.emit('event.foo', 1) // log: FOO 1
EventBus.emit('event.bar', 2) // log: BAR 2
EventBus.emit('event', 3) // log: FOO 3; BAR 3
EventBus.emit('test-one') // unique log: ONE ONE; ONE TWO
EventBus.emit('test-one') // Nothing
EventBus.off('event.foo') // Unsubscribe 'event.foo'
EventBus.emit('event.foo') // Nothing
EventBus.emit('event') // log: BAR
EventBus.off() // Unsubscribe All
EventBus.emit('event') // Nothing
const fozHandler1 = event => console.log( `type: ${ event.type }, namespace: ${ event.handleObj.namespace }` );
const fozHandler2 = event => console.log( `type: ${ event.type }, namespace: ${ event.handleObj.namespace }` );
EventBus
.on('foz.handler1', fozHandler1)
.on('foz.handler2', fozHandler2)
EventBus.emit('foz') // log: type: foz, namespace: handler1; type: foz, namespace: handler2
EventBus.off('foz', fozHandler1) // Unsubscribe fozHandler1
EventBus.emit('foz') // log: type: foz, namespace: handler2
EventBus.off('foz', fozHandler2) // Unsubscribe fozHandler2
EventBus.emit('foz') // Nothing
const bazHandler1 = event => console.log( 'bazHandler1', event.type )
const bazHandler2 = event => console.log( 'bazHandler2', event.type )
EventBus.on('baz', [ bazHandler1, bazHandler2 ] )
EventBus.emit('baz') // log: bazHandler1 baz; bazHandler2 baz
EventBus.off('baz', [ bazHandler1, bazHandler2 ] )
EventBus.emit('baz') // Nothing
EventBus.on('baz', [ bazHandler1, bazHandler2 ] )
EventBus.off('baz', [ bazHandler2 ] )
EventBus.emit('baz') // log: bazHandler1 baz
EventBus.off('baz', [ bazHandler1 ] )
EventBus.emit('baz') // Nothing
EventBus.one('baz', [ bazHandler1, bazHandler2 ] )
EventBus.emit('baz') // log: bazHandler1 foz; bazHandler2 baz
EventBus.emit('baz') // Nothing
const oddPlugin = (_$model, eventBus) => ({
odd(...args) { eventBus.off( args[0] ); eventBus.one.apply( eventBus, args ); return this }
});
const emitterOfPlugin = (_$model, eventBus) => ({
emitterOf: (event, ...defaultPayload) => (...localPayload) => {
const payload = localPayload.length ? localPayload : defaultPayload;
eventBus.emit.apply(eventBus, [ event ].concat( payload ));
}
});
const strictUnsubscribePlugin = (_$model, eventBus) => ({
off(event, callback) {
if ( !event || typeof event !== 'string' ) throw TypeError('event to unsubscribe must be a valid string');
const callbacks = $.map( [ callback ], i => i ).filter( i => i );
if ( !callbacks.length ) { eventBus.off( event ); return this }
if ( callbacks.every( fn => typeof fn !== 'function' ) ) {
throw TypeError('handler to unsubscribe must be a function');
}
eventBus.off.apply(eventBus, [ event ].concat( callbacks )); return this;
}
});
const Plugins = [
oddPlugin
, emitterOfPlugin
, strictUnsubscribePlugin
];
const EventBus2 = createEventBus( Plugins );
EventBus2
.odd('test-odd', (_, arg) => console.log('ODD ONE', arg))
.odd('test-odd', (_, arg) => console.log('ODD TWO', arg))
.odd('test.odd.namespace', (_, arg) => console.log('ODD ONE W NAMESPACE', arg))
.odd('test.odd.namespace', (_, arg) => console.log('ODD TWO W NAMESPACE', arg));
EventBus2.emit('test-odd') // unique log: ODD TWO
EventBus2.emit('test-odd') // Nothing
EventBus2.emit('test.namespace') // unique log: ODD TWO W NAMESPACE
EventBus2.emit('test.namespace') // Nothing
EventBus2
.on('event.foo', (_, arg) => console.log('FOO', arg))
.on('event.bar', (_, arg) => console.log('BAR', arg));
const emitEvent = EventBus2.emitterOf('event', 'default-payload');
emitEvent() // log: FOO default-payload; BAR default-payload
emitEvent(4) // log: FOO 4; BAR 4
EventBus2.off('event')
emitEvent() // Nothing
const foo = (_, arg) => console.log('FOO', arg);
const bar = (_, arg) => console.log('BAR', arg);
EventBus2
.on('event.foo', [ foo ])
.on('event.bar', [ bar ]);
EventBus2.off() // Uncaught TypeError: event to unsubscribe must be a valid string
EventBus2.off('') // Uncaught TypeError: event to unsubscribe must be a valid string
EventBus2.off('event.foo', 1) // Uncaught TypeError: handler to unsubscribe must be a function
EventBus2.off('event.foo') // Unsubscribe 'event.foo'
EventBus2.emit('event.foo') // Nothing
EventBus2.emit('event') // log: BAR
EventBus2.off('event.bar', [ bar ]) // Unsubscribe bar
EventBus2.emit('event') // Nothing
const slice = (args, i = 0) => Array.prototype.slice.call( args, i );
const createId = customId => (id => () => id)(customId ? customId : Math.random().toString( 16 ).slice( 2 ));
const featureHandler = eventBus => name => args => eventBus[name].apply( eventBus, args );
const signalArgsCreator = id => args => [ id() ].concat( slice(args) );
const listenerHandlers = funcs => funcs.map( fn => function() { fn.apply( null, slice(arguments, 1) ) } );
const listenerCreator = (unsubscribe, signalArgs) => subscribe => (function() {
const payload = signalArgs( [ listenerHandlers( $.map(slice( arguments ), fn => fn) ) ] );
subscribe( payload ); return function cancel() { unsubscribe( payload ) }
});
const createSignal = (EventBus => (function(customId) {
const id = createId( customId ), feature = featureHandler( EventBus() );
const signalArgs = signalArgsCreator( id ), unsubscribe = feature('off');
const listenAs = listenerCreator( unsubscribe, signalArgs );
return {
id
, once: listenAs( feature('one') )
, listen: listenAs( feature('on') )
, emit() { feature('emit')( signalArgs( arguments ) ) }
, cancel() { unsubscribe([ id() ]) }
}
}))(createEventBus)
function createSingularSignal(customId) {
const signal = createSignal(customId);
function listen() { signal.cancel(); return signal.once.apply( null, arguments ) };
return $.extend( {}, signal, { listen, once: listen } );
}
// Usage
greet = createSignal('greeter')
greet.id() // return 'greeter'
greet.listen(console.log)
greet.emit('Serradura') // log: Serradura
greet.cancel()
greet.emit('Rodrigo') // Nothing
cancelHello = greet.listen(name => console.log(`Olá ${name}`))
greet.emit('Bella') // log: Olá Bella
cancelHello() // Unsubscribe handler
greet.emit('Bella') // Nothing
cancelHellos = greet.listen([
name => console.log(`Olar ${name}`)
, name => console.log(`Hello ${name}`)
]); // Subscribe multiple handlers
greet.emit('Stela') // log: Olar Stela; Hello Stela
cancelHellos() // Unsubscribe handlers
greet.emit('Stela') // Nothing
sg = createSignal()
sg.id() // return a random (string) id
sg.once(() => console.log('begin'))
sg.once(console.log)
sg.once(() => console.log('end'))
sg.emit(1, 2, 3) // log: begin; 1,2,3; end
sg.emit(1, 2, 3) // Nothing
sg.once([ () => console.log('begin1'), console.log, () => console.log('end2') ])
sg.once([ () => console.log('begin1'), console.log, () => console.log('end2') ])
sg.emit(3, 2, 1) // log: begin1; 3,2,1; end1; begin2; 3,2,1; end2
sg.emit(3, 2, 1) // Nothing
const osg = createSingularSignal()
osg.id() // return a random (string) id
osg.listen(() => console.log('begin'))
osg.listen(console.log)
osg.listen(() => console.log('end'))
osg.emit(1) // log: end
osg.listen([ () => console.log('begin1'), console.log, () => console.log('end1') ])
osg.listen([ () => console.log('begin2'), console.log, () => console.log('end2') ])
osg.emit(2) // log: begin2; 2; end2
osg.emit(2) // Nothing
const oddSignal = createSingularSignal('odd')
oddSignal.id() // return 'odd'
// .once is an alias to .listen
oddSignal.once(() => console.log('begin'))
oddSignal.once(console.log)
oddSignal.once(() => console.log('end'))
oddSignal.emit(1) // log: end
oddSignal.emit(1) // Nothing
oddSignal.once([ () => console.log('begin1'), console.log, () => console.log('end1') ])
oddSignal.once([ () => console.log('begin2'), console.log, () => console.log('end2') ])
oddSignal.emit(2) // log: begin2; 2; end2
oddSignal.emit(2) // Nothing
cancelOdd1 = oddSignal.once([ () => console.log('begin3'), console.log, () => console.log('end3') ])
cancelOdd1() // Usubscribe handlers
oddSignal.emit(3) // Nothing
cancelOdd2 = oddSignal.once(() => console.log('end4'))
cancelOdd2() // Usubscribe handler
oddSignal.emit(4) // Nothing
oddSignal.once(() => console.log('end5'))
oddSignal.cancel() // Usubscribe handlers
oddSignal.emit(5) // Nothing
eb2 = createDestroyableEventBus()
.on('event.foo', (_, arg) => console.log('D: FOO', arg))
.on('event.bar', (_, arg) => console.log('D: BAR', arg))
.one('test-one', (_, arg) => console.log('D: ONE ONE', arg))
.one('test-one', (_, arg) => console.log('D: ONE TWO', arg))
.odd('test-odd', (_, arg) => console.log('D: ODD ONE', arg))
.odd('test-odd', (_, arg) => console.log('D: ODD TWO', arg));
eb2.emit('event.foo') // log: FOO
eb2.emit('event.bar') // log: BAR
eb2.emit('event') // log: FOO; BAR
eb2.emit('event.foo', 1) // log: FOO 1
eb2.emit('event.bar', 2) // log: BAR 2
eb2.emit('event', 3) // log: FOO 3; BAR 3
eb2.emit('test-one') // unique log: ONE ONE; ONE TWO
eb2.emit('test-odd') // unique log: ODD TWO
eb2.emit('test-one') // Nothing
eb2.emit('test-odd') // Nothing
eb2.off('') // Uncaught TypeError: event to unsubscribe must be a valid string
eb2.off('event.foo', 1) // Uncaught TypeError: handler to unsubscribe must be a function
eb2.off('event.foo') // Unsubscribe 'event.foo'
eb2.emit('event.foo') // Nothing
eb2.emit('event') // log: BAR
eb2.off() // Unsubscribe All
eb2.emit('event') // Nothing
const fozHandler1 = event => console.log( `type: ${ event.type }, namespace: ${ event.handleObj.namespace }` );
const fozHandler2 = event => console.log( `type: ${ event.type }, namespace: ${ event.handleObj.namespace }` );
eb2
.on('foz.handler1', fozHandler1)
.on('foz.handler2', fozHandler2)
eb2.emit('foz') // log: type: foz, namespace: handler1; type: foz, namespace: handler2
eb2.off('foz', fozHandler1) // Unsubscribe fozHandler1
eb2.emit('foz') // log: type: foz, namespace: handler2
eb2.off('foz', fozHandler2) // Unsubscribe fozHandler2
eb2.emit('foz') // Nothing
const bazHandler1 = event => console.log( 'bazHandler1', event.type )
const bazHandler2 = event => console.log( 'bazHandler2', event.type )
eb2.on('baz', [ bazHandler1, bazHandler2 ] )
eb2.emit('baz') // log: bazHandler1 baz; bazHandler2 baz
eb2.off('baz', [ bazHandler1, bazHandler2 ] )
eb2.emit('baz') // Nothing
eb2.on('baz', [ bazHandler1, bazHandler2 ] )
eb2.off('baz', [ bazHandler2 ] )
eb2.emit('baz') // log: bazHandler1 baz
eb2.off('baz', [ bazHandler1 ] )
eb2.emit('baz') // Nothing
eb2.one('baz', [ bazHandler1, bazHandler2 ] )
eb2.emit('baz') // log: bazHandler1 foz; bazHandler2 baz
eb2.emit('baz') // Nothing
eb2.destroy() // return true
eb2.destroy() // return false
eb2.on() // Uncaught TypeError: eb2.on is not a function
eb2.one() // Uncaught TypeError: eb2.one is not a function
eb2.odd() // Uncaught TypeError: eb2.odd is not a function
eb2.off() // Uncaught TypeError: eb2.off is not a function
eb2.emit() // Uncaught TypeError: eb2.emit is not a function
eb2.emitterOf('event'); // Uncaught TypeError: eb2.emitterOf is not a function
const mounter = ($model) => (function(feature, argsHandler, iterateOnArray = false) {
const handleArgs = args => argsHandler ? argsHandler.apply( null, args ) : args;
const applyFeature = args => feature.apply( $model, handleArgs( args ) );
return function() {
if ( iterateOnArray && $.isArray( arguments[1] ) ) {
const event = arguments[0];
arguments[1].forEach( fn => applyFeature( [ event, fn ] ) );
} else {
applyFeature( arguments );
}
return this
}
});
const argumentsHandler = {
toUnsubscribe(event, callback) {
if ( !event || typeof event !== 'string' ) throw TypeError('event to unsubscribe must be a valid string');
if ( !callback ) return [ event ];
if ( typeof callback !== 'function' ) throw TypeError('handler to unsubscribe must be a function');
return [ event, '', callback ]
},
toEmit() { return [ arguments[0], Array.prototype.slice.call( arguments, 1 ) ] }
};
const baseCreator = features => $model => features( mounter( $model ) );
const createBase = baseCreator(mount => ({
on: mount($.prototype.on, argumentsHandler.native, true),
one: mount($.prototype.one, argumentsHandler.native, true),
off: mount($.prototype.off, argumentsHandler.toUnsubscribe, true),
emit: mount($.prototype.trigger, argumentsHandler.toEmit),
odd() { this.off( arguments[0] ); return this.one.apply( this, arguments ) },
}));
export const createEventBus = () => (
$.extend( createBase( $({}) ), { emitterOf } )
);
export const createDestroyableEventBus = () => {
let $model = $({}), eventBus = createBase( $model );
let unsubscribe = eventBus.off;
const off = function() {
if ( arguments.length ) return unsubscribe.apply( this, arguments );
$model.off();
return this;
}
const destroy = function() {
if( !this.off ) { return false; }
this.off(); unsubscribe = null; eventBus = null; $model = null;
for( const prop in this ) { if ( prop !== 'destroy' ) delete this[prop] };
return true;
}
return $.extend( eventBus, { off, destroy } );
}
const EventBus = createEventBus();
export default EventBus;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment