JavaScript を使わず HTML と CSS だけで Todo アプリを書く方法
How to write a JavaScript-free todo app using just HTML and CSS
HTML と CSS だけで Todo アプリを作りました。TodoMVC(今回デザインを使用使わせてもらいました)と違って JavaScript は使っておらず、すべてのインタラクションが CSS で動いています。
どういう仕組みなのでしょう?簡単に説明すると、HTML、CSS の一般兄弟セレクタ(~
)、CSS カウンタ、それから :checked
、:target
、:required
疑似セレクタを組み合わせてできています。詳細についてはこれから説明していきます。
できること:
- Todo の追加(最大50件)
- Todo の完了
- Todo の削除
- フィルタリング(完了・未完了)
- 残りのアイテム数をカウントする
- 空文字の追加を許可しない
できないこと:
- ページリロード後の永続性の維持
- 「すべての Todo を完了にする(Mark all as done)」
- エンターキーで Todo を追加する
:checked でコンテンツを表示・非表示
インタラクティブなアプリでは、ステート(状態)の保存や変更を CSS 内で行うことが求められます。通常、ステートは HTML の中にあるのですが、JavaScript を使わない場合 DOM の構造を変更することはできません。
この問題を回避するためには、チェックボックスを使ってステートを保存し、:checked
疑似セレクタを使いステートにアクセスするようにします。
これは簡単な一例です:
Toggle content: <input type="checkbox"></input>
<div id="content">
Hello world!
</div>
<style>
#content {
display: none;
}
input:checked ~ #content {
display: block;
}
</style>
このコードでは、 CSS の一般兄弟セレクタである ~
を使用しています。これは、チェックが付いたチェックボックスの次に続く全ての兄弟にマッチします。この例では、#content
を表示または非表示にしていることになります(一般兄弟セレクタは、チェックボックスの前にある要素にはマッチしません)。
先に進む前に、label
タグの for
属性についても認識しておいてください。これを使うことにより、チェックボックス自身とは別に、チェックボックスのステートを切り替えられるボタンを置くことができます。
<input type="checkbox" id="toggle-box"></input>
<label for="toggle-box">Toggle!</label>
<div id="content">
Hello world!
</div>
表示・非表示のロジックを大規模なものに適用する
さて、ステートを保存する方法がわかれば、Todo アプリを構築することができます。各 Todo アイテムは、次の3つのチェックボックスを持っています:
- Todo が作成されたか
- Todo が完了されたか
- Todo が削除されたか
#1 は、このアプリがどのように動いているのかを知る手がかりになるかもしれません。JavaScript を使わずに DOM を変更する方法はありません。つまり、全ての Todo アイテムは、予め HTML に組み込まれている必要があります。ページのソースを見てみると、プリレンダリングされた Todo アイテムが、すでに50個も含まれていることがわかります。
CSS は、各 Todo アイテムを表示・非表示にするために使われています。
DOM の構成は、次の通りです。最初の Todo アイテムには、それ以降のすべての Todo アイテムがが含まれており、2番目の Todo アイテムには3番目から50番目までの Todo アイテムが含まれています。
.todo#todo-1
input.created-checkbox
.todo#todo-2
input.created-checkbox
.todo#todo-3
...
これは Todo リストのスクリーンショットの一部です:
各 Todo アイテムは次のようになります:
それでは、項目の削除がどのように行われているか、詳しく見ていきましょう。まず、deleted
のステートを保存するためのチェックボックスを持っています:
<input type="checkbox" class="deleted-checkbox" id="deleted-checkbox-3">
そして Todo アイテムを削除するためのラベルを持っています:
<label for="deleted-checkbox-3" class="deleted-checkbox-label">×</label>
チェックボックスを :checked
にしたとき、その項目を非表示にしたいです。しかし各 Todo アイテムには後続の Todo アイテムがすべて含まれているので、次の .todo
は非表示にしないようにしています:
.deleted-checkbox:checked ~ :not(.todo) {
display: none !important;
}
少しでも簡単にするために、チェックボックスは DOM の前の方に置いています。こうすることで、~
セレクタにより後続の兄弟にマッチングさせることができます。
完了・未完了でフィルタリング
TodoMVC には、完了した項目、あるいは完了していない項目だけを表示するオプションがあります。チェックボックスを使って同様の機能を実装することもできますが、URL ハッシュを使うことでより綺麗に実装できます。
フィルタのリンクは次のようになります:
<a class="filter-active" href="#/active">Active</a>
リンクをクリックすると、ブラウザは ID /active
のついた要素へとスクロールします。ここで重要な点は、それよりもむしろ、その要素が :target
疑似セレクタにマッチするようになったことです。
<div id="/completed" class="completed-filter">
<!-- Todo items -->
</div>
これで、作成はされているものの「完了」としてマークされていない子要素を隠せるようになりました。
.completed-filter:target
.created-checkbox:checked
~ .done-checkbox:not(:checked)
~ .todo-input {
display: none !important;
}
というわけで、チェックボックスに加えて、URL 中にもステートを保存・アクセスできるようになりました。
Todo の入力欄は次に来る一番最初の未作成 Todo アイテム
これはとてもシンプルです。未作成な Todo アイテムはすべて非表示になります。ただし、次に来る一番最初の未作成 Todo アイテムを除いてです。
次に来る未作成 Todo アイテムは、position: absolute
でリストの先頭に移動し、「Add」ボタンが表示され、Todo の入力欄となります。
残りのアイテム数をカウントする
CSS には Counters という素敵な機能があります。これは、 CSS セレクタにある項目がいくつ一致するかカウントしてくれるものです。
これを使えば、未完了の Todo がいくつ残っているか表示することができます。
こちらが最終的な CSS です。
body {
counter-reset: items-left;
}
.created-checkbox:checked
~ .deleted-checkbox:not(:checked)
~ .done-checkbox:not(:checked)
~ .items-left-counter-helper {
counter-increment: items-left;
}
#items-left:before {
content: counter(items-left);
}
カウントしたい項目は以下の通りです:
- 作成済みのアイテム
- 削除されていないアイテム
- 完了済みでないアイテム
.items-left-counter-helper
を数えるかわりに、.mark-undone-checkbox-label
を数えることはできないでしょうか?私も最初に試してみたのですが、CSS カウンターは隠されている要素を数えてくれないのです。そのため、「完了」フィルタを適用してみても items left
の値は0でした(未完了のアイテムはすべて非表示なので)。
空のアイテムの作成ができないようにする
擬似セレクタ :required
のおかげで動作させることができます。
HTML には、フォームのバリデーション機能があります。これを使うことにより、例えばテキストフィールドを必須にすることができます:
<input required type="text" value="" class="todo-input">`
また、CSS を使うことで、フィールドに値が入っているか、そしてそれが有効な値かどうかをチェックすることができます:
input:not(:valid) ~ .created-checkbox-label {
pointer-events: none;
}
pointer-events
を使うと、クリックやホバーといったマウス操作を無効にすることができます。
エンターでの ToDo 項目追加は可能か?
TodoMVC では普通に動きますが、ここでは難しいので、代わりに「Add」のボタンを使っています。
しかし、こんな方法ならうまくいくかもしれません:
textarea
を使い、\n
の入力時を何らかの方法で確認する- そしてその
\n
が見えないフォントを使う(Todo が編集可能な場合のみ必要)
「すべての項目を完了にする」はできる?
これは難しいです。なぜなら、ワンクリックで複数のチェックボックスの値を反転することと、別のチェックボックスを「完了」として上書きすること、どちらの意味にもなるからです。
同じ ID を持つ複数のチェックボックスを作成してもうまく動きません。一度に複数のチェックボックスがターゲットになるため、label
のタグをネストすることもできないのです。
後者についても、簡単ではありません。新規作成されたアイテムは「完了」されるべきではないですし、もはや「未完了」としてマークされなくなってしまうかもしれません。
JavaScript を使わずサーバーの永続性を実装する方法とは?
こんなことができるのでしょうか。私もアイデアはあったのですが、途中でうまくいかないことに気が付きました。
CSS を使うことで、Todo アイテムが作成されたとき、あるいは変更されたときに、特定の背景画像を表示することができます。その結果、画像が表示された際に画像のリクエストがサーバーに投げられ、ステータスの変化をサーバに通知することができるのです。
ここで難しいのは、入力テキストを送信することです。背景画像の URL の一部として直接送信することはできませんし、マッチする可能性のある値が多すぎます。
私のアイデアは、 CSS の属性セレクタを使うことでした。
Todo が作成されると、背景画像 todo-created.png
が読み込まれます。サーバーは新しい Todo アイテムが作成されたことを検知できるので、中身を見たくなります。もし HTML にロングポーリングする link
タグがたくさん含まれていれば、バックエンドはスタイルシートを送信して、先頭の文字を尋ねることができます。
「a」と「b」だけで構成されるアルファベットがあるとします。^=
セレクタは、指定された文字から始まる属性と一致します。
input[value^='a'] { background-image: url('first-letter-is-a') }
input[value^='b'] { background-image: url('first-letter-is-b') }
最初の文字が「b」 だったら、次にバックエンドはこのような形のものを送信します。
input[value^='ba'] { background-image: url('second-letter-is-a') }
input[value^='bb'] { background-image: url('second-letter-is-b') }
ユーザーが入力したすべての文字を認識するまで、送信送信・・・と続きます。
なぜ私は、この方法がうまくいくと思っていたのか?それは input[value^='a']
は「a」から始まる HTML 属性値と一致しますが、実際の入力フィールド値とは一致しないからです。Todo アイテムの名前を編集しても、属性は変更されないのですが。
終わりに
必要であれば、ソースコードを見てみてください。
私が気づいた面白い点は、Todo 項目を分けるのに、~
セレクタが非常に都合が良いということでした。
これを始めたとき、各 Todo アイテムごとに deleted-checkbox-34
のようなセレクタと CSS をいくつも作りました。完成した CSS はかなり大きくなっていました。しかし最終的には、インデックス独自のスタイルルールをすべて取り除き、一般的なセレクタのみを使うようにしました。今は ID のみを使って、チェックボックスのラベルをターゲットにしています。
当然ですが、ほぼ CSS だけでできているアプリをプロダクションで使うことはありませんが、CSS を楽しく、より深く理解する良いエキササイズでした。