Electron Adventures: Episode 51: Deleting Files
Tags: javascript, electron, svelte
Published: 2021-09-14 03:13:37

The next operation we need to implement is deleting files - or more accurately moving files to trash, as no file manager in 2021 should actually hard delete files.

### `src/commands.js`

As usual we start by adding a new command to the `commands` list:

```javascript
    {
      name: "Delete Files",
      shortcuts: [{key: "F8"}],
      action: ["activePanel", "deleteFiles"],
    },
```

### `src/Panel.svelte`

We need two things from the active `Panel` - its currently active directory, and which files we should be deleting.

There are three possibilities. We'll have very similar logic for copying files, moving files, and a lot of other operations, so we should refactor this at some point:

* is any files are selected, operate on those
* if no files are selected, operate on currently focused file
* unless the currently focused one is `..`, then do nothing

```javascript
  function deleteFiles() {
    if (selected.length) {
      app.openDeleteDialog(directory, selected.map(idx => files[idx].name))
    } else if (focused && focused.name !== "..") {
      app.openDeleteDialog(directory, [focused.name])
    }
  }
```

### `src/App.svelte`

This is our third dialog, and `App` has far too many responsibilities to also bother with rendering every possible dialog. For now let's refactor dialog opening code to just this:

```javascript
  function openPalette() {
    dialog = {type: "CommandPalette"}
  }
  function openMkdirDialog(base) {
    dialog = {type: "MkdirDialog", base}
  }
  function openDeleteDialog(base, files) {
    dialog = {type: "DeleteDialog", base, files}
  }
```

But maybe we should just have one `openDialog` function, and pass that hash there directly? It's something to consider.

If we continued the template how we had it before it would be:

```html
{#if dialog}
  {#if dialog.type === "CommandPalette"}
    <CommandPalette />
  {:else if dialog.type === "MkdirDialog"}
    <MkdirDialog base={dialog.base} />
  {:else if dialog.type === "DeleteDialog"}
    <DeleteDialog base={dialog.base} files={dialog.files} />
  {/if}
{/if}
```

Let's simplify this to:

```html
{#if dialog}
  <Dialog {...dialog} />
{/if}
```

### `src/Dialog.svelte`

But we don't want to just move that ever growing if/else chain into another file. Let's use some metaprogramming to simplify this.

```html
<script>
  import CommandPalette from "./CommandPalette.svelte"
  import DeleteDialog from "./DeleteDialog.svelte"
  import MkdirDialog from "./MkdirDialog.svelte"

  let {type, ...otherProps} = $$props

  let component = {CommandPalette, MkdirDialog, DeleteDialog}
</script>

<div>
  <svelte:component this={component[type]} {...otherProps}/>
</div>

<style>
  div {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    margin: auto;
    padding: 8px;
    max-width: 50vw;
    background: #338;
    box-shadow: 0px 0px 24px #004;
  }
</style>
```

Svelte normally passes props into individual variables, but you can also access the whole set with `$$props`. We do some destructuring to extract `type` and get the rest of the props into `otherProps`.

Then with `<svelte:component this={component[type]} {...otherProps}/>` we tell Svelte to pick the right component, and pass whatever the rest of the props are.

If you somehow mess up the prop list, you'll get a console warning in development mode, but this is the power of dynamic typing. It Just Works, without pages of mindless boilerplate.

Since code for placing the dialog in the right place is already in `Dialog`, we can remove it from `CommandPalette`, and `MkdirDialog`.

### Moving files to trash

Moving files to trash is something pretty much every operating system made in the last half century supported (even the ancient MS DOS had rudimentary functionality of this kind), but bafflingly most programming languages including node have no support for it at all!

We'll be using [`trash` package](https://www.npmjs.com/package/trash) to do this.

So we need to install it with `npm i trash`.

### `src/DeleteDialog.svelte`

The dialog is very similar to the `MkdirDialog` dialogs.

The main difference is that now the submit action is async, and quite slow as it needs to launch an external program to actually move the files to the trash, so it's quite slow. It really asks for some kind of feedback that deletion is in progress, and of course error handling. We'll get there of course.

It also feels like we should probably move that button bar to another component, as it's nearly exact copypasta of the ones in `MkdirDialog`.

The dialog is a huge improvement over most file managers in that it tells you excatly what it will be deleting. The absolute worst dialogs are: "Are you sure? OK / Cancel". Dialogs "Are you sure you want to delete files? Delete / Cancel" are a bit better. But really we should be very exact, especially with such potentially dangerous actions. Unfortunately what it doesn't handle quite as well is situations where list of files would be too long. We'll get there as well.

```html
<script>
  export let base
  export let files

  import path from "path-browserify"
  import { getContext } from "svelte"

  let { eventBus } = getContext("app")
  let app = eventBus.target("app")
  let bothPanels = eventBus.target("bothPanels")

  async function submit() {
    for (let file of files) {
      let fullPath = path.join(base, file)
      await window.api.moveFileToTrash(fullPath)
    }
    app.closeDialog()
    bothPanels.refresh()
  }
  function focus(el) {
    el.focus()
  }
</script>

<form on:submit|preventDefault={submit}>
  <div>Do you want to delete the following files in {base}:</div>
  <ul>
    {#each files as file}
      <li>{file}</li>
    {/each}
  </ul>
  <div class="buttons">
    <button type="submit" use:focus>Delete</button>
    <button on:click={app.closeDialog}>Cancel</button>
  </div>
</form>

<style>
  .buttons {
    display: flex;
    flex-direction: row-reverse;
    margin-top: 8px;
    gap: 8px;
  }

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

### `preload.js`

And finally we need to expose the relevant method in the preload:

```javascript
let trash = require("trash")

let moveFileToTrash = async (file) => {
  await trash(file)
}
```

It's an interesting question if backend or frontend should be doing the looping. In this case backend looping would have a lot better performance, but it would be significantly more difficult to accurately report errors.

### Result

Here's the results:

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

In the next episode, we'll add support for some error messages.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-51-deleting-files).