Electron Adventures: Episode 26: Svelte Orthodox File Manager
Tags: electron, svelte, javascript
Published: 2021-08-20 23:54:12

This episode was created in collaboration with the amazing [Amanda Cavallaro](https://www.amandacavallaro.com/).

In previous episode, we created some pure HTML+CSS mockup of a file manager. To turn it into reality, we need to rearrange them into some components.

Again, I'll be using Svelte, but you can easily try to follow the same steps using any other framework.

### Structure

We'll end up with a lot more, but for now I'll have just three component classes:

* `App`
* `Panel` (included twice)
* `Footer`

`Footer` has no state.

Right now state of each `Panel`s is split between `App` (which contains list of files) and `Panel` (which contains information which file is focused, and which are selected). This will definitely change a few times before we're done.

### Events

To even see if CSS is correct, I implemented a few events:

* left clicking on a file will focus on it; it will also activate its panel if it's not active
* right clicking on a file will do all that, and also flip its selected status

### `App.js`

The html structure is a single grid component with 4 children:

```html
<div class="ui">
  <header>
    File Manager
  </header>
  <Panel
    files={filesLeft}
    position="left"
    active={activePanel === "left"}
    onActivate={() => activePanel = "left"}
  />
  <Panel
    files={filesRight}
    position="right"
    active={activePanel === "right"}
    onActivate={() => activePanel = "right"}
  />
  <Footer />
</div>
```

There are fancier ways to handle panel activation, but it's very simple state - either left or right panel is active, so what we have is perfectly suitable.

We pass `position` to `Panel` so it can have proper `grid-area`. Panels don't strictly need to know if they're left or right, it just makes CSS more straightforward if they do.

Here's how we style it. I also include body styling here instead of having any kind of global css files:

```html
<style>
  :global(body) {
    background-color: #226;
    color: #fff;
    font-family: monospace;
    margin: 0;
    font-size: 16px;
  }
  .ui {
    width: 100vw;
    height: 100vh;
    display: grid;
    grid-template-areas:
      "header header"
      "panel-left panel-right"
      "footer footer";
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto 1fr auto;
  }
  .ui header {
    grid-area: header;
  }
  header {
    font-size: 24px;
    margin: 4px;
  }
</style>
```

And finally the Javascript. It's just one variable for active panel, and some static data from [Cat Ipsum](http://www.catipsum.com/).

Eventually, `filesLeft` and `filesRight` will both come from what's actually in the filesystem, and will likely be managed elsewhere, but this is good enough for now:

```html
<script>
  import Panel from "./Panel.svelte"
  import Footer from "./Footer.svelte"

  let activePanel = "left"
  let filesLeft = [
    "Cat.js",
    "ipsum.js",
    "dolor.js",
    "sit.js",
    "amet.js",
    "walk.js",
    "on.js",
    "keyboard.js",
    "hide.js",
    "when.js",
    "guests.js",
    "come.js",
    "over.js",
    "play.js",
    "with.js",
    "twist.js",
    "ties.js",
  ]
  let filesRight = [
    "Ask.png",
    "to.png",
    "be.png",
    "pet.png",
    "then.png",
    "attack.png",
    "owners.png",
    "hand.png",
    "need.png",
    "to.jpg",
    "chase.png",
    "tail.png",
  ]
</script>
```

### `Footer.svelte`

Footer is completely static HTML and CSS. We'll make those buttons do things in the future, and maybe we can turn this into some kind of context-sensitive shortcuts bar. For now this will do:

```html
<footer>
  <button>F1 Help</button>
  <button>F2 Menu</button>
  <button>F3 View</button>
  <button>F4 Edit</button>
  <button>F5 Copy</button>
  <button>F6 Move</button>
  <button>F7 Mkdir</button>
  <button>F8 Delete</button>
  <button>F10 Quit</button>
</footer>

<style>
  footer {
    text-align: center;
    grid-area: footer;
  }

  button {
    font-family: inherit;
    font-size: inherit;
    background-color: #66b;
    color: inherit;
  }
</style>
```

### `Panel.svelte`

Even to just have some mockup, we need quite a bit of state:

* `position` - left or right, just to keep CSS easy
* `files` - list of files to display, passed from the parent
* `active` - whether it's active or not - we need this as we need to remember focus in non-active tab, even if we don't show it
* `onActivate` - callback to tell the app that this panel wants to become active
* `onclick` - event handler for left clicking on a file
* `onrightclick`  - event handler for right clicking on a file - browser event is nonsensically named "`oncontextmenu`"

```html
<script>
  export let position
  export let files
  export let active
  export let onActivate

  let focused = files[0]
  let selected = []
  let onclick = (file) => {
    onActivate(position)
    focused = file
  }
  let onrightclick = (file) => {
    onActivate(position)
    focused = file
    if (selected.includes(file)) {
      selected = selected.filter(f => f !== file)
    } else {
      selected = [...selected, file]
    }
  }
</script>
```

HTML is just a simple loop, with events for left and right clicks, and with a bunch of logic controlling CSS classes. If you're following this with a different framework, many lack shortcuts for controlling different classes with separate variables, and for preventing default event handling, so you might need to write a bit extra code:

```html
<div class="panel {position}" class:active={active}>
  {#each files as file}
    <div
      class="file"
      class:focused={file === focused}
      class:selected={selected.includes(file)}
      on:click|preventDefault={() => onclick(file)}
      on:contextmenu|preventDefault={() => onrightclick(file)}
    >{file}</div>
  {/each}
</div>
```

CSS is really easy. As I mentioned before:

```html
<style>
  .panel-left {
    grid-area: panel-left;
  }
  .panel-right {
    grid-area: panel-right;
  }
  .panel {
    background: #338;
    margin: 4px;
  }
  .file {
    cursor: pointer;
  }
  .file.selected {
    color: #ff2;
    font-weight: bold;
  }
  .panel.active .file.focused {
    background-color: #66b;
  }
</style>
```

### Result

Here's the results, looking just as our static mockup:

![Episode 26 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0yskw434pjlrmbx5rzek.png)

OK, that was a lot. From now on, we'll try to work on one thing at a time.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-26-svelte-orthodox-file-manager).