【JavaScript】追加、編集、削除機能付きのToDoアプリを作ってみた その2

## はじめに
先日書いた以下の記事で、「IndexedDBやlocalStorageを使えばより実用的になります」とのコメントをいただいたので、この度localStorageを使ってToDoアプリを改良してみました。

https://qiita.com/ry0h3i/items/a7f74e0333e4c55b95c5

## 完成品
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="rNEPBYX" data-pen-title="Untitled" data-user="ry0h3i" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ry0h3i/pen/rNEPBYX">
  Untitled</a> by ry0h3i (<a href="https://codepen.io/ry0h3i">@ry0h3i</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## コード解説
### 全コード
```javascript:script.js
const form = document.querySelector("#todoForm");
const list = document.querySelector("#todoList");

const storage = localStorage;
const todoArr = [];
const storageKey = "key";

// localStorageへデータを保存する
const saveStorage = (key, val) => {
    storage.setItem(key, JSON.stringify(val));
};

// localStorageからデータを取得する
const getStorage = (key) => {
    // localStorageにデータが残っていれば、データを配列に変換して呼び出し元に返す
    // localStorageにデータが残っていなければ、空の配列を呼び出し元に返す
    return JSON.parse(storage.getItem(key)) ?? [];
};

// localStorageを初期化する
const clearStorage = (key) => {
    storage.removeItem(key);
};

// ToDoを追加するための関数
const addTodo = (val) => {
    // テキストボックスが空欄の場合はアラートを出す
    if (!val) {
        alert("ToDoを入力してください!!");
        return;
    }

    // liタグを作成する
    const item = document.createElement("li");

    // spanタグを作成し、テキストボックスの入力値を代入する
    const label = document.createElement("span");
    label.innerText = val;

    // 編集ボタンを作成する
    const editBtn = document.createElement("button");
    editBtn.classList.add("editBtn");
    editBtn.innerText = "編集する";

    // 削除ボタンを作成する
    const deleteBtn = document.createElement("button");
    deleteBtn.classList.add("deleteBtn");
    deleteBtn.innerText = "削除する";

    // liタグにspanタグ、編集ボタン、削除ボタンを追加する
    item.append(label, editBtn, deleteBtn);

    // ulタグにliタグを追加する
    list.append(item);
};

// ToDoを保存するための関数
const saveTodo = (val) => {
    // localStorageに渡す配列にテキストボックスの入力値を追加する
    todoArr.push(val);

    // localStorageにデータ（配列）を保存する
    saveStorage(storageKey, todoArr);
};

// アプリ開始時とリロード時にデータの確認を行うための関数
const readTodo = () => {
    // localStorageのデータを取得する
    const todos = getStorage(storageKey);

    // localStorageにデータが残っていなければ、処理を終える
    if (todos.length === 0) return;

    // localStorageにデータが残っていた場合
    for (let i = 0; i < todos.length; i++) {
        // 返ってきたデータ（配列）の要素を取り出し、localStorageに渡す配列に追加する
        const todo = todos[i];
        todoArr.push(todo);

        // localStorageにデータを保存する
        saveStorage(storageKey, todoArr);

        // ToDoを追加する
        addTodo(todo);
    }
};

// ToDoを編集するための関数
const editTodo = (target) => {
    // クリックされた編集ボタンにクラスを付与する
    target.classList.toggle("isEdited");

    // クリックされていないその他のボタンを取得する
    const otherBtns = document.querySelectorAll("button:not(.isEdited)");

    if (target.classList.contains("isEdited")) {
        // 編集ボタンのテキストを変更
        target.innerText = "登録する";

        // クリックされていないその他の編集ボタンを押せないようにする
        for (let btn of otherBtns) {
            btn.disabled = true;
        }

        // spanタグを取得
        const label = target.previousElementSibling;

        // テキストボックスを作成し、value属性をspanタグのToDoに設定
        const editInput = document.createElement("input");
        editInput.setAttribute("type", "text");
        editInput.setAttribute("value", label.innerText);

        // spanタグをテキストボックスに差し替える
        label.replaceWith(editInput);
    } else {
        // テキストボックスを取得する
        const editInput = target.previousElementSibling;

        // テキストボックスが空欄の場合はアラートを出す
        if (!editInput.value) {
            alert("ToDoを入力してください!!");
            return;
        }

        // localStorageに渡す配列の要素を全て削除する
        todoArr.splice(0);
        // localStorageを初期化する
        clearStorage(storageKey);

        // spanタグを作成し、テキストボックスの入力値を代入する
        const label = document.createElement("span");
        label.innerText = editInput.value;

        // テキストボックスをspanタグに差し替える
        editInput.replaceWith(label);

        // 全てのliタグを取得する
        const items = document.querySelectorAll("li");

        for (let item of items) {
            // li > spanのテキストを取得する
            const todoVal = item.querySelector("span").innerText;
            // ToDoを保存する
            saveTodo(todoVal);
        }

        // クリックされていないその他のボタンを再び押せるようにする
        for (let btn of otherBtns) {
            btn.disabled = false;
        }

        // 編集ボタンのテキストを変更
        target.innerText = "編集する";
    }
};

// ToDoを削除するための関数
const deleteTodo = (target) => {
    // 削除ボタンの親liタグを削除
    target.closest("li").remove();

    // localStorageに渡すための配列の要素を全て削除する
    todoArr.splice(0);
    // localStorageを初期化する
    clearStorage(storageKey);

    // liタグを全て取得する
    const items = document.querySelectorAll("li");

    // ToDoが0個だった場合、処理を終える
    if (!items) return;

    // ToDoが残っている場合
    for (let item of items) {
        // li > spanのテキストを取得する
        const todoVal = item.querySelector("span").innerText;
        // ToDoを保存する
        saveTodo(todoVal);
    }
};

// アプリ開始時とリロード時にデータの確認を行う
readTodo();

document.addEventListener("click", function (e) {
    // イベントが発生した要素を取得
    const element = e.target;

    if (!(element instanceof HTMLElement)) return;

    // 編集ボタンがクリックされた場合、ToDo編集の関数を実行する
    if (element.matches(".editBtn")) {
        editTodo(element);
    }
    // 削除ボタンがクリックされた場合、ToDo削除の関数を実行する
    else if (element.matches(".deleteBtn")) {
        deleteTodo(element);
    }
});

form.addEventListener("submit", function (e) {
    e.preventDefault();

    // テキストボックスの入力値を取得する
    const todoVal = document.querySelector("#todo").value;
    // ToDoを追加する
    addTodo(todoVal);
    // ToDoを保存する
    saveTodo(todoVal);
    // テキストボックスの値を初期化する
    document.querySelector("#todo").value = "";
});
```
### 前回から変更した箇所
#### イベントリスナー周り
```javascript:script.js
form.addEventListener("submit", function (e) {
    e.preventDefault();

    // テキストボックスの入力値を取得する
    const todoVal = document.querySelector("#todo").value;
    // ToDoを追加する
    addTodo(todoVal);
    // ToDoを保存する
    saveTodo(todoVal);
    // テキストボックスの値を初期化する
    document.querySelector("#todo").value = "";
});
```
- ToDoを追加した後に、ToDoをlocalStorageに保存する処理を追加しました
***
#### localStorage周り
```javascript:script.js
const storage = localStorage;
const todoArr = [];
const storageKey = "key";

// localStorageへデータを保存する
const saveStorage = (key, val) => {
    storage.setItem(key, JSON.stringify(val));
};

// localStorageからデータを取得する
const getStorage = (key) => {
    // localStorageにデータが残っていれば、データを配列に変換して呼び出し元に返す
    // localStorageにデータが残っていなければ、空の配列を呼び出し元に返す
    return JSON.parse(storage.getItem(key)) ?? [];
};

// localStorageを初期化する
const clearStorage = (key) => {
    storage.removeItem(key);
};
```
- `todoArr` ... ToDoを格納し、localStorageにデータとして保存するための配列
- `storageKey` ... localStorageに渡すキー
***
```javascript:script.js
// ToDoを保存するための関数
const saveTodo = (val) => {
    // localStorageに渡す配列にテキストボックスの入力値を追加する
    todoArr.push(val);

    // localStorageにデータ（配列）を保存する
    saveStorage(storageKey, todoArr);
};
```
- テキストボックスの入力値をlocalStorageに保存する配列へ追加して、その配列をlocalStorageに保存します
***
```javascript:script.js
// ToDoを削除するための関数
const deleteTodo = (target) => {
    // 削除ボタンの親liタグを削除
    target.closest("li").remove();

    // localStorageに渡すための配列の要素を全て削除する
    todoArr.splice(0);
    // localStorageを初期化する
    clearStorage(storageKey);

    // liタグを全て取得する
    const items = document.querySelectorAll("li");

    // ToDoが0個だった場合、処理を終える
    if (!items) return;

    // ToDoが残っている場合
    for (let item of items) {
        // li > spanのテキストを取得する
        const todoVal = item.querySelector("span").innerText;
        // ToDoを保存する
        saveTodo(todoVal);
    }
};
```
- ToDoを削除したら、localStorageのデータを整理するために、localStorageとlocalStorageに渡す配列を一旦リセットします
- ToDoが他に残っている場合はそれらを取得して、localStorageに保存し直します
***
```javascript:script.js
// ToDoを編集するための関数
const editTodo = (target) => {
    // クリックされた編集ボタンにクラスを付与する
    target.classList.toggle("isEdited");

    // クリックされていないその他のボタンを取得する
    const otherBtns = document.querySelectorAll("button:not(.isEdited)");

    if (target.classList.contains("isEdited")) {
        // 編集ボタンのテキストを変更
        target.innerText = "登録する";

        // クリックされていないその他の編集ボタンを押せないようにする
        for (let btn of otherBtns) {
            btn.disabled = true;
        }

        // spanタグを取得
        const label = target.previousElementSibling;

        // テキストボックスを作成し、value属性をspanタグのToDoに設定
        const editInput = document.createElement("input");
        editInput.setAttribute("type", "text");
        editInput.setAttribute("value", label.innerText);

        // spanタグをテキストボックスに差し替える
        label.replaceWith(editInput);
    } else {
        // テキストボックスを取得する
        const editInput = target.previousElementSibling;

        // テキストボックスが空欄の場合はアラートを出す
        if (!editInput.value) {
            alert("ToDoを入力してください!!");
            return;
        }

        // localStorageに渡す配列の要素を全て削除する
        todoArr.splice(0);
        // localStorageを初期化する
        clearStorage(storageKey);

        // spanタグを作成し、テキストボックスの入力値を代入する
        const label = document.createElement("span");
        label.innerText = editInput.value;

        // テキストボックスをspanタグに差し替える
        editInput.replaceWith(label);

        // 全てのliタグを取得する
        const items = document.querySelectorAll("li");

        for (let item of items) {
            // li > spanのテキストを取得する
            const todoVal = item.querySelector("span").innerText;
            // ToDoを保存する
            saveTodo(todoVal);
        }

        // クリックされていないその他のボタンを再び押せるようにする
        for (let btn of otherBtns) {
            btn.disabled = false;
        }

        // 編集ボタンのテキストを変更
        target.innerText = "編集する";
    }
};
```
- クリックされた編集ボタンに`isEdited`クラスを付け外しして、`isEdited`クラスの有無で処理を切り替えます
- 1つの編集ボタンがクリックされたら、その他のボタンは編集作業が完了するまで押せないようにします
- ToDo編集後にもう一度編集ボタンがクリックされたら、一旦localStorageとlocalStorageに保存する配列をリセットし、現存するToDoをlocalStorageに保存し直します
***
```javascript:script.js
// アプリ開始時とリロード時にデータの確認を行うための関数
const readTodo = () => {
    // localStorageのデータを取得する
    const todos = getStorage(storageKey);

    // localStorageにデータが残っていなければ、処理を終える
    if (todos.length === 0) return;

    // localStorageにデータが残っていた場合
    for (let i = 0; i < todos.length; i++) {
        // 返ってきたデータ（配列）の要素を取り出し、localStorageに渡す配列に追加する
        const todo = todos[i];
        todoArr.push(todo);

        // localStorageにデータを保存する
        saveStorage(storageKey, todoArr);

        // ToDoを追加する
        addTodo(todo);
    }
};

// アプリ開始時とリロード時にデータの確認を行う
readTodo();
```
- アプリ開始時とリロード時にlocalStorageのデータを確認します
- localStorageにデータが残っていた場合、返ってきたデータをlocalStorageへ渡す配列に追加し、その配列をlocalStorageに保存し直します
## 参考記事

https://webliker.info/web-skill/how-to-use-localstrage/

https://yurupro.cloud/3107/

https://sunday-morning.app/posts/2019-10-14-document-queryselectorall-not

https://qiita.com/niwasawa/items/89e86c0fd85ad4591e1c