Electron Adventures: Episode 42: Marko File Manager
Tags: javascript, electron, marko
Published: 2021-09-04 04:50:17

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

In previous episode we wrote a Hello World in Marko. Let's try to write something more substantial - a very simple file manager. To keeps things manageable, we're not going to try to reach feature parity with Svelte version, in particular there will be no keyboard support.

### `window` problem

And instantly we run into our first problem. We'd like to access the `window` object from our Marko code. Unfortunately Marko firmly believes that everything should be possible render server-side, so `window` is not available. Code like this will absolutely crash:

```html
<file-list initial=(window.api.currentDirectory()) />
```

That is sort of fine for the Web, but it's absolutely terrible idea for Electron, and it will make a lot of code awkward.

### `src/pages/index/index.marko`

As I mentioned before, all components need `-` in their names. Other than that, it's very straigthforward.

```html
<app-layout title="File Manager">
  <file-manager></file-manager>
</app-layout>
```

### `src/components/buttons-footer.marko`

Instead of starting from the top, let's start from the simplest component.

The footer buttons bar does only one thing, and disregarding labels on the buttons, by mouse click only.

```html
$ function quit() {
  window.close()
}

<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 on-click(quit)>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>
```

Contrary to what you might expect from Svelte, `$` is not reactive statement, it's just inline Javascript not wrapped inside `class { ... }` or such.

There are many ways to handle events. `on-click(quit)` means to call `quit` function. Very similar looking `on-click("quit")` would mean to call `this.quit()` method.

### `src/components/file-manager.marko`

Let's go through the main component one section at a time. This time it's more complicated, so we wrap it in a class.

We'd love to just set `this.state.cwd = window.api.currentDirectory()` - or even don't bother with state and put that in the template part - unfortunately Marko believes in server side rendering so we need to postpone setting that up to `onMount`.

We have one event - activate left or right panel.

```javascript
class {
  onCreate() {
    this.state = {
      cwd: null,
      active: "left",
    }
  }
  onMount() {
    this.state.cwd = window.api.currentDirectory()
  }
  activate(panel) {
    this.state.active = panel
  }
}
```

Template part should be understandable enough, but it has a few complications. First as `state.cwd` is `null`, and we really don't want to bother panels with `null` directory, we wrap the whole thing in `state.cwd`. Essentially we disable server-side rendering here, as server really has no way of knowing what files we have.

`on-activate("activate", "left")` means that when given component emits custom `activate` event, `this.activate("left")` will be called. Marko strongly believes in custom events over React-style callbacks - Svelte works both ways, but custom events are generally nicer.

```html
<div class="ui">
  <header>
    File Manager
  </header>
  <if(state.cwd)>
    <file-list
      initial=(state.cwd)
      id="left"
      active=(state.active==="left")
      on-activate("activate", "left")
    />
    <file-list
      initial=(state.cwd + "/node_modules")
      id="right"
      active=(state.active==="right")
      on-activate("activate", "right")
    />
  </if>
  <buttons-footer />
</div>
```

At least the style section is completely straightforward:

```html
<style>
  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 minmax(0, 1fr) auto;
  }
  .ui header {
    grid-area: header;
  }
  header {
    font-size: 24px;
    margin: 4px;
  }
</style>
```

### `src/components/file-list.marko`

And finally, the most complex component. We'll go through it out of code order, to make understanding easier.

Styling is completely straighforward:

```html
<style>
  .left {
    grid-area: panel-left;
  }
  .right {
    grid-area: panel-right;
  }
  .panel {
    background: #338;
    margin: 4px;
    display: flex;
    flex-direction: column;
  }
  header {
    text-align: center;
    font-weight: bold;
  }
  .file-list {
    flex: 1;
    overflow-y: scroll;
  }
  .file {
    cursor: pointer;
  }
  .file.selected {
    color: #ff2;
    font-weight: bold;
  }
  .panel.active .file.focused {
    background-color: #66b;
  }
</style>
```

Template has a few tricks:

```html
<div class={panel: true, active: input.active}>
  <header>${state.directory.split("/").slice(-1)[0]}</header>
  <div class="file-list">
    <for|file,idx| of=state.files>
      <div
        class={
          file: "file",
          focused: (idx === state.focusedIdx),
          selected: state.selected.includes(idx),
        }
        on-click("click", idx)
        on-contextmenu("rightclick", idx)
        on-dblclick("dblclick", idx)
        >${file.name}
      </div>
    </for>
  </div>
</div>
```

