Adding simple PubSub, part 2
Tags: craftcode, pubsub, decouple, vanillacode
Published: 2024-01-01 21:50:46

**From event delegation to true pubsub.**

_Less than 5 minutes, 1178 words, 4th grade_

In our [previous article](https://dev.to/craft-code/adding-simple-pubsub-part-1-4hi0) we built a simple event delegation system. Our intent was to develop that into a full **PubSub** system. We can use this PubSub system to create an **event-driven architecture.**

It will be better if we split the event delegation feature from the PubSub part so we are clear what weʼre doing. We can begin by creating a `listeners` object to replace our `subscriptions` object. Weʼll store our listeners here:

```js
export default {}
```

We will also rename our `subscribe` function to `register`. We will move the event listener callback into our `register` function:

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

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

  listeners[type] ??= {}

  if (! Object.keys(listeners[type]).length) {
    document.body.addEventListener(type, function (event) {
      const id = event.target?.id
      const type = event.type

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

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

It should be obvious what this does (see [our previous essay](https://dev.to/craft-code/adding-simple-pubsub-part-1-4hi0)). Now letʼs change our `unsubscribe` function to an `unregister` function:

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

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

  delete listeners?.[type]?.[id]

  if (! Object.keys(listeners[type] || {}).length) {
    delete listeners[type]
  }
}
```

Again, this works exactly the same as our previous `unsubscribe` function. Now we have moved our event delegation system into itʼs own module. Now to our PubSub system.

Note that after we unregister the last listener, we clean up after ourselves.

As a reminder, here is our `castEvent` function code:

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

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

  return type
}
```

## Adding the PubSub system

We copied the code above into new files. This left our previous `pubsub` files. So, we already have our basic PubSub module. But we will need to update them a bit.

The `subscriptions` shared object remains as before:

```js
export default {}
```

Our `unsubscribe` function is also as before. But as we are no longer dealing with browser events, we donʼt need the `castEvent` function anymore. Those browser events go to our event delegator system above, not to PubSub.

We have no ID, but weʼll want a way to unsubscribe, so we generate a <abbr title="universally-unique identifier">UUID</abbr> and return that from our `subscribe` function.

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

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

  if (Object.keys(subscriptions[topic]) < 1) {
    document.body.addEventListener(
      topic,
      (event) => {
        for (
          const cb of Object.values(subscriptions[topic])
        ) {
          cb(event)
        }
      },
      true
    )
  }

  const token = crypto.randomUUID()

  subscriptions[topic][token] = callback

  return token
}
```

This is pretty self-explanatory, I hope. Our `subscribe` function takes two parameters:

- the event `type` to listen for, e.g., `EMAIL_UPDATED`
- the `callback` function to call when some component publishes an event of this type

**We** will decide which events to publish and to subscribe to. The nice thing about a PubSub system is that **we can create any event we like.** We are not limited to browser events.

First, we ensure that there is an object associated with this subscription type.

If this is the first subscription of this `type`, then we add a listener for this event type. Our callback loops through all the individual callbacks stored in the `subscriptions` object. It calls each in turn, passing the current `event` object.

Then we generate a random UUID as our `token`. We use this to assign the passed-in callback to this topic and token (line #16). Finally, we return the token for use by the `unsubscribe` function.

What about our `unsubscribe` function? It is simple as can be:

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

export default function (topic, token) {
  delete subscriptions?.[topic]?.[token]

  if (Object.keys(subscriptions[topic]) < 1) {
    delete subscriptions[topic]
  }
}
```

We take as parameters the `topic` and the `token`. Then we delete the `callback` for that topic/token in the `subscriptions` object.

Finally, if there are no remaining subscribers for that `topic`, we delete the topic.

Now we need a `publish` function to allow us to publish to custom events. Here it is:

```js
export default function (topic, detail = {}) {
  const customEvent = new CustomEvent(
    topic,
    {
      bubbles: false,
      detail,
    }
  )

  document.body.dispatchEvent(customEvent)
}
```

We pass the `topic`, which is our custom event such as `EMAIL_UPDATED`. And we pass a `detail` object. This is the data that our custom event will carry. It could be anything. See our examples below.

On lines #2 to #8, we create a `CustomEvent`. We assign it our `topic` as the event type. This is what our subscribers are listening for. We also pass the `detail` object and set event bubbling to `false`.

Then on line #10 we call `dispatchEvent` on the `document.body` element to dispatch our custom event. By dispatching it and listening for it on the `body` element, we do not need to bubble it.

## Our PubSub system in action

We add the PubSub system to our window (`globalThis`) object. We add a single object representing our namespace, `_xx`. Then we add our various modules to that:

```js
import listeners from "./modules/listeners.js"
import publish from "./modules/publish.js"
import register from "./modules/register.js"
import subscribe from "./modules/subscribe.js"
import subscriptions from "./modules/subscriptions.js"
import unregister from "./modules/unregister.js"
import unsubscribe from "./modules/unsubscribe.js"

globalThis._xx ??= {}

Object.assign(globalThis._xx, {
  listeners,
  publish,
  register,
  subscribe,
  subscriptions,
  unregister,
  unsubscribe,
})

console.info("« PubSub and event delegation enabled. »")
```

[There must be a better way to do this. Ideas?]

Weʼve set up [an example of the PubSub system in use.](http://localhost:3011/examples/pubsub-2/index.html) You can see the various modules there:

- [/modules/cast-event.js](https://sitebender.io/examples/pubsub-2/modules/cast-event.js)
- [/modules/listeners.js](https://sitebender.io/examples/pubsub-2/modules/listeners.js)
- [/modules/publish.js](https://sitebender.io/examples/pubsub-2/modules/publish.js)
- [/modules/register.js](https://sitebender.io/examples/pubsub-2/modules/register.js)
- [/modules/subscribe.js](https://sitebender.io/examples/pubsub-2/modules/subscribe.js)
- [/modules/subscriptions.js](https://sitebender.io/examples/pubsub-2/modules/subscriptions.js)
- [/modules/unregister.js](https://sitebender.io/examples/pubsub-2/modules/unregister.js)
- [/modules/unsubscribe.js](https://sitebender.io/examples/pubsub-2/modules/unsubscribe.js)
- [index.js](https://sitebender.io/examples/pubsub-2/index.js)

Now letʼs set up a [simple test page.](https://sitebender.io/examples/pubsub-2) Imagine that we have a set of individually-editable fields, such as name, email, and phone:

```html
<form id="name-editor">
  <label for="name">Name</label>
  <input id="name" name="name" type="text" />
  <button type="submit">Update</button>
</form>
<form id="email-editor">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" />
  <button type="submit">Update</button>
</form>
<form id="phone-editor">
  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" />
  <button type="submit">Update</button>
</form>
```

And we will include some temporary elements to permit us to display our events:

```html
<pre id="name-output"></pre>
<pre id="email-output"></pre>
<pre id="phone-output"></pre>
```

Now in our script we first import the PubSub module and then register our event listeners for `submit`:

```html
<script src="./index.js" type="module"></script>
<script type="module">
  globalThis.addEventListener(
    "DOMContentLoaded",
    async () => {
      globalThis._xx?.register(
        "submit",
        "name-editor",
        (event) => {
          event.preventDefault()

          globalThis._xx?.publish("NAME_UPDATED", {
            id: event.target.elements.name.id,
            name: event.target.elements.name.value,
          }
        )
      },
    )

    // register submit listeners for email and phone, too
  })
</script>
```

First, we import our PubSub system as a module. Then we add a `DOMContentLoaded` event listener to run our registration code after the DOM loads.

Next we use the `register` function of our event delegation system to register a `submit` handler. This listens for a `submit` event on the form with the passed ID of “name-editor”. On submit, it calls the passed callback, which:

1. prevents the default submission so that we donʼt reload the page
2. calls the `publish` PubSub function to publish our custom event. We will give it the type `NAME_UPDATED`. And weʼll pass a `details` object with the `id` and `value` of the input.

We do the same thing for the `email` and `phone` fields. Now, when we submit any of these mini-forms, PubSub will **create a custom event and publish it to our PubSub system.**

We can now subscribe to these events anywhere in our app. And when a component publishes that event, then we can do nothing, one thing, or **many things.**

Our forms donʼt know who is listening to their events. Our subscribers donʼt know (or care) who is raising these events or why. **We have decoupled our code.** The PubSub system acts as an event bus to pass around these events. And we can make up any events we need.

Now, to show how it works, we will add subscribers. These will post the stringified events to the `pre` elements we added above:

```html
<script type="module">
  globalThis.addEventListener(
    "DOMContentLoaded",
    async () => {
      // register event listeners that publish custom events
    
      const first = globalThis._xx?.subscribe(
        "NAME_UPDATED",
        (event) => {
          const pre = document.querySelector("pre#name-output")
       
          pre.appendChild( 
            document.createTextNode(
              JSON.stringify({
                type: event.type,
                detail: event.detail,
              }, null, 2) + "\n\n"
            )
          )
        },
      )

      // subscribe to EMAIL_UPDATED and
      // PHONE_UPDATED events as well
  })
</script>
```

Here we call the `subscribe` function of our PubSub module. We pass it the `topic` to which we wish to subscribe. And we pass a `callback` function to call when a component raises that topic.

In our example callback function, we grab the `pre` element for that event. Then we stringify the event detail and append it as a child to that element.

You can try this yourself on [our example test page.](https://sitebender.io/examples/pubsub-2)

Of course, this is a simple example. We can do much more with this. For example, we can include much more detail about the event. We can also have more generic or more specific events. We could also:

- Allow handlers to delete themselves after a specific number of calls (e.g., once)
- Broadcast an event to all topics
- Use `BroadcastChannel` to pass events between browser tabs or windows
- Use `websockets` (or similar) to pass events between different devices

In part 3 of this series, weʼll extend the system a bit. Stay tuned.