LoginSignup
8
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-06

Hyperappを学ぶ(フロントエンドに慣れる)ために定番のTODOアプリを作りました。
reactangularのチュートリアルに触れたことがある程度の経験の僕でも結構あっさり作れて楽しいです。
todo.gif

開発環境はこの記事と同じです。

state

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

./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安全な言語なのでtargetnullでないかつHTMLInputElementであることを確認するコードが必要でした。
    • コードがちょっと増えるけど、ミスなく書けるのはやっぱり素敵です。
  • addinputTextの値をクリアしTodosに加えます。
    • 空のTodoが加わらないように未入力の場合、そのままstateを返します。
  • toggle:対象のTODOの未着手/作業済を切り替えます。
  • delete:作業済みのTODOを一覧から削除します。

view

./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.tsIndex.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の「型ないよ」警告が気になる...)
./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がかなり肥大化してるのでそこも適切にリファクタリングしたい...

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6