Marko has similar shortcut for setting multiple classes as Vue - `class={class1: condition1, class2: condition2, ...}`. I think Svelte's `class:class1=condition1` is a bit more readable, but it's perfectly fine either way.

`<for|file,idx| of=state.files>` is Marko version of a loop. Every framework has some sort of loops, and some sort of ifs, with its unique syntax. All do basically the same thing.

Template refers to two objects - `state` and `input`. `state` is the state of the component (`this.state`).

`input` is component's props as they currently are, and this is strangely not available in the class, and there's no reactive way to do things based on props changing! We'd need to write `onInput` lifecycle method, and do all the logic there. I find this much more complicated than Svelte's or React's system.

Let's get to the class. It starts with `onCreate` setting up initial state:

```javascript
class {
  onCreate(input) {
    this.state = {
      directory: input.initial,
      id: input.id,
      files: [],
      focusedIdx: 0,
      selected: [],
    }
  }
  ...
}
```

It's important to know that this `input` is the props as they were when the component was created. It's not going to be called again when `active` prop changes. We can either use `onInput` to react to props changes, or we can use `input.active` in the template - where it always corresponds to the latest value. I find it very non-intuitive.

And as mentioned before, we don't have access to `window` in `onCreate`.

Once component mounts, we can ask Electron (more specifically our preload) for list of files in the directory:

```javascript
  onMount() {
    this.fetchFiles()
  }
  fetchFiles() {
    let filesPromise = window.api.directoryContents(this.state.directory)
    filesPromise.then(x => {
      this.state.files = x
    })
  }
```

We'd like to make this reactive like in Svelte `$:` (or like React would do with `useEffect`). Doesn't seem like we can, we need to call `fetchFiles` manually every time `this.state.directory` changes.

Now the event handlers. Various kinds of mouse clicks change `this.state.focusedIdx` to the clicked file's index, emit custom `activate` event to the parent, and then do some specific action based on left, right, or double click.

```javascript
  click(idx) {
    this.emit("activate")
    this.state.focusedIdx = idx
  }
  rightclick(idx) {
    this.emit("activate")
    this.state.focusedIdx = idx
    this.flipSelected(idx)
  }
  dblclick(idx) {
    this.emit("activate")
    this.state.focusedIdx = idx
    this.enter()
  }
}
```

Right click flips selection:

```javascript
  flipSelected(idx) {
    if (this.state.selected.includes(idx)) {
      this.state.selected = this.state.selected.filter(f => f !== idx)
    } else {
      this.state.selected = [...this.state.selected, idx]
    }
  }
```

And double click enters the clicked file if it's a directory. As we can't make this reactive, we need to call `fetchFiles` manually here.

```javascript
  enter() {
    let focused = this.state.files[this.state.focusedIdx]
    if (focused?.type === "directory") {
      if (focused.name === "..") {
        this.state.directory = this.state.directory.split("/").slice(0, -1).join("/") || "/"
      } else {
        this.state.directory += "/" + focused.name
      }
      this.fetchFiles()
    }
  }
```

### First Impressions of Marko

Overall I haven't been very impressed. I despise boilerplate (and that's why there will be zero TypeScript in this series), so I can definitely appreciate Marko's concise syntax.

On the other hand, we ran into a lot of cases where we had to explicitly handle updates while Svelte's (or even React Hooks, just with more explicit dependency list) reactivity would do it for us.

There were also issues one might expect from a less popular framework. VSCode Marko plugin was fairly bad - it couldn't guess how to comment out code due to Marko's complex syntax, so it would put try `<!-- -->` in Javascript section, and getting syntax error. Error messages were very confusing, and often I had to reset `npm run dev` after fixing syntax error, as it would strangely not pick up that file changed when I reloaded the page. Documentation on the website was very poor, and googling answers was not very helpful.

Marko's website features [Marko vs React section](https://markojs.com/docs/marko-vs-react/), which is fair enough, as React is the most popular framework of previous generation, but it compares it with fairly old style of React - hooks style React tends to cut on boilerplate a good deal with small components like that.

It also doesn't really try to compare with current generation frameworks like Svelte or Imba. I don't think comparison would go too well.

### Result

Here's the results:

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

In the next episodes, we'll be back to improving our Svelte version.

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