Last active
September 20, 2017 18:25
-
-
Save serradura/81601ca614533099d395660e94a84f81 to your computer and use it in GitHub Desktop.
jQuery Signal + EventBus
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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