You are looking for the concept of Observer or Publisher/Subscriber ("PubSub").
These are pretty easy to create on your own.
Here's a slightly cleaned version of the application-only code (that might run on DOM-ready, or whatever), to run the demo code below.
const trigger = $("#Trigger");
const system = Publisher();
// add my listeners
system
.on("trigger", triggerContentUpdate)
.on("update", updateView)
.on("update", logUpdate); // note how I have two things bound to the same event
// something that kicks off the "trigger" event
// could be a timer, or an AJAX call, or anything else
trigger.onclick = () => system.fire("trigger");
Assuming you had a Publisher library and had the above listener functions in your app, this is pretty much all of the JS you'd have to write to make the demo below work.
The library I wrote as practice, and the implementation of the demo are below.
/*** LIBRARY CODE ***/
// Channel.js
const Channel = () => {
const listeners = new Set();
const channel = {
listen(listener) {
listeners.add(listener);
return channel;
},
ignore(listener) {
listeners.delete(listener);
return channel;
},
notify(data) {
listeners.forEach(react => react(data));
return channel;
}
};
return channel;
};
// Publisher.js
const Publisher = () => {
const channels = {};
function getChannel(name) {
const channel = channels[name] || Channel();
channels[name] = channel;
return channel;
}
const publisher = {
on(name, subscriber) {
getChannel(name).listen(subscriber);
return publisher;
},
off(name, subscriber) {
getChannel(name).ignore(subscriber);
return publisher;
},
fire(name, data) {
getChannel(name).notify(data);
return publisher;
}
};
return publisher;
}
/*** END OF LIBRARY CODE ***/
/*** APPLICATION CODE ***/
// const Publisher = require("publisher"); or import Publisher from "publisher";
const $ = selector => document.querySelector(selector);
const trigger = $("#Trigger");
const input = $("#Content");
const output = $("#Result");
const log = $("#Log");
const system = Publisher();
system
.on("trigger", () => {
const content = input.value;
system.fire("update", content);
})
.on("update", content => output.value = content)
// normally, I never use innerHTML, and my logic and view are never attached, but this is dirt-simple now
.on("update", content => log.innerHTML += `<li>[${ (new Date()).toISOString() }]: ${content}</li>`);
trigger.onclick = () => system.fire("trigger");
/*** END OF APPLICATION CODE ***/
<h3>Input</h3>
<div>
<input id="Content" type="text">
<button id="Trigger">Update</button>
</div>
<h3>Output</h3>
<output id="Result"></output>
<h3>Log</h3>
<ul id="Log"></ul>
Of course, I'm using relatively modern syntax, but nothing that can't be rewritten in older compatible code, or transpiled and given a polyfill, to go all the way down to IE7/8 support.
If I wanted to make some arbitrary object a publisher, I could do that rather quickly as well.