Electron Adventures: Episode 49: Mkdir Dialog
Tags: javascript, electron, svelte
Published: 2021-09-11 05:03:15

It's time to add our first dialog - one for creating a new directory. But wait, it's not actually our first one, command palette is a dialog too.

So before we do anything let's refactor some code so it supports both dialogs - and more we'll be adding in the future.

### Rename `closePalette` to `closeDialog`

First, in `src/commands.js`, let's replace `palette` context with a single `closePalette` command, just renamed to `dialog` context with a single command `closeDialog`:

```javascript
  dialog: [
    {
      shortcuts: [{key: "Escape"}],
      action: ["app", "closeDialog"],
    }
  ],
```

And let's change calls to `app.closePalette()` in `src/CommandPalette.svelte`, `src/CommandPaletteEntry.svelte` and `src/App.svelte`.

### Start event chain in `src/Panel.svelte`

When the user presses F7, we need to bounce the event around a bit. First we need to send it to the active panel, because that's the component which knows where we're creating that directory at.

So here's another entry for `src/commands.js`:

```javascript
    {
      name: "Create Directory",
      shortcuts: [{key: "F7"}],
      action: ["activePanel", "createDirectory"],
    },
```

And here's its handler in `src/Panel.svelte`:

```javascript
  function createDirectory() {
    app.openMkdirDialog(directory)
  }
```

We don't need to do anything special here, just add current directory to the event and pass it along to the app.

### Continue event chain in `src/App.svelte`

The `App` component used to have `paletteOpen` boolean flag. We need to replace it with `dialog` object.

Here's the relevant functions:

```javascript
  let dialog = null

  $: {
    keyboardMode = "default"
    if (dialog) keyboardMode = "dialog"
    if (preview) keyboardMode = "preview"
  }
  function openPalette() {
    dialog = {type: "palette"}
  }
  function openMkdirDialog(base) {
    dialog = {type: "mkdir", base}
  }
  function closeDialog() {
    dialog = null
  }
```

And we also need to add it to the template:

```html
{#if preview}
  <Preview {...preview} />
{/if}

<div class="ui">
  <header>
    File Manager
  </header>
  <Panel initialDirectory={initialDirectoryLeft} id="left" />
  <Panel initialDirectory={initialDirectoryRight} id="right" />
  <Footer />
</div>

<Keyboard mode={keyboardMode} />

{#if dialog}
  {#if dialog.type === "palette"}
    <CommandPalette />
  {:else if dialog.type === "mkdir"}
    <MkdirDialog base={dialog.base} />
  {/if}
{/if}
```

`CommandPalette`, `MkdirDialog`, and future dialogs we'll be adding share a lot of functionality, so perhaps there should be a `Dialog` component which includes them, and sets them in a proper place.

### `src/MkdirDialog.svelte`

We just need a simple dialog with one input and the usual OK/Cancel buttons. One thing we've learned from the time when Orthodox File Managers were first created is that "OK" buttons should never actually say "OK", they should describe the actual action.

```html
<form on:submit|preventDefault={submit}>
  <label>
    <div>Enter directory name:</div>
    <input use:focus bind:value={dir} placeholder="directory">
  </label>
  <div class="buttons">
    <button type="submit">Create directory</button>
    <button on:click={app.closeDialog}>Cancel</button>
  </div>
</form>
```

Styling is very close to what `CommandPalette` and `Footer` already do:

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

  input {
    font-family: inherit;
    background-color: inherit;
    font-size: inherit;
    font-weight: inherit;
    box-sizing: border-box;
    width: 100%;
    margin: 0;
    background: #66b;
    color: inherit;
  }

  input::placeholder {
    color: inherit;
    font-style: italic;
  }

  .feedback {
    font-style: italic;
  }

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

The dialog does very little - beyond some boilerplate it just needs a `submit` handler. Then if user typed anything, we create a new directory relative to the current directory of the active panel. If user typed nothing, we just close.

```html
<script>
  export let base

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

  let { eventBus } = getContext("app")
  let dir = ""

  let app = eventBus.target("app")

  function submit() {
    app.closeDialog()
    if (dir !== "") {
      let target = path.join(base, dir)
      window.api.createDirectory(target)
    }
  }
  function focus(el) {
    el.focus()
  }
</script>
```

To create new directory we need to add function to `preload.js`

### `preload.js`

It used to be a huge pain to create directories in JavaScript, but node finally added `{recursive: true}` which makes it simple enough:

```javascript
let createDirectory = (dir) => {
  fs.mkdirSync(dir, {recursive: true})
}
```

### Result

Here's the results:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dvlxo7qwe2htm0rroyw.png)

It opens a dialog, lets user type a name, and then creates the directory. So what's missing?

* any kind of error handling - if user tries to create directory in place they have no access to, or operating system returns any other kind of error, we don't let them know in any way
* any kind of instant feedback - and really, we can predict what would happen. If user tries to create `/etc/hack` we could give live feedback below the input saying that `/etc` is read only for them, and such things. This is a wishlist item, which we'll likely not get to in this series, but a polished program would at least give it a try to cover more common scenarios. "It didn't work" messages should be a fallback, not a regular occurrence.
* once we create the directory, it's not actually displayed in the active panel, as it doesn't refresh unless you navigate somewhere

In the next episode, we'll try to deal with that last problem, and refresh panels when necessary, as well as add a manual refresh command.

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