Pub/sub pattern
If you’re not feeling like figure things out yourself use my framework agnostic broadcaster package.
npm install broadcaster/foundit
…or use one of these…
/*
* Broadcaster © 2008 Tore Darell
*
*
*
* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar
* 14 rue de Plaisance, 75014 Paris, France
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT THE FUCK YOU WANT TO.
*
*
*
* Implements a centralised broadcast/listen pattern. A broadcaster
* is simple and dumb and only knows how to broadcast a message to
* listeners for that particular message.
*
* var b = new Broadcaster();
*
* b.broadcast('a message'); //Nothing happens
* b.listen('a message', function(){ alert('a message was received'); });
* b.broadcast('a message'); //alerts
*
* b.listen('some state has changed', function(s){ alert('new state is now: '+s); });
* b.broadcast('some state has changed', 'new state'); // alerts "new state is now: new state'"
*
* var collector = [];
* b.listen('new item', function(i){ this.push(i); }, collector);
* b.broadcast('new item', 'cat'); // ['cat']
* b.broadcast('new item', 'dog'); // ['cat', 'dog']
*
* The special message '*' is used as a global listener which will receive all messages:
*
* b.listen('*', function(message){ alert(message + ' was received'); })
* b.fire('foo'); //Alerts "foo was received"
*
* A broadcaster can easily be used to make an object observable:
*
* function ElementObserver(element, interval){
* this.element = element;
* this.broadcaster = new Broadcaster(); //The magic line
* var that = this, oldValue = element.innerHTML;
* this._interval = setInterval(function(){
* var newValue = element.innerHTML;
* if (newValue !== oldValue) {
* that.broadcaster.broadcast('value changed', newValue, oldValue);
* }
* oldValue = newValue;
* }, interval || 500);
* };
*
* var observers = ['some_id', 'some_other_id'].map(function(id){ return new ElementObserver($(id)); });
* observers.each(function(o){
* o.broadcaster.listen('value changed', function(ov, nv){
* alert('Value in '+o.element+' changed from '+ov+' to '+nv);
* });
* });
*
*/
Broadcaster = function () {
this.listeners = {}
}
;(function (p) {
p.defaultScope = this // window/global
//Attach a listener for a particular message with a callback function and
//an optional scope in which it will run. Returns the callback function.
p.listen = function (message, callback, scope) {
if (!this.listeners[message]) {
this.listeners[message] = []
}
this.listeners[message].push({ callback: callback, scope: scope })
return callback
}
p.subscribe = p.listen
//Remove a listener which matches a particular message and callback function
p.stopListening = function (message, callback) {
var l = this.listeners,
m = message,
c = callback,
i
if (l[m]) {
for (i = 0; i < l[m].length; i++) {
if (l[m][i].callback == c) {
l[m].splice(i, 1)
}
}
}
}
p.unsubscribe = p.stopListening
//Broadcast a message. Any additional arguments are proxied to
//the listener's callback function. Listeners for the special
//message '*' will receive all messages that are fired
p.broadcast = function (message) {
var l = this.listeners[message],
g = this.listeners['*'],
args,
i
if (l || g) {
args = Array.prototype.slice.call(arguments, 1)
if (l) {
//Specific listeners
for (i = 0; i < l.length; i++) {
l[i].callback.apply(l[i].scope || this.defaultScope, args)
}
}
if (g) {
//Global listeners
for (i = 0; i < g.length; i++) {
//Globals also receive message name
g[i].callback.apply(g[i].scope || this.defaultScope, arguments)
}
}
}
}
p.fire = p.broadcast
p.send = p.broadcast
})(Broadcaster.prototype)
/**
* The Publisher/Subscriber Pattern in JavaScript
* From https://medium.com/better-programming/the-publisher-subscriber-pattern-in-javascript-2b31b7ea075a
* The publisher/subscriber pattern is a design pattern that allows us
* to create powerful dynamic applications with modules that can communicate
* with each other without being directly dependent on each other.
* Advatage: Nifty
* Disadvantage: Does not scale well. Can't assert if you already subscribed to the same callback before.
* Best for: Usecases with a limited scope.
*/
function pubSub() {
const subscribers = {}
function publish(eventName, data) {
if (!Array.isArray(subscribers[eventName])) {
return
}
subscribers[eventName].forEach((callback) => {
callback(data)
})
}
function subscribe(eventName, callback) {
if (!Array.isArray(subscribers[eventName])) {
subscribers[eventName] = []
}
subscribers[eventName].push(callback)
const index = subscribers[eventName].length - 1
return {
unsubscribe() {
subscribers[eventName].splice(index, 1)
/* Alt. without using index */
// subscribers[eventName] = subscribers[eventName].filter((cb) => {
// /* Does not include the callback in the new array */
// return (cb === callback)? false: true;
// })
},
}
}
return {
publish,
subscribe,
}
}
// ===========
function showMeTheMoney(money) {
console.log(money)
}
pubSub().subscribe('show-money', showMeTheMoney)
// Later...
pubSub().publish('show-money', 1000000)
//============
const unsubscribeFood = subscribe('food', function (data) {
console.log(`Received some food: ${data}`)
})
// Removes the subscribed callback
unsubscribeFood()
A refactored version of my Broadcaster lib
No frills barbones version where you can namespace it to fit your needs.
const namespace = 'SKF_UI_'
//--------------------------------------------------------------------------------------------------
// Eported eventEmitter
//--------------------------------------------------------------------------------------------------
interface emitEventProps {
eventDescription: string
eventRootElement?: HTMLElement
tagName: string
}
/**
* Creates a namespaced event and emits it from given element.
*
* @param {Object} obj - Object containing event data
* @param {string} obj.eventDescription - A string that the details property in the listener callback can read
* @param {string} obj.eventRootElement - a reference to the element from where to emit
* @param {string} obj.tagName - A namespace string that holds the ui tag name of the emitter
* @returns namespaced event id string mostly for debuggin purpose. This is the id to use in your listener.
*/
const eventEmit = ({
eventDescription,
eventRootElement,
tagName,
}: emitEventProps) => {
if (!eventRootElement) return
const eventNameId = `${namespace}${tagName.toLocaleUpperCase()}`
const tag_event = new CustomEvent(eventNameId, {
bubbles: true,
detail: {
description: eventDescription,
},
})
eventRootElement?.dispatchEvent(tag_event)
return eventNameId
}
//--------------------------------------------------------------------------------------------------
// Eported eventListener
//--------------------------------------------------------------------------------------------------
interface EventListenerOptionsType {
capture?: boolean
once?: boolean
passive?: boolean
}
// Constants
const hubId = ` ${namespace}LISTENER `
const eventTarget = createOrGetCustomEventNode(hubId)
/**
* A optional event listener function matching the 'eventEmit' function. Feature de-anonymizer
* on listener callback (de-anonymizing and thereby making inline functions cancelable),
* returns a cancel-listener-function (practical in useEffects) and
* create and appends a comment node where one can inspect listeners from devtools.
*
* @param tagName - The name of the ui component emitting the event.
* @param listenerCallback - Callback function triggered on reception of emit.
* @param options - Takes an object with boolean key of 'once' that auto removes listener after one receieved emit.
* @returns function to cancel listener
*/
const eventListener = (
tagName: string,
listenerCallback: EventListenerOrEventListenerObject,
options: { once?: boolean } = {}
) => {
return bind(eventTarget, {
type: (namespace +
tagName.toLocaleUpperCase()) as keyof HTMLElementEventMap,
listener: listenerCallback,
options,
})
}
//--------------------------------------------------------------------------------------------------
// Functional lego logic
//--------------------------------------------------------------------------------------------------
/** Helps name anonumous functions that can not be used in native event handler */
function bind(
target: Node,
{
type,
listener,
options,
}: {
listener: EventListenerOrEventListenerObject
options?: EventListenerOptionsType
type: keyof HTMLElementEventMap
}
) {
target.addEventListener(type, listener, options)
return function unbind() {
target.removeEventListener(type, listener, options)
}
}
/** Initiate or retreive node for custom event */
function createOrGetCustomEventNode(hubId: string): Node {
const nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_COMMENT
)
while (nodeIterator.nextNode()) {
if (nodeIterator.referenceNode.nodeValue === hubId) {
return nodeIterator.referenceNode
}
}
return document.body.appendChild(document.createComment(hubId))
}
//--------------------------------------------------------------------------------------------------
// Exports
//--------------------------------------------------------------------------------------------------
export { eventEmit, eventListener }