hyperappを理解するためにTODOアプリを作成しました

`Hyperapp`を学ぶ（フロントエンドに慣れる）ために定番のTODOアプリを作りました。
`react`や`angular`のチュートリアルに触れたことがある程度の経験の僕でも結構あっさり作れて楽しいです。
![todo.gif](https://qiita-image-store.s3.amazonaws.com/0/195024/c0411da9-4336-0c6e-d8b6-6a8c7af9fdc2.gif)

開発環境は[この記事](https://qiita.com/sanonosa/items/b33a1ac69a738e0750c8)と同じです。

## `state`

```typescript:./src/state/todo.ts
export interface ITodoItem {
  id: string;
  done: boolean;
  text: string;
}

export interface ITodoState {
  inputText: string;
  todos: ITodoItem[];
}

export const todoState: ITodoState = {
  inputText: "",
  todos: [
    /* ダミーのTodoを格納しておきます */
    { id: "1", done: false, text: "dummy todo 1" },
    { id: "2", done: false, text: "dummy todo 2" },
    { id: "3", done: false, text: "dummy todo 3" }
  ]
};
```
- `ITodoItem`：個々のTODO
    - `id`：個々のTODOを区別する一意なID
    - `done`：作業済みかどうか
    -  `text`：内容です
- `inputText`：現在フォームに入力中の値です。
- `todos`：TODO一覧です。簡略化のためDBやローカルストレージは使わずメモリ上に保持します。

## `actions`

```ts:./src/actions/todo.ts
import { ActionsType } from "hyperapp";
import { ITodoState } from "../states/todo";

const uniqueId: () => string = () =>
  Math.random()
    .toString(34)
    .slice(2);

export interface ITodoActions {
  onInput: (e: Event) => ITodoState;
  add: () => ITodoState;
  toggle: (id: string) => ITodoState;
  delete: () => ITodoState;
}

export const todoActions: ActionsType<ITodoState, ITodoActions> = {
  onInput: (e: Event) => (state): ITodoState => {
    const target: EventTarget | null = e.target;
    if (target && target instanceof HTMLInputElement) {
      return {
        inputText: target.value,
        todos: state.todos
      };
    } else {
      return state;
    }
  },
  add: () => (state): ITodoState =>
    state.inputText
      ? {
          inputText: "",
          todos: [
            ...state.todos,
            {
              id: uniqueId(),
              done: false,
              text: state.inputText
            }
          ]
        }
      : state,
  toggle: (id: string) => (state): ITodoState => ({
    inputText: state.inputText,
    todos: state.todos.map(t => ({
      id: t.id,
      done: t.id === id ? !t.done : t.done,
      text: t.text
    }))
  }),
  delete: () => (state): ITodoState => ({
    inputText: state.inputText,
    todos: state.todos.filter(t => !t.done)
  })
};
```

TypeScriptで戻り値の型を明示したおかげで個々のactionが複雑になってもvscodeに助けられながらスムーズに書くことができました。

- `uniqueId`：（ほぼ）ユニークなIDを生成します。
    - 実行すると`03ndvcgpbxi3`みたいのが返ります。
    - TODOごとのIDに使います。
- `onInput`：入力があるたびにstateの`inputText`を更新します。
    - 入力値の値を`e.target.value`と一気に得ようとしたのですが、TypeScriptはnull安全な言語なので`target`が`null`でないかつ`HTMLInputElement`であることを確認するコードが必要でした。
    - コードがちょっと増えるけど、ミスなく書けるのはやっぱり素敵です。
- `add`：`inputText`の値をクリアし`Todos`に加えます。
    - 空のTodoが加わらないように未入力の場合、そのまま`state`を返します。
- `toggle`：対象のTODOの未着手／作業済を切り替えます。
- `delete`：作業済みのTODOを一覧から削除します。

## `view`

```tsx:./src/views/todo.ts
import { h, View, Component } from "hyperapp";
import { ITodoState, ITodoItem } from "../states/todo";
import { ITodoActions, todoActions } from "../actions/todo";

const TodoItem: Component<ITodoItem> = ({ id, done, text }) =>
  done ? <strike>{text}</strike> : <span>{text}</span>;

export const TodoList: View<ITodoState, ITodoActions> = (state, actions) => (
  <main>
    <h1>To-Do List</h1>
    <ul>
      {state.todos.map(t => (
        <li key={t.id} onclick={() => actions.toggle(t.id)}>
          <TodoItem id={t.id} done={t.done} text={t.text} />
        </li>
      ))}
    </ul>
    <input
      type="text"
      placeholder="Add a to-do ..."
      oninput={(e: Event) => actions.onInput(e)}
      value={state.inputText}
    />
    <button onclick={() => actions.add()}>ADD</button>
    <button onclick={() => actions.delete()}>DELETE</button>
  </main>
);
```

- コンポーネントへの引数の渡し方に少し詰まりました。
    - コンポーネント側で引数を`({ id, done, text })`とすると`利用する際にJSX Attributeで渡すことができました。
    - また、その際不正な引数名や型を与えるとエラーを出してくれることに感動しました。

## `Index.ts`と`Index.html`

```./src/index.ts
import { app } from "hyperapp";
import { ITodoState, todoState } from "./states/todo";
import { ITodoActions, todoActions } from "./actions/todo";
import { TodoList } from "./views/todo";
import { withLogger } from "@hyperapp/logger";

withLogger(app)(todoState, todoActions, TodoList, document.body);
```

- `@hyperapp/logger`：コンソールにstateやactionが吐き出します。
    - これでデバッグできる！
    - 型定義ファイルが見つかりませんでしたが、`withLogger(app)(...)`しただけなので特に困りませんでした（が、vscodeの「型ないよ」警告が気になる...）

```html:./index.html
<!DOCTYPE html>
<html lang='en'>
  <head>
    <meta charset='UTF-8'>
    <title>To-Do List</title>
  </head>
  <body>
    <script src='./src/index.ts'></script>
  </body>
</html>
```

今度はメモリ上のデータでなく、サーバー側のAPIと連携して実際にDBを使うアプリにチャレンジしてみたいです！
あと、actionがかなり肥大化してるのでそこも適切にリファクタリングしたい...