Adding simple PubSub, part 1
Tags: craftcode, pubsub, vanillacode, decouple
Published: 2023-12-13 04:23:39


**PubSub is a great way to decouple components.**

_4 minutes or so, 1037 words, 4th grade_

One of the most important Craft Code principles is [loose coupling](http://localhost:3002/glossary/l#loose-coupling) (or decoupling). If we organize our code into **loosely-coupled modules**, then we can swap them out with ease. We can refactor them, update them, reduce tech debt.

In general, the more we can **split our code into simple modules,** the better. We can treat them as black boxes. But then we will need a way for them to _communicate_.

An excellent way is via an `event bus`, or a publish-subscribe (PubSub) system. This is a tried-and-true design pattern. It has been around since, hmm, [Amenhotep](https://en.wikipedia.org/wiki/Amenhotep_I) (Egyptian dude). Or thereabouts.

We can build a **simple pubsub module** in JavaScript. Then we can use that to connect our components to each other. And we can do it without any component having to “know” any other component. Thatʼs loose coupling.

So letʼs get to it. And in successive parts, weʼll see how it all fits together.

## Our simple pubsub module

First, weʼre gonna need a place to put our **subscriptions**. This should work:

```js
export default {}
```

All we need to track our subscribers is a shared object. Thus, our `subscriptions` module exports an **empty object literal.** We can share this across our other modules.

Doesnʼt get much easier than that!

We will add our subscribers to this object by event type and element ID. We need a `subscribe` function to let us do that:

```js
import eventListener from "./event-listener.js"
import subscriptions from "./subscriptions.js"

export default function (type, id, callback) {
  subscriptions[type] ??= {}

  if (! subscriptions[type].length) {
    document.body.addEventListener(type, eventListener)
  }

  subscriptions[type][id] = callback
}
```

You can see from the code that we begin by importing our `subscriptions` module. This is no more than a plain JavaScript object.

Then we create **an anonymous function** that we export as the `default`. This is our `subscribe` function. In it, we ensure that our event type exists as an object inside the `subscriptions` object. If not, we assign an empty object literal to that key. For example, `subscriptions["click"] = {}`.

OK, so are we **already listening** for this event type? If we have no subscribers yet, then we can assume not. So we add an **event listener** to the `body` element listening for that type of event. We pass it an `eventListener` function. Weʼll call this function when the user triggers that event. We discuss the `eventListener` function below.

Finally, after exiting the conditional, we set a property on the `type` object. The key is the ID of the element weʼre watching. Our callback function is the value. Example: a “click” event on an element with ID of “my-button.” Like this: `subscriptions["click"]["my-button"] = eventListener`.

What, then, does this `eventListener` function do? Here it is:

```js
import subscriptions from "./subscriptions.js"

export default function (event) {
  const id = event.target?.id

  event.target && subscriptions?.[event.type]?.[id]?.(event)
}
```

This is a very simple function. It takes the passed `event`. Then it extracts the ID of the `target` element. Finally, it uses the `event.type` and that ID to get the callback function from the `subscriptions`. **We call the callback function and pass it the event.**

Done.

Of course, weʼll also need to be able to unsubscribe:

```js
import subscriptions from "./subscriptions.js"

export default function (type, id) {
  subscriptions[type] ??= {}

  delete subscriptions[type][id]

  if (! Object.keys(subscriptions[type])?.length) {
    delete subscriptions[type]
  }
}
```

Our `unsubscribe` function works like the `subscribe` function but in reverse. Who knew?

We import the `subscriptions` model. Then we **export an anonymous function** as `default`. This is our `unsubscribe` function.

It first ensures the the `type` key (our event type) exists in the `subscriptions` object. Then we use the `delete` operator to remove the ID key from that object, if it exists. Finally, we check to see if there are **any remaining keys** in that `type` object. If not, we delete that object as well.

## How it works

Letʼs create a button we can use to trigger an event:

```html
<button id="do-it">Do it!</button>
```

Rather than adding our `click` event listener to the element itself, we can use our PubSub system. We will subscribe to `click` events on the element with ID “do-it” instead:

```js
import subscribe from "./modules/subscribe.js"

subscribe(
  "click",
  "do-it",
  () => console.log("Click on do-it element."),
)
```

## Wait … what about the pub in PubSub?

Ah ha! Good catch. Where is our `publish` function?

Hmm. Our simple PubSub module is actually only half an implementation. What we have here is more **event delegation.** It is the browser that does the publishing by raising events. All we do is to subscribe to those events.

Guess what Part 2 of this essay involves.

But before we go there, there are some problems with this model. Event delegation is efficient and the way to go, but **not all events bubble up to the body.** Sigh … nothingʼs perfect.

The two that we most want to handle are `focus` and `blur`. The good news is that we can use `focusin` and `focusout` on the body to do the same thing. But it would be nice if module users could still think `focus` and `blur`.

So we can update our functions to map one set of terms to the other. We will need a mapper function. We can extend this later as necessary.

```js
export default function (type) {
  if (type === "blur") {
    return "focusout"
  }

  if (type === "focus") {
    return "focusin"
  }

  return type
}
```

We can update our `subscribe` function:

```js
import castEvent from "./cast-event.js"
import eventListener from "./event-listener.js"
import subscriptions from "./subscriptions.js"

export default function (eventType, id, callback) {
  const type = castEvent(eventType)

  subscriptions[type] ??= {}

  if (! subscriptions[type].length) {
    document.body.addEventListener(type, eventListener)
  }

  subscriptions[type][id] = callback
}
```

And weʼll need to do the same to our `unsubscribe` function:

```js
import castEvent from "./cast-event.js"
import subscriptions from "./subscriptions.js"

export default function (eventType, id) {
  const type = castEvent(eventType)

  subscriptions[type] ??= {}

  delete subscriptions[type][id]

  if (! Object.keys(subscriptions[type])?.length) {
    delete subscriptions[type]
  }
}
```

We will continue this in Part 2. Meanwhile, here is a [simple, vanilla JS example.](https://sitebender.io/examples/pubsub/index.html)

Open up the browserʼs DevTools console and then click on the “DO IT!” button. Nothing should happen. Now click the “SUBSCRIBE” button. Youʼve now subscribed to clicks on the “DO IT!” button. Click DO IT!. This time you should see a message in the console.

Now click the “UNSUBSCRIBE” button. You have unsubscribed and further clicks on DO IT! will have no effect.

You can also see how the castEvent mapping works. Take a look at the **page source.**

On lines #96 to #102 you can see how we subscribed to the focus and blur events on the SUBSCRIBE button. Tab to the button (or click on it) to focus it, and then tab away. You should see messages in the console to show that you focused then blurred the button.

Nice, eh?