Electron Adventures: Episode 33: Event Routing
Tags: javascript, electron, svelte
Published: 2021-08-28 00:55:54

Most web apps have fairly straightforward event system - you click on something, or you focus on some field, then type some stuff into it. That event either affects just the component, or component might send it to its parent.

Unfortunately that's not good enough for our file manager.

### File Manager Events

Events can come from multiple sources, and affect multiple components, with dynamic mapping between event type and target. If user wanted to create a new directory, there are so many ways:

* press F7 (or some other shortcut key, if that got changed by the user)
* click "F7 Mkdir" button in the footer
* open command palette, then select "New Folder" from the list
* choose "File > New Folder" from application menu - on Windows it's on top of the window, on OSX on top of the screen

Then whichever way this event triggers, it needs to go to correct active panel. And we should probably ignore such event if some dialog is already open.

So there's a lot of logic, and it would be a huge mess if we smooshed it all over the codebase. There should be some central place where most events are sent, and which then decides what to do with those events.

That doesn't mean we couldn't also have local events - for example clicking a button, or typing something into a field can be managed by a single component just fine.

We're going to to use Svelte stores, Svelte context, and simple `EventBus` class to manage all that.

### Simple Event Routing App

We'll integrate it into our file manager app, but it's easier to experiment on something smaller first.

So here's the app:

* there are 4 boxes
* keys 1-4 switch between boxes
* letters a-z or A-Z type into the selected box
* backspace deletes the last character in selected box
* to avoid any complications with modifier keys, I'll use F1, F2, and F3 as cut/copy/paste text in current box - it has nothing to do with operating system clipboard, it's just an internal thing
* F10 quits the app
* and for good measure clicking on each box selects it
* and all that is also available in the footer as clickable buttons

We'll add application menu and command palette to the app later, but it's a lot already.

### `src/EventBus.js`

Well, first event bus. It's a very simple Javascript object. You create an instance, then register event handlers with it.

There's `emit` method, which takes named event target, event name, and any number of arguments. It also handles `*` special event handler, for handling any events that don't have a specific handler.

Right now it will quietly drop any events without specific handler or appropriate target, but maybe we should `console.log` a warning about this? It depends on the use case.

```javascript
export default class EventBus {
  constructor() {
    this.callbacks = {}
  }

  handle(target, map) {
    this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
  }

  emit(target, event, ...details) {
    let handlers = this.callbacks[target]
    if (handlers) {
      if (handlers[event]) {
        handlers[event](...details)
      } else if (handlers["*"]) {
        handlers["*"](event, ...details)
      }
    }
  }
}
```

Nothing about it is specific to Electron or Svelte, it's just very simple pattern.

### `src/App.svelte` template

First, let's get the template and styling as there's nothing fancy here:

```html
<div class="app">
  <Box id="box-1" />
  <Box id="box-2" />
  <Box id="box-3" />
  <Box id="box-4" />
  <Footer />
</div>

<Keyboard />

<style>
  :global(body) {
    margin: 0;
  }
  .app {
    background-color: hsl(180,100%,20%);
    font-family: monospace;
    color: #333;
    height: 100vh;
    width: 100vw;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr auto;
    gap: 10px;
  }
</style>
```

It's a simple grid with 4 boxes and footer. `id` does not have anything to do with HTML DOM `id`, it actually lets each box identify itself to the event system.

`Keyboard` is a bit ununsual component that doesn't generate any DOM - it attaches some event handlers to the main window.

### `src/App.svelte` script

Now let's get to the juicy part:

```html
<script>
  import { writable } from "svelte/store"
  import { setContext } from "svelte"

  import Box from "./Box.svelte"
  import Footer from "./Footer.svelte"
  import Keyboard from "./Keyboard.svelte"
  import EventBus from "./EventBus.js"

  let activeBox = writable("box-1")
  let clipboard = writable("")
  let eventBus = new EventBus()

  setContext("app", {activeBox, clipboard, eventBus})
</script>
```

We create two Svelte stores here - `activeBox` showing which box is currently active, and `clipboard` with contents of the clipboard. We also create `EventBus` instance, where we can register event handlers.

### Contexts and Stores

Then we save all of them into a single context object under key `app`. We could alternatively use 3 separate contexts:

```javascript
  setContext("activeBox", activeBox)
  setContext("clipboard", clipboard)
  setContext("eventBus", eventBus)
```

It doesn't really make any difference since we're setting them from the same place, but if we had more complicated app, multiple contexts could be necessary.

Why do we put a store in a context, not just value? Contexts are read when component is created, and aren't automatically updated. So this would not really work:

```javascript
  let activeBox = "box-1"
  let clipboard = ""
  setContext("app", {activeBox, clipboard, eventBus})
```

This could work:

```javascript
  let activeBox = "box-1"
  let activeBoxSubscriptions = []
  function changeActiveBox(newValue) {
    activeBox = newValue
    for (let callback of activeBoxSubscriptions) {
      callback(newValue)
    }
  }
  function subscribeToActiveBoxChanges(callback) {
    activeBoxSubscriptions.push(callback)
  }
  setContext("app", { activeBox, subscribeToActiveBoxChanges, ... })
```

As long as we remember to only change `activeBox` through `changeActiveBox`. Well, we'd also need to add some mechanism for unsubscribing when component is destroyed.

This kind of subscribing, unsubscribing, callbacks to change values and so on is extremely tedious, so Svelte has stores as shortcut.

If you ever use `$activeBox` anywhere in your component, Svelte will automatically try to subscribe to `activeBox` store, and update `$activeBox` variable for you through such callback. It will also unsubscribe when needed.
This variable is properly reactive, so any changes will automatically apply to template, or to any reactive statements you do.

