vanillaのTypeScriptでToDoアプリを作成

# はじめに

タイトル通り、vanillaのTypeScriptでシンプルなToDoアプリを作成しました。

昨今、フロントエンドで開発するには基本VueやReact、AngularといったUIフレームワークを用いて開発するのが主流かと思います。
ですので敢えて、そういったUIフレームワークを使わず、TypeScriptとHTMLだけでToDoアプリを作成することで、改めてUIフレームワークのありがたみを知ろうというのが今回実装したきっかけになります。
そしてせっかく作成したのだから見てほしい！　と、本記事を執筆しました。

# 成果物

- [gh pages](https://masayasviel.github.io/vanilla-ts-todo-app/)
- [github](https://github.com/masayasviel/vanilla-ts-todo-app/settings/pages)

# 環境構築

[parcel](https://ja.parceljs.org/)を用います。
parcelはtsconfigとか飛ばして動かせるのでtypescript要らないかもです。

```sh
npm init -y
npm install parcel typescript
npx tsc --init
```

## ディレクトリ構成

```
.
├── package.json
├── src
│   ├── category.ts
│   ├── index.html
│   ├── main.ts
│   ├── parent-category.ts
│   ├── style.css
│   ├── task-form.ts
│   └── todo-item.ts
└── tsconfig.json
```

## 実行

```sh
npx parcel src/index.html --open
```

# HTML・CSS

説明すべきところがあまりなかったので解説かなり薄いです。

## HTML

<details>
<summary>index.html</summary>
<div>

```html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ToDoList</title>
    <link rel="stylesheet" href="style.css">
    <script type="module" src="./todo-item.ts"></script>
    <script type="module" src="./category.ts"></script>
    <script type="module" src="./task-form.ts"></script>
    <script type="module" src="./parent-category.ts"></script>
    <script type="module" src="./main.ts" defer></script>
</head>
<body>
    <header>
        <h1>ToDoリスト</h1>
    </header>
    <main>
        <div class="task-form">
            <div>
                <label for="task-input">タスク</label>
                <input type="text" id="task-input"/>
            </div>
            <button id="add-button">追加</button>
        </div>

        <div class="task-categories">
            <div class="category-box">
                <h2>未着手</h2>
                <div
                    id="not-started-yet"
                    class="item-box"
                >
                </div>
            </div>

            <div class="category-box">
                <h2>進行中</h2>
                <div
                    id="wip"
                    class="item-box"
                >
                </div>
            </div>

            <div class="category-box">
                <h2>完了</h2>
                <div
                    id="finish"
                    class="item-box"
                >
                </div>
            </div>
        </div>
        <dialog id="item-operate-dialog">
            <form class="dialog-form">
                どうする？
                <div class="dialog-operate">
                    <div>
                        <label for="select-new-state">状態:</label>
                        <select id="select-new-state">
                            <option value="not-started-yet">未着手</option>
                            <option value="wip">進行中</option>
                            <option value="finish">完了</option>
                        </select>
                    </div>
                    <div class="dialog-buttons">
                        <button
                                value="cancel"
                                formmethod="dialog"
                        >キャンセル</button>
                        <button id="change-button">変更</button>
                        <button id="remove-button">削除</button>
                    </div>
                </div>
            </form>
        </dialog>
    </main>
</body>
</html>
```

</div></details>

### type="module"

```html
<script type="module" src="./**.ts"></script>
```

[import/export](https://typescriptbook.jp/reference/import-export-require)を使ったらエラーになったのでこういう形で[module](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)で読み込んでいます。

## CSS

<details>
<summary>style.css</summary>
<div>

```css
main {
    margin: 24px;
    padding: 0;
}

.task-form {
    display: flex;
    gap: 8px;
}

.task-categories {
    display: flex;
    justify-content: space-around;
}

.category-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

.item-box {
    width: 25vw;
    height: 560px;
    border: 1px solid black;
}

.dialog-form {
    min-width: 300px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.dialog-operate {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.dialog-buttons {
    display: flex;
    justify-content: space-around;
}
```

</div></details>

最低限見た目を整えましたが、基本はブラウザのデフォルトスタイルまんまです。
見た目を整えるのにはflexboxを多用しています。

- [flexboxチートシート](https://www.webcreatorbox.com/tech/css-flexbox-cheat-sheet)
- [gap](https://developer.mozilla.org/ja/docs/Web/CSS/gap)

# TypeScirpt

依存関係順に紹介します。
DOMの取得周りは存在するもの前提で書いています。

## task-form.ts

タスク入力フォーム回りをまとめたクラスです。

```ts
/**
 * タスク追加ボタン押下時に発火する関数の型
 * @param inputValue 入力値
 */
type RegisterAddTaskButtonClickEventCallbackType = (inputValue: string) => void;

/** タスク登録フォーム */
export class TaskForm {
  /** タスク名入力フォーム */
  private taskInput: HTMLInputElement;
  /** タスク追加ボタン */
  private addTaskButton: HTMLButtonElement;

  constructor() {
    this.taskInput = document.querySelector('#task-input')!
    this.addTaskButton = document.querySelector('#add-button')!
  }

  /** タスク追加ボタン押下イベントで発火する関数を登録 */
  registerAddTaskButtonClickEventFunc(f: RegisterAddTaskButtonClickEventCallbackType): void {
    this.addTaskButton.addEventListener('click', () => {
      f(this.taskInput.value);
    })
  }
}
```

### [JSdoc](https://www.typescriptlang.org/ja/docs/handbook/jsdoc-supported-types.html)

```ts
/**
 * 説明
 * @param param パラメータ
 * @return 返却値
 */
```

### import、export

- [[ES2015] ECMAScriptモジュール](https://jsprimer.net/basic/module/)
- [import、export、require](https://typescriptbook.jp/reference/import-export-require)

```ts
export class ClassName
```

### class

- [jsprimer](https://jsprimer.net/basic/class/)
- [サバイバルTypeScript](https://typescriptbook.jp/reference/object-oriented/class)

```ts
class MyClass {
    constructor() {}
}
```

- [アクセス修飾子](https://typescriptbook.jp/reference/object-oriented/class/access-modifiers)

```ts
private prop: Type;
```

### [document.querySelector](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector)

```ts
element = document.querySelector('%query_selector%')!;
```

[Element型](https://developer.mozilla.org/ja/docs/Web/API/Element)を返します。
Element型を [HTMLInputElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement) や [HTMLButtonElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLButtonElement) に何故代入できるのか？　それは、それらの型がElement型の拡張だからです。

- [クラスの継承](https://typescriptbook.jp/reference/object-oriented/class/class-inheritance)
- [インターフェースの継承](https://typescriptbook.jp/reference/object-oriented/interface/interface-inheritance)

仕様書

- [HTMLInputElement](https://html.spec.whatwg.org/multipage/input.html#htmlinputelement)
- [HTMLButtonElement](https://html.spec.whatwg.org/multipage/form-elements.html#htmlbuttonelement)

からも、[HTMLElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement)を継承していることがわかり、HTMLElementがElementの継承であることも、

> 親である Element からプロパティを継承しています。
> https://developer.mozilla.org/ja/docs/Web/API/HTMLElement

から分かります。

### [addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)

特定のイベントが起きた時に第二引数の関数を実行します。
特定のイベントは第一引数に指定します。

- [イベントリファレンス](https://developer.mozilla.org/ja/docs/Web/Events)

### 関数を引数に受け取る

```ts
registerAddTaskButtonClickEventFunc(f: RegisterAddTaskButtonClickEventCallbackType): void {
    this.addTaskButton.addEventListener('click', () => {
        f(this.taskInput.value);
    })
}
```

関数を引数に受け取り実行できます。
せっかくTypeScriptを使っているのですから、関数の受け取る引数及び返り値の型も縛りたいものです。

- [関数の型の宣言](https://typescriptbook.jp/reference/functions/function-type-declaration)

ここではボタン押下時に発火する関数を引数として受け取り、そこに入力された値を流し込みたいため、

```ts
type RegisterAddTaskButtonClickEventCallbackType = (inputValue: string) => void;
```

と定義されます。

## todo-item.ts

それぞれのタスクについて処理をまとめたクラスです。

```ts
/**
 * アイテムクリック時イベントで発火する関数の型
 * @param event クリック時イベント
 * @param todoItem this
 */
type ClickEvent = (event: MouseEvent, todoItem: TodoItem) => void;
/** カテゴリの種別、およびそれぞれのID */
export type BoxIdLiteral = 'not-started-yet' | 'wip' | 'finish';

/** タスク */
export class TodoItem {
  /** タスクを一意に示すID */
  uuid: string;
  /** 現在所属するグループ */
  currentGroup: BoxIdLiteral = 'not-started-yet';
  /** Node */
  elm: HTMLDivElement | null = null;
  constructor(
    taskName: string,
    clickEvent: ClickEvent
  ) {
    this.uuid = crypto.randomUUID();
    const div = document.createElement('div');
    div.textContent = taskName;
    div.addEventListener(
      'click',
      (event) => clickEvent(event, this)
    );
    this.elm = div;
  }

  /** 自身を削除 */
  remove(): void {
    if (this.elm) {
      this.elm.remove();
      this.elm = null;
    } else {
      throw Error('[TodoItem.remove] this.elm = null');
    }
  }
}
```

### [リテラル型](https://typescriptbook.jp/reference/values-types-variables/literal-types)

```ts
type BoxIdLiteral = 'not-started-yet' | 'wip' | 'finish';
```

これと似た使い方をするのに[enums](https://typescriptbook.jp/reference/values-types-variables/enum/enum-problems-and-alternatives-to-enums)を用いることもあります。

### [uuid](https://developer.mozilla.org/ja/docs/Glossary/UUID)

どうせ被んないだろという楽観的考えのもと、重複判定を行なっておりません。
[Crypto.randomUUID](https://developer.mozilla.org/ja/docs/Web/API/Crypto/randomUUID)で生成できます。
[Crypto](https://developer.mozilla.org/ja/docs/Web/API/Crypto)は[Web API](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs)なのでJavaScriptおよびTypeScriptの機能ではありません。

重複判定を行うとしたら、現在持つUUIDを[Set](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set)あたりを使って管理するのが良さそうですね。

### [document.createElement](https://developer.mozilla.org/ja/docs/Web/API/Document/createElement)

HTML要素を生成します。
第一引数で与えた文字列に応じた型で生成してくれます。

```ts
const div = document.createElement('div');
```

例えば上記の例なら、[HTMLDivElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLDivElement)で生成してくれます。

なぜこのように柔軟になっているのでしょうか？
型定義を覗いてみます。

```ts
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
```

- [keyof](https://typescriptbook.jp/reference/type-reuse/keyof-type-operator)
- [typeof](https://typescriptbook.jp/reference/values-types-variables/typeof-operator)
- [generics](https://typescriptbook.jp/reference/generics)

<details>
<summary>HTMLElementTagNameMap</summary>
<div>

```ts
interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "area": HTMLAreaElement;
    "article": HTMLElement;
    "aside": HTMLElement;
    "audio": HTMLAudioElement;
    "b": HTMLElement;
    "base": HTMLBaseElement;
    "bdi": HTMLElement;
    "bdo": HTMLElement;
    "blockquote": HTMLQuoteElement;
    "body": HTMLBodyElement;
    "br": HTMLBRElement;
    "button": HTMLButtonElement;
    "canvas": HTMLCanvasElement;
    "caption": HTMLTableCaptionElement;
    "cite": HTMLElement;
    "code": HTMLElement;
    "col": HTMLTableColElement;
    "colgroup": HTMLTableColElement;
    "data": HTMLDataElement;
    "datalist": HTMLDataListElement;
    "dd": HTMLElement;
    "del": HTMLModElement;
    "details": HTMLDetailsElement;
    "dfn": HTMLElement;
    "dialog": HTMLDialogElement;
    "div": HTMLDivElement;
    "dl": HTMLDListElement;
    "dt": HTMLElement;
    "em": HTMLElement;
    "embed": HTMLEmbedElement;
    "fieldset": HTMLFieldSetElement;
    "figcaption": HTMLElement;
    "figure": HTMLElement;
    "footer": HTMLElement;
    "form": HTMLFormElement;
    "h1": HTMLHeadingElement;
    "h2": HTMLHeadingElement;
    "h3": HTMLHeadingElement;
    "h4": HTMLHeadingElement;
    "h5": HTMLHeadingElement;
    "h6": HTMLHeadingElement;
    "head": HTMLHeadElement;
    "header": HTMLElement;
    "hgroup": HTMLElement;
    "hr": HTMLHRElement;
    "html": HTMLHtmlElement;
    "i": HTMLElement;
    "iframe": HTMLIFrameElement;
    "img": HTMLImageElement;
    "input": HTMLInputElement;
    "ins": HTMLModElement;
    "kbd": HTMLElement;
    "label": HTMLLabelElement;
    "legend": HTMLLegendElement;
    "li": HTMLLIElement;
    "link": HTMLLinkElement;
    "main": HTMLElement;
    "map": HTMLMapElement;
    "mark": HTMLElement;
    "menu": HTMLMenuElement;
    "meta": HTMLMetaElement;
    "meter": HTMLMeterElement;
    "nav": HTMLElement;
    "noscript": HTMLElement;
    "object": HTMLObjectElement;
    "ol": HTMLOListElement;
    "optgroup": HTMLOptGroupElement;
    "option": HTMLOptionElement;
    "output": HTMLOutputElement;
    "p": HTMLParagraphElement;
    "picture": HTMLPictureElement;
    "pre": HTMLPreElement;
    "progress": HTMLProgressElement;
    "q": HTMLQuoteElement;
    "rp": HTMLElement;
    "rt": HTMLElement;
    "ruby": HTMLElement;
    "s": HTMLElement;
    "samp": HTMLElement;
    "script": HTMLScriptElement;
    "search": HTMLElement;
    "section": HTMLElement;
    "select": HTMLSelectElement;
    "slot": HTMLSlotElement;
    "small": HTMLElement;
    "source": HTMLSourceElement;
    "span": HTMLSpanElement;
    "strong": HTMLElement;
    "style": HTMLStyleElement;
    "sub": HTMLElement;
    "summary": HTMLElement;
    "sup": HTMLElement;
    "table": HTMLTableElement;
    "tbody": HTMLTableSectionElement;
    "td": HTMLTableCellElement;
    "template": HTMLTemplateElement;
    "textarea": HTMLTextAreaElement;
    "tfoot": HTMLTableSectionElement;
    "th": HTMLTableCellElement;
    "thead": HTMLTableSectionElement;
    "time": HTMLTimeElement;
    "title": HTMLTitleElement;
    "tr": HTMLTableRowElement;
    "track": HTMLTrackElement;
    "u": HTMLElement;
    "ul": HTMLUListElement;
    "var": HTMLElement;
    "video": HTMLVideoElement;
    "wbr": HTMLElement;
}
```

</div></details>

なるほど。対応する型をマッピングしていました。

### [textContent](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent)と[innerHTML](https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)

「textContent　innerHTML」みたいに検索をかければ山ほど違いに関する記述が出てくるため検索してください。
textContentを用いている理由は[XSS](https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html)を防ぐためです。

### 自身を削除

```ts
remove(): void {
  if (this.elm) {
    this.elm.remove();
    this.elm = null;
  } else {
    throw Error('[TodoItem.remove] this.elm = null');
  }
}
```

削除時に[Element.remove](https://developer.mozilla.org/ja/docs/Web/API/Element/remove)でエレメントを消してあげます。
そして明示的に `this.elm` をnullにします。
今回は処理の流れ的に `this.elm = null` でremoveが叩かれることはありえないためエラーを発生させています。

- [throw](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw)

## category.ts

タスクの状態(未着手か、進行中か、完了か)のそれぞれのカテゴリについてのクラスです。

```ts
import { TodoItem, BoxIdLiteral } from './todo-item';

/** カテゴリ */
export class Category {
  /** 所属するタスク一覧 */
  children: TodoItem[] = [];
  /** Node */
  private categoryBox: HTMLDivElement;

  /**
   * @param boxId カテゴリ種別
   */
  constructor(private readonly boxId: BoxIdLiteral) {
    this.categoryBox = document.querySelector(`#${boxId}`)!;
  }

  /**
   * タスクの追加
   * @param item タスク
   */
  appendTodo(item: TodoItem): void {
    if (item.elm) {
      item.currentGroup = this.boxId;
      this.children.push(item);
      this.refreshCategoryBox();
    } else {
      throw Error('[Category.appendTodo] item.elm = null');
    }
  }

  /**
   * タスクをカテゴリから削除
   * @param itemUuid タスクのUUID
   */
  deleteTodo(itemUuid: string): void {
    this.children = this.children.filter((child) => child.uuid !== itemUuid);
    this.refreshCategoryBox();
  }

  /** 画面表示のリフレッシュ */
  private refreshCategoryBox(): void {
    while (this.categoryBox.firstChild) {
      this.categoryBox.removeChild(this.categoryBox.firstChild);
    }
    this.children.forEach((child) => {
      if (child.elm) {
        this.categoryBox.appendChild(child.elm);
      }
    })
  }
}
```

### モジュール

```ts
import { TodoItem, BoxIdLiteral } from './todo-item';
```

- [import、export、require](https://typescriptbook.jp/reference/import-export-require)
- [JavaScript モジュール](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)

### インスタンス

```ts
children: TodoItem[] = [];
```

`class TodoItem` のインスタンスの配列型です。

```ts
new TodoItem(/** param */)
```

でインスタンスを作成して、それを格納します。

classやインスタンスが何かは下記を読んだりご自身で調べるなどしてください。

- [MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes)
- [jsprimer](https://jsprimer.net/basic/class/)
- [サバイバルTypeScript](https://typescriptbook.jp/reference/object-oriented/class)

また、余談ですがどのクラスのインスタンスかを確認するには[Object.prototype.constructor](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor)で確認できます。

```ts
>> class A {
    constructor(a) {
        this.a = a;
    }
}
>> const ac = new A('a');
>> ac.constructor
class A {
    constructor(a) {
        this.a = a;
    }
}
```

そして、そのインスタンスがあるクラスのインスタンスかを判別するには[instanceof](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof)を用います。

```ts
>> ac instanceof A
true
```

さらに余談ですが、[JavaScriptのArray](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array)はObjectです。

```ts
>> typeof ['a', 'b', 'b']
'object'
```

なので配列かを確認したい場合はinstanceofやconstructorをみるか、[isArray](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)を用います。

```ts
>> ['a', 'b', 'c'].constructor
ƒ Array() { [native code] }
>> ['a', 'b', 'c'] instanceof Array
true
>> Array.isArray(['a', 'b', 'c'])
true
>> Array.isArray({a: 'aaa', b: 'bbb'})
false
```

配列はオブジェクトというのは文字だけで見ると違和感を覚えます(Objectがルートクラスな言語も多いのでやっぱ違和感覚えないかもです)が、挙動を見るとキーが連番の数字のオブジェクトだと納得できます。
振る舞いを見てみましょう。

```ts
>> {...['a', 'b', 'c']}
{0: 'a', 1: 'b', 2: 'c'}
>> const a = ['a', 'b', 'c'];
>> delete a[1]
true
>> a
['a', empty, 'c']
>> a[1]
undefined
>> {...a}
{0: 'a', 2: 'c'}
```

- [配列はオブジェクト](https://typescriptbook.jp/reference/values-types-variables/array/array-as-a-object)
- [スプレッド演算子](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)
- [delete](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/delete)

### [constructor shorthand](https://typescriptbook.jp/reference/object-oriented/class/constructor-shorthand)

```ts
constructor(private readonly boxId: BoxIdLiteral) {}
```

### 画面表示のリフレッシュ

カテゴリ内の要素を一旦全部消して、最新の状態で作り直しています。

```ts
private refreshCategoryBox(): void {
  while (this.categoryBox.firstChild) {
    this.categoryBox.removeChild(this.categoryBox.firstChild);
  }
  this.children.forEach((child) => {
    if (child.elm) {
      this.categoryBox.appendChild(child.elm);
    }
  });
}
```

[removeChild](https://developer.mozilla.org/ja/docs/Web/API/Node/removeChild)で対象子要素の削除を[while](https://jsprimer.net/basic/loop/#while-statement)で[firstChild](https://developer.mozilla.org/ja/docs/Web/API/Node/firstChild)が取れる限りぶん回して要素内の子要素を全て削除しています。

ちなみに、このfirstChildはCSSにも擬似クラスとして存在します。

- [:first-child](https://developer.mozilla.org/ja/docs/Web/CSS/:first-child)

そして、 `this.children.forEach` で現在の子要素を取得し、[appendChild](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)で詰め直しています。
`child.elm` はnullの可能性があるため、[型ガード](https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard)を挟んでいます。

要素の洗い替えは重たい処理のため、該当箇所の追加・削除のみにすべきですが、面倒だったので全て洗い替えしています。

## parent-category.ts

カテゴリ全体を統率する親カテゴリのクラスです。

```ts
import { Category } from './category';
import { BoxIdLiteral, TodoItem } from './todo-item';

/** カテゴリ全体を統括 */
export class ParentCategory {
  /** ダイアログ */
  private dialog: HTMLDialogElement;
  /** [ダイアログ]セレクトボックス */
  private selectInput: HTMLSelectElement;
  /** [ダイアログ]変更ボタン */
  private changeButton: HTMLButtonElement;
  /** [ダイアログ]削除ボタン */
  private removeButton: HTMLButtonElement;
  /** 未着手カテゴリ */
  private notStartedYetCategory: Category;
  /** 進行中カテゴリ */
  private wipCategory: Category;
  /** 完了カテゴリ */
  private finishCategory: Category;

  /** 現在選択中タスク */
  private currentSelectItem: TodoItem | null = null;

  constructor() {
    this.dialog = document.querySelector('#item-operate-dialog')!;
    this.selectInput = document.querySelector('#select-new-state')!;
    this.changeButton = document.querySelector('#change-button')!;
    this.removeButton = document.querySelector('#remove-button')!;
    this.notStartedYetCategory = new Category('not-started-yet');
    this.wipCategory = new Category('wip');
    this.finishCategory = new Category('finish');

    this.dialog.addEventListener('close', () => {
      if (this.currentSelectItem === null) {
        throw Error('[ParentCategory.constructor] this.currentSelectItem = null');
      }

      switch (this.dialog.returnValue) {
        case 'not-started-yet':
        case 'wip':
        case 'finish':
          this.moveItem(this.dialog.returnValue);
          break;
        case 'remove':
          this.deleteItem();
          break;
        default:
          return;
      }

      this.currentSelectItem = null;
    });

    this.changeButton.addEventListener('click', (event) => {
      event.preventDefault();
      this.dialog.close(this.selectInput.value);
    });

    this.removeButton.addEventListener('click', (event) => {
      event.preventDefault();
      this.dialog.close('remove');
    });
  }

  /**
   * 新アイテム追加
   * @param taskName タスク名
   * @note 新アイテムの追加は仕様上未着手にしか入らない
   */
  appendNewTask(taskName: string): void {
    const todoItem = new TodoItem(
      taskName,
      (_event, todoItem) => {
        this.currentSelectItem = todoItem;
        this.selectInput.value = todoItem.currentGroup;
        this.dialog.showModal();
      }
    );
    this.notStartedYetCategory.appendTodo(todoItem);
  }

  /**
   * アイテムの移動
   * @param category 移動先カテゴリ
   */
  private moveItem(category: BoxIdLiteral): void {
    if (this.currentSelectItem === null) {
      throw Error('[ParentCategory.moveItem] this.currentSelectItem = null');
    }

    this.removeItemInCategory();

    switch (category) {
      case 'not-started-yet':
        this.notStartedYetCategory.appendTodo(this.currentSelectItem);
        break;
      case 'wip':
        this.wipCategory.appendTodo(this.currentSelectItem);
        break;
      case 'finish':
        this.finishCategory.appendTodo(this.currentSelectItem);
        break;
    }

    this.currentSelectItem = null;
  }

  /** タスクの削除 */
  private deleteItem(): void {
    if (this.currentSelectItem === null) {
      throw Error('[ParentCategory.deleteItem] this.currentSelectItem = null');
    }

    this.currentSelectItem.remove();
    this.removeItemInCategory();
    this.currentSelectItem = null;
  }

  /** カテゴリからタスクを取り除く */
  private removeItemInCategory(): void {
    if (this.currentSelectItem === null) {
      throw Error('[ParentCategory.removeItemInCategory] this.currentSelectItem = null');
    }

    switch (this.currentSelectItem.currentGroup) {
      case 'not-started-yet':
        this.notStartedYetCategory.deleteTodo(this.currentSelectItem.uuid);
        break;
      case 'wip':
        this.wipCategory.deleteTodo(this.currentSelectItem.uuid);
        break;
      case 'finish':
        this.finishCategory.deleteTodo(this.currentSelectItem.uuid);
        break;
    }
  }
}
```

### ダイアログ

```ts
this.dialog.addEventListener('close', () => {...});

this.changeButton.addEventListener('click', (event) => {
    event.preventDefault();
    this.dialog.close(this.selectInput.value);
});

this.removeButton.addEventListener('click', (event) => {
    event.preventDefault();
    this.dialog.close('remove');
});
```

[ダイアログ](https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog)からほぼまるパクしてます。

- [HTMLDialogElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLDialogElement)
- [Event: preventDefault](https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault)

削除もキャンセル同様

```html
<button value="remove" formmethod="dialog">削除</button>
```

としても良かったかもしれません。

### [switch-case](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/switch)

```ts
case 'not-started-yet':
case 'wip':
case 'finish':
    this.moveItem(this.dialog.returnValue);
    break;
```

`moveItem` は引数に `'not-started-yet' | 'wip' | 'finish'` を取ります。
caseはbreakしなければ続けて下の処理も行います。
これを利用して、

```ts
case 'not-started-yet':
case 'wip':
case 'finish':
```

とすることで、this.dialog.returnValueは `'not-started-yet' | 'wip' | 'finish'` のいずれかとなり、下記のように似たコードを書かずに済みます。

```ts
case 'not-started-yet':
    this.moveItem('not-started-yet');
    break;
case 'wip':
    this.moveItem('wip');
    break;
case 'finish':
    this.moveItem('finish');
    break;
```

### アイテムの移動

カテゴリから削除し、選択された移住先カテゴリに追加しています。

```ts
// カテゴリから削除
this.removeItemInCategory();

// 選択された移住先カテゴリに追加
switch (category) {
  case 'not-started-yet':
    this.notStartedYetCategory.appendTodo(this.currentSelectItem);
    break;
  case 'wip':
    this.wipCategory.appendTodo(this.currentSelectItem);
    break;
  case 'finish':
    this.finishCategory.appendTodo(this.currentSelectItem);
    break;
}
```

### タスクの削除

カテゴリからの削除だけでなく、タスクの削除もしています。

```ts
this.currentSelectItem.remove();
this.removeItemInCategory();
```

## main.ts

メイン処理。

```ts
import { TaskForm } from './task-form';
import { ParentCategory } from './parent-category';

function main(): void {
  const taskForm = new TaskForm();
  const parentCategory = new ParentCategory();
  taskForm.registerAddTaskButtonClickEventFunc((inputValue) => {
    parentCategory.appendNewTask(inputValue);
  });
}

window.addEventListener('load', main);
```

フォームと親カテゴリの実態を作って、結合してあげて終了です。
お疲れ様でした！

# 終わりに

実際にやってみて、
HTMLやCSSを書いているときはコンポーネントとして分割し、スコープを小さくしたい。
TypeScriptを書いているときはpropsやemitが欲しくなることが多々ありました。
また、イベント登録もテンプレート側に書いて、関数を紐づけるだけなのもUIフレームワークのいいところですね。([buttonエレメント](https://developer.mozilla.org/ja/docs/Web/HTML/Element/button)にも `onClick` が生えていますが)
それとUIフレームワークだけでなく、大体UIフレームワークに紐づくUIライブラリも使いながら開発することのほうが多いので、自分で頑張ってCSSを書くのも久々の経験でした。
ほんと感謝ですね。

ただ、実際Vanilla TSで書いてみて、データの受け渡しやクラスの切り分け方は改めて勉強になりました。昨今のUIフレームワークへの解像度も上がったと思います。

ここまで読んで頂きありがとうございました。
終わります。