はじめに
みなさん、こんにちは。torihaziです。
今日はVanillaJSでTodoリストを作ってみました。
ネットで色々調べても編集だったり削除とかだったりを実装している記事が
見当たらなかったのでそういった方のための助けになればと思い
書いてみました。
※タグにも記載ありますが、初心者です。
間違いが無いように最善を尽くしていますが、
もしあればコメント等でそっと指摘をいただけますと幸いです。
どんなものができるか。
こんな感じのものです。
コード(結論だけ知りたい方へ)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet">
<title>To Do List</title>
</head>
<body>
<div class="container">
<div class="header-container">
<h2>To Do</h2>
<button id="dark-mode-btn">ダークモード</button>
</div>
<form id="js-form">
<input
id="js-form-input"
class="new-todo"
type="text"
placeholder="What need to be done?"
autocomplete="off"
>
<input type="submit" value="保存">
</form>
<div id="js-list-container">
<h3>To-do</h3>
<div id="js-todo-list" class="todo-list">
</div>
<h3>Done</h3>
<div id="js-done-list" class="done-list">
</div>
</div>
<footer class="footer">
<span id="js-all-count">全タスク数: 0</span>
<span id="js-todo-count">未完了: 0</span>
<span id="js-done-count">完了済: 0</span>
</footer>
</div>
<template id="list-template">
<li>
<input type="checkbox" class="checkbox">
<span class="content"></span>
<button class="updBtn">編集</button>
<button class="delBtn">削除</button>
</li>
</template>
<template id="update-form-template">
<input
id="js-updateForm-input"
class="update-todo"
type="text"
autocomplete="off"
>
<button class="finBtn">更新</button>
</template>
<script src="./index.js" type="module"></script>
</body>
</html>
import * as util from "./src/Util.js";
const formElement = document.querySelector("#js-form");
const inputElement = document.querySelector("#js-form-input");
const listContainer = document.querySelector("#js-list-container");
const todoLists = document.querySelector("#js-todo-list");
const doneLists = document.querySelector("#js-done-list");
const allItemCountElement = document.querySelector("#js-all-count");
const todoItemCountElement = document.querySelector("#js-todo-count");
const doneItemCountElement = document.querySelector("#js-done-count");
// ダークモード用
const body = document.querySelector("body");
const darkmodeBtn = document.querySelector("#dark-mode-btn");
darkmodeBtn.addEventListener("click", () => {
body.classList.toggle("dark");
})
let todoCount = 0;
let doneCount = 0;
// 追加ボタン押下
formElement.addEventListener("submit", (event) => {
event.preventDefault();
const sanitizedText = util.escapeSpecialChars(inputElement.value.trim());
if (!sanitizedText) return;
const listTemplate = document.getElementById("list-template");
const liContent = listTemplate.content;
const listClone = document.importNode(liContent, true);
listClone.querySelector(".content").innerText = sanitizedText;
todoLists.appendChild(listClone);
todoCount = todoLists.childElementCount;
doneCount = doneLists.childElementCount;
todoItemCountElement.textContent = `未完了: ${todoCount}`;
doneItemCountElement.textContent = `完了済: ${doneCount}`;
allItemCountElement.textContent = `全タスク数: ${todoCount + doneCount}`;
inputElement.value = "";
});
listContainer.addEventListener("click", (event) => {
const parentElement = event.target.parentElement;
// チェックボックス
if (event.target.classList.contains("checkbox")) {
if ( event.target.checked ){
parentElement.className = "done";
todoLists.removeChild(parentElement);
doneLists.appendChild(parentElement);
todoCount = todoLists.childElementCount;
doneCount = doneLists.childElementCount;
todoItemCountElement.textContent = `未完了: ${todoCount}`;
doneItemCountElement.textContent = `完了済: ${doneCount}`;
allItemCountElement.textContent = `全タスク数: ${todoCount + doneCount}`;
} else {
parentElement.className = "todo";
doneLists.removeChild(parentElement);
todoLists.appendChild(parentElement);
todoCount = todoLists.childElementCount;
doneCount = doneLists.childElementCount;
todoItemCountElement.textContent = `未完了: ${todoCount}`;
doneItemCountElement.textContent = `完了済: ${doneCount}`;
allItemCountElement.textContent = `全タスク数: ${todoCount + doneCount}`;
}
}
// 編集ボタン押下
if (event.target.classList.contains("updBtn")) {
const updFormTemplate = document.querySelector("#update-form-template");
const updFormContent = updFormTemplate.content;
const updFormClone = document.importNode(updFormContent, true);
const previousContent = parentElement.querySelector(".content").textContent;
parentElement.querySelector(".checkbox").style.display = "none";
parentElement.querySelector(".delBtn").style.display = "none";
parentElement.querySelector(".updBtn").style.display = "none";
parentElement.style.display = "inline-block";
updFormClone.querySelector("#js-updateForm-input").value = previousContent;
parentElement.querySelector(".content").replaceWith(updFormClone);
}
// 更新ボタン押下
if (event.target.classList.contains("finBtn")) {
const updateInputElement = parentElement.querySelector("#js-updateForm-input");
const sanitizedUpdateText = util.escapeSpecialChars(updateInputElement.value.trim());
if(!sanitizedUpdateText) return;
const spanElement = document.createElement("span");
spanElement.className = "content";
spanElement.textContent = sanitizedUpdateText;
event.target.remove();
updateInputElement.replaceWith(spanElement);
parentElement.querySelector(".checkbox").style.display = "";
parentElement.querySelector(".delBtn").style.display = "";
parentElement.querySelector(".updBtn").style.display = "";
parentElement.style.display = "";
}
// 削除ボタン押下
if (event.target.classList.contains("delBtn")) {
let result = confirm("本当によろしいですか");
if (result) {
parentElement.remove();
todoCount = todoLists.childElementCount;
doneCount = doneLists.childElementCount;
todoItemCountElement.textContent = `未完了: ${todoCount}`;
doneItemCountElement.textContent = `完了済: ${doneCount}`;
allItemCountElement.textContent = `全タスク数: ${todoCount + doneCount}`;
} else {
return;
}
}
});
export function escapeSpecialChars(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/* 共通の設定 */
li {
list-style: none;
margin-bottom: 0.3rem;
}
button {
padding: 0.2rem 0.1rem;
}
/* コンテンツを囲む大枠のcontainer */
.container {
display: flex;
flex-direction: column;
width: 50%;
height: 100vh;
margin: auto;
}
/* 見出しを含むcontainer */
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
/* おまけ用のダークモードボタン */
#dark-mode-btn {
background-color: gray;
color: white;
width: 150px;
height: 30px;
text-align: center;
cursor: pointer;
}
#dark-mode-btn:hover {
opacity: 0.7;
}
.dark {
background-color: black;
color: silver;
}
.dark #dark-mode-Btn {
background-color: silver;
color: black;
}
/* チェック後の文字に取り消し線 */
li.done span{
text-decoration: line-through;
}
解説
イベントデリゲーション
これは権限委譲というものになります。
各子要素にイベントハンドラを設定するのではなく、
それらの子要素を内包する親要素にイベントハンドラを設定することで
キャプチャリングとバブリングというものを利用し、子要素のイベントを検知することができる仕組みです。
こうすることで動的に生成される要素(今回なら個々のTodoタスク)に
addEventlistenerを設定しなくても良くなります。
コード内でいうと
listContainer.addEventListener("click", (event) => {
~~
の部分になります。
ちなみにその親要素に上のように指定すると イベントを発生させた要素は
event.targetとして取得できます。
今回はその要素が対象のcssクラスを持っているかで "何が押されたか"を判別しました。
template要素
こちらはjavascriptで使うことを前提として使用される HTML要素です。
これを使用することで要素を生成するときにありがちな
多重createElement地獄を避けることができます。
template要素で囲まれたものに関しては htmlのページには表示されません。
template要素の使い方については書き記事が参考になるかと思います。
ダークモードについて
こちらの方のものを使用させていただきました。
終わりに
const なんとかElement = document . *****みたいなものが
色々ありすぎて途中から何だかわからなくなりました。
以前までRuby on Railsを使っていたので
何だか不思議な感覚でした。
純粋なJavascriptで書くと実装が大変になるということはどの記事も言っていましたが
こういうことだったのかと今更ながら実感しています。
Githubにもありますのでよろしかったらご覧ください
以上となります。