DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 29: Vue Orthodox File Manager

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.

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

src/Panel.vue

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

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

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:

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

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

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

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:

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

src/App.vue

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

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

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

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

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:

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

Result

Here's the results:

Episode 29 Screenshot

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.

Discussion (0)