It should become clearer as we go through a few examples of contexts, stores, and `EventBus` usage in various components.

### `src/App.svelte` event handlers

Application has two event handlers - `quit` (F10) closes the window, and `changeBox` changes which box is active.

`activeBox.set(id)` updates the store, which then runs callbacks in all subscribers (including `App` component itself, there's nothing special about it), setting `$activeBox` in all of them.

```javascript
  function quit() {
    window.close()
  }
  function changeBox(id) {
    activeBox.set(id)
  }
  eventBus.handle("app", {quit, changeBox})
```

There's also one more thing to do - we register a wildcard callback for virtual target `"activeBox"`, which we then resend to whichever box is actually active right now.

```javascript
  function emitToActiveBox(...args) {
    eventBus.emit($activeBox, ...args)
  }

  eventBus.handle("activeBox", {"*": emitToActiveBox})
```

### `src/Footer.svelte`

Well, that was a lot. Fortunately the rest of the app is fairly simple. Here's the footer:

```html
<script>
  import { getContext } from "svelte"
  let { eventBus } = getContext("app")
</script>

<footer>
  <button on:click={() => eventBus.emit("app", "changeBox", "box-1")}>Box 1</button>
  <button on:click={() => eventBus.emit("app", "changeBox", "box-2")}>Box 2</button>
  <button on:click={() => eventBus.emit("app", "changeBox", "box-3")}>Box 3</button>
  <button on:click={() => eventBus.emit("app", "changeBox", "box-4")}>Box 4</button>
  <button on:click={() => eventBus.emit("activeBox", "cut")}>F1 Cut</button>
  <button on:click={() => eventBus.emit("activeBox", "copy")}>F2 Copy</button>
  <button on:click={() => eventBus.emit("activeBox", "paste")}>F3 Paste</button>
  <button on:click={() => eventBus.emit("app", "quit")}>F10 Quit</button>
</footer>

<style>
  footer {
    grid-column-start: span 2;
    text-align: center;
  }
  button {
    font-size: 24px;
    font-weight: bold;
    color: inherit;
    background-color: hsl(180,100%,40%);
    font-family: inherit;
  }
</style>
```

All it does is get `eventBus` instance from the context, then when you click on various buttons it calls `eventBus.emit(target, event, arguments)`.

How it gets delivered to either `app` itself or to the right box is not the footer's business.

### `src/Keyboard.svelte`

```html
<script>
  import { getContext } from "svelte"
  let { eventBus } = getContext("app")

  function handleKey({key}) {
    if (key.match(/^[1234]$/)) {
      eventBus.emit("app", "changeBox", `box-${key}`)
    }
    if (key.match(/^[a-zA-Z]$/)) {
      eventBus.emit("activeBox", "letter", key)
    }
    if (key === "Backspace") {
      eventBus.emit("activeBox", "backspace", key)
    }
    if (key === "F1") {
      eventBus.emit("activeBox", "cut")
    }
    if (key === "F2") {
      eventBus.emit("activeBox", "copy")
    }
    if (key === "F3") {
      eventBus.emit("activeBox", "paste")
    }
    if (key === "F10") {
      eventBus.emit("activeBox", "quit")
    }
  }
</script>

<svelte:window on:keydown={handleKey} />
```

Keyboard is a another pure event source component. It might be a bit unusual in that it doesn't actually add anything to the DOM, it attaches itself to the main `window`.

And again, it gets `eventBus` from the context, handles `keydown` events, and depending on which key was pressed, emits the right event to the right target.

As you can imagine, this component could be extended to handle modifier keys (like Cmd-C or Ctrl-C - this would probably need some platform-specific logic as conventions are different), and even read shortcut preferences from some local configuration, so user can change them. Maybe even to vim keybindings, who knows. All in one place.

### `src/Box.svelte`

With so much logic being elsewhere, the `Box` component is fairly simple. First, the template and styling:

```html
<div class="box" class:active on:click={onClick}>
  {text}
</div>

<style>
.box {
  font-size: 48px;
  font-weight: bold;
  background-color: hsl(180,100%,30%);
  display: flex;
  justify-content: center;
  align-items: center;
}
.box.active {
  background-color: hsl(180,100%,40%);
}
</style>
```

Nothing unusual here. We have a box, displaying `text`, it has `active` class if `active` variable is true, and clicking on it will call `onClick` method.

```html
<script>
  import { getContext } from "svelte"
  let { eventBus, activeBox, clipboard } = getContext("app")

  export let id
  let text = "A"

  function onClick() {
    eventBus.emit("app", "changeBox", id)
  }
  function letter(key)  {
    text += key
  }
  function backspace() {
    text = text.slice(0, -1)
  }
  function cut() {
    clipboard.set(text)
    text = ""
  }
  function copy() {
    clipboard.set(text)
  }
  function paste() {
    text = $clipboard
  }

  eventBus.handle(id, {letter, backspace, cut, copy, paste})

  $: active = ($activeBox === id)
</script>
```

We register long list of events with the `eventBus` instance. Event handlers are super simple here.

There's small trick here that `active` flag changes reactively whenever `activeBox` changes. All the subscriptions, and callback, and such, are handled by Svelte without us having to do anything.

### Result

Here's the results:

![Episode 33 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91x0nrq75tfqwb68ctng.png)

I think it's a fairly clean architecture, code is very concise (unlike with something like let's say Redux), and it's easy to extend it to handle more complex cases.

Svelte stores and contexts are standard part of Svelte, but `EventBus` is just something I created for this app.

Would you design it in a different way? If so, let me know of alternative approaches in the comments.

In the next episode we'll add application menu.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-33-event-routing).