Todoアイテムの追加を実装する
ここからはJavaScriptでTodoアプリの機能を作成していきます。
このセクションでは、前のセクションでHTMLに目印を付けたTodoリスト(#js-todo-list
)に対してTodoアイテムを追加する処理を実装します。
Todoアイテムの追加
まず、Todoアプリではどのような操作をしたら、Todoアイテムを追加できるかを見ていきます。
Todoアプリでは、ユーザーが次のような操作を行った場合に、Todoアイテムを追加します。
- 入力欄にTodoアイテムのタイトルを入力する
- 入力欄でEnterキーを押し送信する
- TodoリストにTodoアイテムが追加される
これをJavaScriptで実現するには次のことが必要です。
- form要素から送信(
submit
)されたことをイベントで受け取る - input要素(入力欄)に入力された内容を取得する
- 入力内容をタイトルにしたTodoアイテムを作成し、Todoリスト(
#js-todo-list
)にTodoアイテム要素を追加する
まずは、form要素から送信されたイベントを受け取り、入力内容をコンソールログに表示してみることから始めてみましょう。
入力内容をコンソールに表示
form要素でEnterキーを押し送信するとsubmit
イベントが発生します。
このsubmit
イベントはaddEventListener
メソッドを利用することで受け取れます。
// id="js-form`の要素を取得
const formElement = document.querySelector("#js-form");
// form要素から発生したsubmitイベントを受け取る
formElement.addEventListener("submit", (event) => {
// イベントが発生した時に呼ばれるコールバック関数
});
フォームが送信されたときに入力内容をコンソールに表示するには、
addEventListener
コールバック関数内で入力内容をConsole APIで出力すればよいことになります。
入力内容はinput要素のvalue
プロパティから取得できます。
const inputElement = document.querySelector("#js-form-input");
console.log(inputElement.value); // => "input要素の入力内容"
これらを組み合わせてApp.js
に「入力内容をコンソールに表示」する機能を実装してみましょう。
App
クラスにmount
というメソッドを定義して、その中に処理を書いていきましょう。
次のコードでは、フォーム(#js-form
)をEnterで送信すると、input要素(#js-form-input
)の内容をコンソールへ表示する処理を実装しています。
src/App.js
export class App {
mount() {
const formElement = document.querySelector("#js-form");
const inputElement = document.querySelector("#js-form-input");
formElement.addEventListener("submit", (event) => {
// submitイベントの本来の動作を止める
event.preventDefault();
console.log(`入力欄の値: ${inputElement.value}`);
});
}
}
このままでは、App#mount
は呼び出されないため何も行われません。
そのため、index.js
も変更して、App
クラスのmount
メソッドを呼び出すようにします。
index.js
import { App } from "./src/App.js";
const app = new App();
app.mount();
これらの変更後にブラウザでページをリロードすると、App#mount
メソッドが実行されるようになります。
submit
イベントがリッスンされているので、入力欄に何か入力してEnterで送信してみるとその内容がコンソールに表示されます。
先ほどのApp#mount
メソッドでは、submit
イベントのイベントリスナー内でevent.preventDefault
メソッドを呼び出しています。
event.preventDefault
メソッドは、submit
イベントの発生元であるフォームがもつデフォルトの動作をキャンセルするメソッドです。
フォームがもつデフォルトの動作とは、フォームの内容を指定したURLへ送信するという動作です。
ここではform
要素に送信先が指定されていないため、現在のURLに対してフォームを送信が行われます。
event.preventDefault
メソッドを呼び出すことで、このデフォルトの動作をキャンセルしています。
formElement.addEventListener("submit", (event) => {
// submitイベントの本来の動作を止める
event.preventDefault();
console.log(`入力欄の値: ${inputElement.value}`);
});
現在のURLに対してフォームを送信が行われると、結果的にページがリロードされてしまうため、event.preventDefault()
を呼び出していました。
これはevent.preventDefault()
をコメントアウトすると、ページがリロードされてしまうことが確認できます。
formElement.addEventListener("submit", (event) => {
// preventDefaultしないとページがリロードされてしまう
// event.preventDefault();
console.log(`入力欄の値: ${inputElement.value}`);
});
ここまででtodoapp
ディレクトリは次のような変更を加えました。
todoapp
├── index.html
├── index.js (App#mountの呼び出し)
└── src
└── App.js (App#mountの実装)
ここまでのTodoアプリは次のURLで確認できます。
入力内容をTodoリストに表示
フォーム送信時に入力内容を取得する方法が分かったので、次はその入力内容をTodoリスト(#js-todo-list
)に表示します。
HTMLではリストのアイテムを記述する際には<li>
タグを使います。
また後ほどTodoリストに表示するTodoアイテムの要素には、完了状態を表すチェックボックスや削除ボタンなども含めたいです。
これらの要素を含むものを手続き的にDOM APIで作成すると見通しが悪くなるため、HTML文字列からHTML要素を生成するユーティリティモジュールを作成しましょう。
次のhtml-util.js
をsrc/view/html-util.js
というパスに作成します。
このhtml-util.js
は「ajaxapp: HTML文字列をDOMに追加する」でも利用したescapeSpecialChars
をベースにしています。
ajaxappでのescapeHTML
タグ関数では出力はHTML文字列でしたが、今回作成するelement
タグ関数の出力はHTML要素(Element)です。
これはTodoリスト(#js-todo-list
)というすでに存在する要素に対して要素を追加するには、HTML文字列ではなく要素が必要になります。
また、HTML文字列に対してはaddEventListener
でイベントをリッスンできません。
そのため、チェックボックスの状態が変わったことや削除ボタンが押されたことを知る必要があるTodoアプリでは要素が必要になります。
src/view/html-util.js
export function escapeSpecialChars(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
export function htmlToElement(html) {
const template = document.createElement("template");
template.innerHTML = html;
return template.content.firstElementChild;
}
/**
* HTML文字列からDOM Nodeを作成して返すタグ関数
* @return {Element}
*/
export function element(strings, ...values) {
const htmlString = strings.reduce((result, string, i) => {
const value = values[i - 1];
if (typeof value === "string") {
return result + escapeSpecialChars(value) + string;
} else {
return result + String(value) + string;
}
});
return htmlToElement(htmlString);
}
/**
* コンテナ要素の中身をbodyElementで上書きする
* @param {Element} bodyElement コンテナ要素の中身となる要素
* @param {Element} containerElement コンテナ要素
*/
export function render(bodyElement, containerElement) {
// rootElementの中身を空にする
containerElement.innerHTML = "";
// rootElementの直下にbodyElementを追加する
containerElement.appendChild(bodyElement);
}
element
タグ関数では、同じファイルに定義したhtmlToElement
関数を使ってHTML文字列からHTML要素を作成しています。
htmlToElement
関数の中で利用しているtemplate要素はHTML5で追加された、HTML文字列の断片からHTML要素を作成できる要素です。
このelement
タグ関数を使うことで、次のようにHTML文字列からHTML要素を作成できます。
作成した要素は、appendChild
メソッドなどで既存の要素に子要素として追加できます。
// HTML文字列からHTML要素を作成
const newElement = element`<ul>
<li>新しい要素</li>
</ul>`;
// 作成した要素を`document.body`の子要素として追加(appendChild)する
document.body.appendChild(newElement);
ブラウザが提供するappendChild
メソッドは子要素を追加するだけであるため、すでに別の要素がある場合は末尾に追加されます。
このセクションではまだ利用しませんが、html-util.js
にはrender
という関数を定義しています。
render
関数は指定したコンテナ要素(親となる要素)の子要素を上書きする関数となります。
動作的には一度子要素をすべて消したあとにappendChild
で子要素として追加しています。
// `ul`要素の空タグを作成
const newElement = element`<ul />`;
// `newElement`を`document.body`の子要素として追加する
// 既に`document.body`以下に要素がある場合は上書きされる
render(newElement, document.body);
最後に、このelement
タグ関数を使い、フォームから送信された入力内容をTodoリストに要素として追加してみます。
App.js
から先ほど作成したhtml-util.js
のelement
タグ関数をimport
します。
次にsubmit
イベントのリスナー関数で、Todoアイテムを表現する要素を作成し、Todoリスト(#js-todo-list
)の子要素として追加(appendChild
)します。
最後にTodoアイテム数(#js-todo-count
)のテキスト(textContent
)を更新します。
src/App.js
import { element } from "./view/html-util.js";
export class App {
mount() {
const formElement = document.querySelector("#js-form");
const inputElement = document.querySelector("#js-form-input");
const containerElement = document.querySelector("#js-todo-list");
const todoItemCountElement = document.querySelector("#js-todo-count");
// Todoアイテム数
let todoItemCount = 0;
formElement.addEventListener("submit", (event) => {
// 本来のsubmitイベントの動作を止める
event.preventDefault();
// 追加するTodoアイテムの要素(li要素)を作成する
const todoItemElement = element`<li>${inputElement.value}</li>`;
// Todoアイテムをcontainerに追加する
containerElement.appendChild(todoItemElement);
// Todoアイテム数を+1し、表示されてるテキストを更新する
todoItemCount += 1;
todoItemCountElement.textContent = `Todoアイテム数: ${todoItemCount}`;
// 入力欄を空文字にしてリセットする
inputElement.value = "";
});
}
}
これらの変更後にブラウザでページをリロードしてから、フォームに入力してからEnterを押すとTodoリストにTodoアイテムが追加されます。
また、入力内容を送信するたびにtodoItemCount
が加算され、Todoアイテム数の表示も更新されます。
このセクションでの変更点は次のとおりです。
todoapp
├── index.html
├── index.js
└── src
├── App.js(Todoアイテムの表示の実装)
└── view
└── html-util.js(新規追加)
ここまでのTodoアプリは次のURLで確認できます。
まとめ
このセクションではform要素のsubmit
イベントをリッスンし、入力内容を元にTodoアイテムをTodoリストの追加を実装しました。
今回のTodoアイテムの追加のように多くのウェブアプリは、何らかのイベントをリッスンして表示を更新します。
このようなイベントが発生したことを元に処理を進める方法をイベント駆動(イベントドリブン)と呼びます。
今回のTodoアイテムの追加では、submit
イベントを入力にして、Todoリスト要素を直接HTML要素として追加するという方法を取っていました。
このように直接DOMを更新するという方法はコードが短くなりますが、DOMのみにしか状態は残らないため柔軟性がなくなるという問題があります。
次のセクションでは、実際にどのような問題がおきるかやそれを解決するための仕組みを見ていきます。
このセクションのチェックリスト
- フォームの入力内容をイベントで受け取ることができた
- HTML文字列からHTML要素を作成するhtml-util.jsを実装した
- フォームからTodoアイテムを追加できた
- Todoアイテムの追加に合わせてTodoアイテム数を更新できた
このセクションでTodoアプリにTodoアイテムの追加する機能が実装できました。
- Todoアイテムを追加できる
- Todoアイテムの完了状態を更新できる
- Todoアイテムを削除できる