Quick Look feature for a Web App
Tags: javascript, svelte, webdev
Published: 2022-09-16 12:03:29

## Introduction
Hi Dev.to community. My name is Askarbek. I'm a frontend developer. I wanted to share my process of building one component I developed recently as a small side project and see if I can get some ideas.

## Short description
One of my favorite features of MacOS is the Quick Look. and I'm sure it could be very useful for a Business Application.

## What is Quick Look and why it's so good?
It's when you're browsing a folder and press Spacebar it shows the content of the current file. It can be a picture, video or a text file. The good thing is that you can select next or previous file by pressing arrow buttons without closing the Quick Look. And when you eventually close Quick Look by pressing Spacebar again the selected file would be the last viewed file.

## Business Application
By Business Application I mean applications like CRM, ERP etc. Those Apps can be very complex and they do solve complex problems but, it all comes down to two tools: Table and Form views. A Table view lists some entities and a Form view shows details of a single entity. The Form view also allows a user to edit the current entity.

## Use Case
When you open a Table view, you can select a line then press Spacebar that would open a Form view and then keep navigating through the table lines by pressing up/down arrows without closing the Form view.
As an additional features a user should be also able to:
* Edit the data
* Save or discard changes
* Change the status (if applicable)

So let's build a very simple Business App that implements the use case.

## Project stack
I'm going to use Svelte, Tailwind and Vite as my frontend stack. I'm not gonna need a backend for this project since I am not building a "full-blown app", it's just a POC so I'll just use a `json` file as a datasource.

## Hotkeys
Table and Form view components will be made mostly with html and css, nothing complex. The main feature we'll be focusing on is the hotkeys. Here is the list of initial hotkeys we'll implement:

* **Spacebar**: Open/Close View form (read mode by default)
* **Enter**: Switch to edit mode of the View form 
* **Ctrl + Enter**: Save changes and switch back to read mode of the View form
* **Esc**: Discard changes and switch back to read mode
* **N**: Open an empty View form to create a new item
* **S**: Change the status of the current item

So I should explain the idea of read/edit mode. When we open a View form it should display all the details of the currently selected table line (entity) as a preview (readonly). That way we can guarantee that all other hotkeys like: up/down arrows and Spacebar will keep working and made sense. If a View form would have some `<input>` then a user by typing a space in it could close the Form since Spacebar is a hotkey for open/close. Of course we could override the hotkey while input is focused but IMO it would overcomplicate the UX. Two modes of a Form (read/write) will strictly divide the UX, since in edit mode the hotkeys mentioned will stop working until we switch back to read mode by saving or discarding changes.

## Code

We'll create a separate component called `HotkeyDispatcher` that will handle the hotkey logic and fire appropriate events to it's parent component. The `HotkeyDispatcher` will not have any layout. 

To handle hotkeys we need a global dispatcher to add event listeners to, so we'll use `svelte:window` element for that.

```typescript
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} />
```

I'll create a boolean variable for each hotkey and will set it to `true` or `false` depending on which key is pressed right now. We should also prevent default behavior of the browser so it won't interfere with our logic. Also browser's default behavior is the reason why I'm using keydown instead of keypress.

```typescript
const onKeyDown = (e: KeyboardEvent) => {
  if (isOpen) {
    if (isEditMode) {
      onCtrlEnter = e.code === 'Enter' && e.ctrlKey && !e.shiftKey;
      onEscape = e.code === 'Escape';
    } else {
      if (e.code === 'Space' || e.code === 'Enter' || e.code === 'ArrowDown' || e.code === 'ArrowUp')
        e.preventDefault();
      onArrowDown = e.code === 'ArrowDown';
      onArrowUp = e.code === 'ArrowUp';
      onSpace = e.code === 'Space';
      onEscape = e.code === 'Escape';
};

const onKeyUp = (e: KeyboardEvent) => {
  onArrowDown = false;
  onArrowUp = false;
  onSpace = false;
  onEscape = false;
  onEnter = false;
  onCtrlEnter = false;
};
```

Depending on which key is pressed we dispatch a corresponding event to a parent component.

```typescript
const SpaceHandler = () => {
  isOpen = !isOpen;
  if (isOpen) {
    dispatch('Open');
  } else {
    dispatch('Close');
  }
};

$: onSpace && SpaceHandler();
```

```typescript
const EscapeHandler = () => {
  if (isEditMode) {
    isEditMode = false;
    dispatch('Discard');
    dispatch('ViewMode');
  } else {
    isOpen = false;
    dispatch('Close');
  }
};

$: onEscape && EscapeHandler();
```

Now we can drop in our `HotkeyDispatcher` into a Page that has all the html and css we need. In our case the Page has two components `DataTable` and `FormView`. We open or close `FormView` and switch its modes as a response to `HotkeyDispatcher`'s events.

```typescript
<script lang="ts">
  const OnNextRow = () => {
    if (selectedRow < rows.length - 1) selectedRow++;
  };

  const OnPrevRow = () => {
    if (selectedRow > 0) selectedRow--;
  };

  const OnOpen = () => {
    showForm = true;
  };

  const OnClose = () => {
    showForm = false;
  };

  const OnEditMode = () => {
    editMode = true;
  };

  const OnViewMode = () => {
    editMode = false;
  };
</script>

<HotkeyDispatcher 
  on:NextRow="{OnNextRow}" 
  on:PrevRow="{OnPrevRow}" 
  on:Open="{OnOpen}" 
  on:Close="{OnClose}" 
  on:EditMode="{OnEditMode}"
  on:ViewMode="{OnViewMode}"
  on:Save="{OnSave}"
  on:Discard="{OnDiscard}"
/>

<DataTable bind:selectedRow columns="{columns}" rows="{rows}" fields="{fields}" />

{#if showForm}
  <FormView
    editMode="{editMode}"
    data="{rows[selectedRow]}"
    bind:modifiedData
    fields="{fields}"
    fieldOrder="{fieldOrder}"
    hotkeyTrigger="{trigger}"
  />
{/if}
```

# Result

{% youtube Ssqx4quH30A %}

I didn't include the whole code here otherwise the post would get too big. You can checkout the full source on [GitHub](https://github.com/Ascarbek/svelte-quick-look) any suggestions are welcome!