/* js-events-demo-01.js Simple JavaScript events system (with handler filtering on data objects) Author: Pat Long Date: 2018/01/23 License: CC-BY-SA-4.0 */ // Events manager: var Events = { callback: {}, on: function(type, filter, callback, id) { if (type && typeof callback == 'function') { if (!this.callback[type]) { this.callback[type] = {}; } if (!id) { // generate unique id id = this.Utils.ID.getUnique(type, this.callback[type]); } if (id) { if (!this.callback[type][id]) { // create new entry if id doesn't exist this.callback[type][id] = { id: id, filter: filter, callback: callback }; } // return entry at id return this.callback[type][id]; } } return null; }, off: function(type, id) { if (type && id && this.callback[type] && this.callback[type][id]) { var handler = this.callback[type][id]; // cache handler to return later this.callback[type][id] = undefined; // detach handler from list delete this.callback[type][id]; // remove id entry from list var ids = Object.keys(this.callback[type]); if (!ids || !ids.length) { // cleanup empty callback[type] this.callback[type] = undefined; delete this.callback[type]; } // return the lifted entry return handler; } return null; }, emit: function(type, args, item) { if (type && this.callback[type]) { for (var id in this.callback[type]) { var handler = this.callback[type][id]; // verify the handler has a callback and that the item passes any filter the handler has if (typeof handler.callback == 'function' && (!item || !handler.filter || (item && handler.filter && this.Utils.Item.passesFilter(item, handler.filter)))) { // run the callback for the item handler.callback(args, item); } } } }, Utils: { ID: { getUnique(baseID, inList, max=-1) { var tryCount = 0; var tryID = baseID; while (inList[tryID] && (max < 0 || tryCount < max)) { tryID = baseID+'-'+(++tryCount); } if (max >= 0 && inList[tryID]) { return undefined; } return tryID; } }, Item: { passesFilter: function(item, filter) { if (!filter) { // no filters return true; } if (item) { // basic filter is true until proven false var result = true; for (var prop in filter) { // handle different cases of filter vs value to prove false if (Array.isArray(filter[prop]) && !Array.isArray(item[prop])) { // array filter value vs non-array data value if (filter[prop].indexOf(item[prop]) == -1) { // array does not contain data value result = false; } } else if (filter[prop].constructor == RegExp && typeof item[prop] == 'string') { // regular expression filter vs string data value if (!filter[prop].test(item[prop])) { result = false; } } else if (typeof filter[prop] == 'function') { // function filter vs any data value if (!filter[prop](item[prop])) { result = false; } } else if (item[prop] != filter[prop]) { // simple non-equality check as last resort result = false; } } return result; } return false; } } } }; // Demonstration: // some arbitrary event handlers var handlers = { demoA: function(args, data) { console.log('[demo handler A] ( '+JSON.stringify(args)+' ) ON => '+JSON.stringify(data)); }, demoB: function(args, data) { console.log('[demo handler B] ( '+JSON.stringify(args)+' ) ON => '+JSON.stringify(data)); } }; // register handler with no (null) filter var handlerA = Events.on('demo', null, handlers.demoA); // register handler that runs for data items with property foo == 'bar' var handlerB = Events.on('demo', {foo: 'bar'}, handlers.demoB); // notice we track the return values - this is so they can be managed (dropped, inspected, altered, etc) // emit some "demo" type events console.log('Emitting first "demo" event...'); Events.emit('demo', {'message': 'Hello world!'}, {id: 42, foo: 'buz'}); // foo != 'bar' (only handler A should run) console.log('Emitting second "demo" event'); Events.emit('demo', {'message': 'Hello again world!'}, {id: 42, foo: 'bar'}); // foo == 'bar' (both A and B should run) // drop handlerB console.log('Dropping "demo" handler ['+handlerB.id+']'); var handler = Events.off('demo', handlerB.id); console.log('Dropped "demo" handler ['+handler.id+']'); // and try one more "demo" type event console.log('Emitting third "demo" event'); Events.emit('demo', {'message': 'Hello Mars!'}, {id: 42, foo: 'bar'}); // foo == 'bar' (but B was dropped!) // finished for now console.log('Demo complete.');