DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 26: Svelte Orthodox File Manager

This episode was created in collaboration with the amazing Amanda Cavallaro.

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 Panels 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:

<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>
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

And finally the Javascript. It's just one variable for active panel, and some static data from Cat Ipsum.

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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"
<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>
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

CSS is really easy. As I mentioned before:

<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>
Enter fullscreen mode Exit fullscreen mode

Result

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

Episode 26 Screenshot

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.

Discussion (0)