Electron Adventures: Episode 29: Vue Orthodox File Manager
Tags: javascript, electron, vue
Published: 2021-08-24 22:53:55

Of all the "major" frameworks, Vue is the one I used the least, as it seems like it's most popular in China and not so much here, so I can't guarantee that my Vue code will be all that great.

But it's an adventure, so let's try anyway. I'll try to implement Svelte file manager from episode 27 as a Vue app. After that I'll switch back to the Svelte version for a while, but you can follow along in Vue, or in any other framework you prefer.

### `src/Footer.vue`

This is almost identical to Svelte version - we just wrap the whole thing in `<template>` tag, and add a small declaration of what we're exporting.

All that stuff is redundant in Svelte, where we can just write the contents of the template for simple components like this.

```html
<script>
  export default {
    name: "Footer"
  }
</script>

<template>
  <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>
</template>

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

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

### `src/Panel.vue`

This component is responsible for one of the panels of the file manager. The styling is identical to the Svelte version:

```html
<style>
  .left {
    grid-area: panel-left;
  }
  .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;
  }
  .debug {
    max-width: 40vw;
  }
</style>
```

Template isn't too bad. There's no expressions for flipping each class separately like Svelte's `class:a={conditionA} class:b={conditionB}`, but you can have `v-bind:class` with object value like `v-bind:class={a: conditionA, b: conditionB}`, which is more or less the same thing.

Loops go into `v-for` attribute of each element, not outside the element. I find it less intuitive, but I guess it's somewhat more concise. This should be clear enough if you know Svelte:

```html
<template>
  <div v-bind:class="{
        panel: true,
        active: active,
        left: (position==='left'),
        right: (position==='right'),
      }">
    <div
      v-for="file in files"
      v-bind:class="{
        file: true,
        focused: (file === focused),
        selected: (selected.includes(file)),
      }"
      @click="() => onclick(file)"
      @contextmenu="() => onrightclick(file)"
    >
      {{file}}
    </div>
  </div>
</template>
```

Script part is a good deal more complicated than Svelte version:

```html
<script>
  export default {
    name: "Panel",
    props: ["position", "files", "active"],
    data: (e) => {
      return {
        focused: e.$props.files[0],
        selected: [],
      }
    },
    methods: {
      onclick: function(file) {
        this.$data.focused = file
        this.$emit("activate")
      },
      onrightclick: function(file) {
        this.$data.focused = file
        this.$emit("activate")
        this.flipSelected(file)
      },
      flipSelected: function(file) {
        if (this.$data.selected.includes(file)) {
          this.$data.selected = this.$data.selected.filter(f => f !== file)
        } else {
          this.$data.selected = [...this.$data.selected, file]
        }
      },
      goUp: function() {
        let i = this.$props.files.indexOf(this.$data.focused)
        if (i > 0) {
          this.$data.focused = this.$props.files[i - 1]
        }
      },
      goDown: function() {
        let i = this.$props.files.indexOf(this.$data.focused)
        if (i < this.$props.files.length - 1) {
          this.$data.focused = this.$props.files[i + 1]
        }
      },
      handleKey: function(e) {
        if (!this.$props.active) {
          return
        }
        if (e.key === "ArrowDown") {
          e.preventDefault()
          this.goDown()
        }
        if (e.key === "ArrowUp") {
          e.preventDefault()
          this.goUp()
        }
        if (e.key === " ") {
          e.preventDefault()
          this.flipSelected(this.$data.focused)
          this.goDown()
        }
      },
    },
    mounted() {
      window.addEventListener("keydown", this.handleKey)
    },
  }
</script>
```

What is even going on here?

* we declare that our component is a `Panel`
* we declare properties - you can also specify their types, validations, and some extra attributes here
* do declare initial state, we make `data` function which returns initial state, it can use `this.$props`
* all methods go into `methods` dictionary, inside it we can access `this.$data`, `this.$props`
* we can call parent either through events with `this.$emit`, or by passing callbacks as props - Svelte also supports both mechanism, Vue and Svelte documentation differ on which one to promote, but really it's up to you (React only has callbacks, no events)
* methods can also call other methods with `this.methodName(...)`
* this part tripped me over - everything must use `function()` style functions, not `=>` style arrow functions, otherwise `this` will be incorrect
* there's no equivalent of `<svelte:window>` to which we can attach and automatically detach event listeners - so I used `mounted()` lifecycle hook; in general we should also call `window.removeEventListener` when component is unmounted, it's just not something our app does

### Vue vs Svelte example

To compare just one small function between Vue and Svelte, Svelte saves us a lot of `this.$stuff`, but other than that they follow very similar flow:

```javascript
// Vue
goUp: function() {
  let i = this.$props.files.indexOf(this.$data.focused)
  if (i > 0) {
    this.$data.focused = this.$props.files[i - 1]
  }
},

// Svelte
let goUp = () => {
  let i = files.indexOf(focused)
  if (i > 0) {
    focused = files[i - 1]
  }
}
```

### `src/App.vue`

This is a simpler component. Global styles and component styles go into separate tags:

```html
<style global>
  body{
    background-color: #226;
    color: #fff;
    font-family: monospace;
    margin: 0;
    font-size: 16px;
  }
</style>

<style>
  .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>
```

The template is very close to what we did in Svelte:

```html
<template>
  <div class="ui">
    <header>
      File Manager
    </header>
    <Panel
      v-bind:files=filesLeft
      position="left"
      v-bind:active="(activePanel === 'left')"
      @activate="activateLeft"
    />
    <Panel
      v-bind:files=filesRight
      position="right"
      v-bind:active="(activePanel === 'right')"
      @activate="activateRight"
    />
    <Footer />
  </div>
</template>
```

We map with `@activate` what to do when specific component emits `activate` event.

And in script part we have a bit more boilerplate than in Svelte version, such as explicit list of component we want to use:

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

  export default {
    name: "App",
    components: {
      Panel,
      Footer,
    },
    data: () => ({
      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",
      ],
      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",
      ],
      activePanel: "left",
    }),
    methods: {
      activateLeft: function() {
        this.$data.activePanel = "left"
      },
      activateRight: function() {
        this.$data.activePanel = "right"
      },
      handleKey: function(e) {
        if (e.key === "Tab") {
          e.preventDefault()
          if (this.$data.activePanel === "left") {
            this.$data.activePanel = "right"
          } else {
            this.$data.activePanel = "left"
          }
        }
      }
    },
    mounted() {
      window.addEventListener("keydown", this.handleKey)
    },
  }
</script>
```

### Result

Here's the results:

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

As I mentioned, I haven't used Vue much, so it's possible I missed something really obvious here. If so, let me know.

In the next episode we'll go back to our Svelte app, and make it work with real files not just static mock data.

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