Chapter 02無料公開

React を使わずに作る Todo リストは辛い

sadnessOjisan
sadnessOjisan
2021.04.29に更新

それではさっそく React を使わずに Todo リストを作っていきましょう。このような Todo リストを作ります。

TODOリストの完成系画像

機能としては、以下の通りです。

  • 一覧表示
  • 追加
  • 完了・未完了のチェック
  • 削除

プレーンな JavaScript のみで実装する

それでは Todo リストを作っていきます。

雛形を用意する

このような HTML を用意します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css"
    />
    <link rel="stylesheet" type="text/css" href="style.css" />
    <title>naive todo</title>
  </head>
  <body>
    <!-- ここに要素を書いていく -->
    <script src="index.js"></script>
  </body>
</html>

style.css へ CSS, index.js に JavaScript を書くことを前提とした構成です。

inter.cssnew.min.css は new.css という CSS フレームワークを読み込むためのものです。機能実現のためには不要ですが、見栄えが良くなるので導入しました。これは No class CSS[1] という種類のもので、クラス定義を書かなくても見栄えを整えてくれる CSS です。

フォームを作る

Todo を登録するための入力フォームを作ります。

index.html
<form onsubmit="handleFormSubmit(); return false">
  <input id="task-input" />
  <button>登録する</button>
</form>

送信時にこれから JavaScript で定義する handleFormSubmit 関数を呼び出します。return false はフォーム送信後の遷移を防ぐためのものです。

要素を取得する

それでは form を送信し、そのイベントをハンドリングして Todo を追加する処理を書いていきましょう。

index.js
/**
 * formの送信時に実行する関数。
 * input form の入力内容を取得してそれをTODOリストに加える
 */
function handleFormSubmit() {
  // 入力内容の取得
  const inputEl = document.getElementById("task-input");
  const inputValue = inputEl.value;

  // 入力値チェック
  if (!inputValue.length > 0) {
    alert("テキストを入力してください。");
    return;
  }

  // Todo の追加対象を取得する
  const todosEl = document.getElementById("todos");

  // TODO要素を作る (チェックボックス・ラベル・削除ボタンのセット)
  const todoEl = createTodoElement(inputValue);

  // TODOの表示エリアにTODOを追加
  todosEl.appendChild(todoEl);

  // input のリセット
  input.value = "";
}

getElementById で入力エリアの DOM を取得し、入力内容を取得します。

const inputEl = document.getElementById("task-input");
const inputValue = inputEl.value;

この課題には必須ではありませんが、入力値チェックもしておきましょう。いまは !inputValue.length > 0 を確認し、なにか入力されないと Todo を追加できないようにします。

if (!inputValue.length > 0) {
  alert("テキストを入力してください。");
  return;
}

Todo 要素を追加するので、その追加対象となるエリアの DOM を document.getElementById("todos"); で取得します。

const todosEl = document.getElementById("todos");

そして createTodoElement 関数で入力内容から Todo 要素を作ります。

const todoEl = createTodoElement(inputValue);

Todo 要素ができたら表示エリアに追加し、入力エリアをリセットします。

todosEl.appendChild(todoEl);
input.value = "";

Todo 要素を作る

では、 createTodoElement の中をみていきましょう。

/**
 * TODO要素を作る関数
 * @param {*} inputValue TODO文字列
 * @returns TODO要素
 */
function createTodoElement(task) {
  // TODO要素を作る
  const todoEl = document.createElement("li");

  // checkbox要素を作る
  const checkBoxEl = document.createElement("input");
  checkBoxEl.type = "checkbox";
  checkBoxEl.onchange = function (e) {
    const checked = e.target.checked;
    if (checked) {
      todoEl.className = `checked`;
    } else {
      todoEl.className = "";
    }
  };

  // label要素を作る
  const labelEl = document.createElement("label");
  labelEl.innerText = task;

  // ボタン要素を作る
  const buttonEl = document.createElement("button");
  buttonEl.innerText = "削除";
  buttonEl.onclick = function () {
    todoEl.remove();
  };

  // checkbox, label, button を TODO要素にセットする
  todoEl.appendChild(checkBoxEl);
  todoEl.appendChild(labelEl);
  todoEl.appendChild(buttonEl);

  return todoEl;
}

とても長いですね。上から順にみていきましょう。

まず Todo 要素はチェックボックス・ラベル・削除ボタンから成り立ちます。それらの要素をまとめる箱として TodoEl を定義します。

const todoEl = document.createElement("li");

チェックボックス要素は <input type='checkbox'> なので、 input 要素を作ります。そして type に checkbox を指定します。この切り替わりに応じてスタイルを変えたいので、onchange メソッドを定義します。スタイルは class の切り替えで実現することを考え、class の付け替えをします。

const checkBoxEl = document.createElement("input");
checkBoxEl.type = "checkbox";
checkBoxEl.onchange = function (e) {
  const checked = e.target.checked;
  if (checked) {
    todoEl.className = `checked`;
  } else {
    todoEl.className = "";
  }
};

ラベル要素には Todo の内容(task)を差し込みます。

const labelEl = document.createElement("label");
labelEl.innerText = task;

button 要素は削除という文字列を差込み、onclick イベントを定義します。クリックされた時に Todo 要素を削除したいので todoEl.remove()を呼び出します。

const buttonEl = document.createElement("button");
buttonEl.innerText = "削除";
buttonEl.onclick = function () {
  todoEl.remove();
};

最後にこれらの 3 要素を todoEl に追加します。

todoEl.appendChild(checkBoxEl);
todoEl.appendChild(labelEl);
todoEl.appendChild(buttonEl);

return todoEl;

CSS を付け替える

さきほど class の付け替えでスタイルを切り替えられるようにしました。そのスタイルをスタイルシートに定義しましょう。

checked という class がつくと打ち消し線を表示するようにします。

.checked {
  text-decoration: line-through;
}

辛いポイントを見よう

さぁ、これで Todo リストが完成しました。しかし、実装には改善したい点もありそうです。

関数が長く複雑で見通しが悪い

createTodoElement 関数は要素を作る関数です。ここで作られる要素にイベントハンドラを設定するために、関数の中で関数定義と代入しています。また他にも 1 つ 1 つの要素に type や text の内容をセットしています。そのため処理自体が長くなってしまっています。

そして View を作る処理とイベントハンドラを登録する処理も同じ関数で行われており、責務が分割されておらず見通しも悪くなっています。

オブジェクトへの代入で UI や挙動を作るため処理が追い辛く、破壊も容易

基本的に DOM 要素への代入によって値や関数をセットしています。このような方法で UI や挙動を組み上げていくと、意図しない再代入で破壊する恐れがあります。コードベースが大きくなると、どの値がどの代入処理で作られたかを追うことも大変になり、改修コストは高くなります。

データと UI の密結合

Todo 一覧が JS が管理されておらず、UI にしかその情報がありません。これは Todo を更新するときに UI を直接更新する必要があります。[1:1]

苦しみが想像できなかったあなたへ

もし具体的な苦しさが想像できない場合は、上記のコードもしくは自分で考えた何の設計論を適用していない JS だけの Todo リストに対して次のことを試みてください。

  • タスク内容の一括編集機能を実装して下さい
  • タスクに対して完了以外に中断中という状態を定義してください
  • その上でタスクの状態に応じたフィルター機能を実装してください

とくにデータと UI が密結合していることに対する苦しみが顕著に体験できるはずです。

まとめ

このようにただの Todo リストですら愚直に作ると処理を追い難かったり、変更が難しくなります。その原因は 責務が分割されていないことです。そこで次は MVC と呼ばれるアーキテクチャを取り入れて修正を図ってみましょう。

先ほどの実装は こちら です。

脚注
  1. もし JS 上で表現された Todo データを更新することで UI が更新できれば良さそうですね。これは React 導入の大きなモチベーションとなります。 ↩︎ ↩︎