JavaScriptでTODO機能を実装する – LocalStorageでデータを保存する方法

プログラミング

今回は、JavaScriptを使ってTODO機能(タスクの追加・削除)を一から実装する方法を解説します。

TODO機能は、プログラミング学習でよく作られるアプリケーションの一つです。フォームからデータを受け取り、LocalStorageに保存して、画面に表示するという基本的な流れを学ぶことができます。

この記事では、フォーム操作、LocalStorageでのデータ永続化、JSON変換など、TODO機能の実装で必要な技術とつまずいたポイントを詳しく解説します。

TODO機能とは?

TODO機能は、やるべきタスクを登録・管理する機能です。基本的な機能としては、タスクの追加、タスクの削除、タスクの表示があります。

今回実装するTODO機能の特徴は以下の通りです。

タスクをフォームから追加でき、LocalStorageに保存されるため、ページをリロードしてもデータが残ります。また、削除ボタンでタスクを削除することもできます。サーバー不要で、ブラウザだけで動作します。

完成イメージ

完成イメージ

作るTODO機能の完成形です。

入力フォームからタスクを追加し、削除ボタンでタスクを削除できます。データはLocalStorageに保存されます。

TODO機能の完全なコード

まず、完全なコードを見てから、各部分を詳しく解説します。

HTML (index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODOリスト</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>TODOリスト</h1>
        
        <!-- タスク追加フォーム -->
        <form id="todoForm">
            <input type="text" id="taskInput" placeholder="タスクを入力" required>
            <button type="submit">追加</button>
        </form>
        
        <!-- タスク一覧 -->
        <ul id="todoList"></ul>
    </div>
    
    <script src="script.js"></script>
</body>
</html>

CSS (style.css)

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    background-color: #f5f5f5;
    padding: 20px;
}

.container {
    max-width: 600px;
    margin: 0 auto;
    background-color: white;
    padding: 30px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 30px;
}

/* フォーム */
#todoForm {
    display: flex;
    gap: 10px;
    margin-bottom: 30px;
}

#taskInput {
    flex: 1;
    padding: 10px 15px;
    border: 2px solid #ddd;
    border-radius: 5px;
    font-size: 1em;
}

#taskInput:focus {
    outline: none;
    border-color: #4CAF50;
}

button {
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1em;
}

button:hover {
    background-color: #45a049;
}

/* タスク一覧 */
#todoList {
    list-style: none;
}

.todo-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px;
    margin-bottom: 10px;
    background-color: #f9f9f9;
    border-radius: 5px;
    border-left: 4px solid #4CAF50;
}

.todo-item span {
    flex: 1;
    font-size: 1em;
    color: #333;
}

.delete-btn {
    padding: 5px 15px;
    background-color: #f44336;
    color: white;
    border: none;
    border-radius: 3px;
    cursor: pointer;
    font-size: 0.9em;
}

.delete-btn:hover {
    background-color: #da190b;
}

JavaScript (script.js)

// 要素を取得
const form = document.getElementById('todoForm');
const taskInput = document.getElementById('taskInput');
const todoList = document.getElementById('todoList');

// ページ読み込み時にタスクを表示
document.addEventListener('DOMContentLoaded', () => {
    displayTasks();
});

// フォーム送信時の処理
form.addEventListener('submit', (e) => {
    e.preventDefault();  // ページリロードを防ぐ
    addTask();
});

// タスクを追加する関数
function addTask() {
    const taskText = taskInput.value.trim();
    
    // 空欄チェック
    if (taskText === '') {
        alert('タスクを入力してください');
        return;
    }
    
    // タスクオブジェクトを作成
    const task = {
        id: Date.now(),
        text: taskText,
        completed: false
    };
    
    // LocalStorageに保存
    saveTask(task);
    
    // 画面に表示
    displayTasks();
    
    // フォームをクリア
    taskInput.value = '';
}

// タスクをLocalStorageに保存
function saveTask(task) {
    let tasks = getTasks();
    tasks.push(task);
    localStorage.setItem('tasks', JSON.stringify(tasks));
}

// LocalStorageからタスクを取得
function getTasks() {
    let tasks = localStorage.getItem('tasks');
    return tasks ? JSON.parse(tasks) : [];
}

// タスクを画面に表示
function displayTasks() {
    let tasks = getTasks();
    todoList.innerHTML = '';
    
    tasks.forEach(task => {
        const li = document.createElement('li');
        li.className = 'todo-item';
        li.innerHTML = `
            <span>${task.text}</span>
            <button class="delete-btn" onclick="deleteTask(${task.id})">削除</button>
        `;
        todoList.appendChild(li);
    });
}

// タスクを削除
function deleteTask(taskId) {
    let tasks = getTasks();
    tasks = tasks.filter(task => task.id !== taskId);
    localStorage.setItem('tasks', JSON.stringify(tasks));
    displayTasks();
}


コードの詳細解説

それでは、各部分を詳しく見ていきましょう。

HTMLの構造

HTMLは非常にシンプルです。フォームとタスク一覧だけで構成されています。

<!-- タスク追加フォーム -->
<form id="todoForm">
    <input type="text" id="taskInput" placeholder="タスクを入力" required>
    <button type="submit">追加</button>
</form>

<!-- タスク一覧 -->
<ul id="todoList"></ul>

required 属性をつけることで、空欄での送信を防ぎます。タスク一覧は空の ul 要素で、JavaScriptで動的に中身を生成します。

フォーム送信の処理

フォームが送信されたときの処理が、TODO機能の起点になります。

form.addEventListener('submit', (e) => {
    e.preventDefault();  // ページリロードを防ぐ
    addTask();
});

preventDefault()の重要性

e.preventDefault() を書かないと、フォーム送信時にページがリロードされてしまいます。

// preventDefault()なし
form.addEventListener('submit', (e) => {
    addTask();
    // → ページがリロードされる
});
// preventDefault()あり
form.addEventListener('submit', (e) => {
    e.preventDefault();
    addTask();
    // → ページはそのまま
});

フォームの submit イベントは、デフォルトでページをリロードする動作があります。これを止めるために e.preventDefault() が必要です。最初、これを書き忘れて、タスクを追加してもすぐに消えてしまう問題に悩まされました。

タスクの追加

addTask() 関数でタスクを追加します。

function addTask() {
    const taskText = taskInput.value.trim();
    
    // 空欄チェック
    if (taskText === '') {
        alert('タスクを入力してください');
        return;
    }
    
    // タスクオブジェクトを作成
    const task = {
        id: Date.now(),
        text: taskText,
        completed: false
    };
    
    // LocalStorageに保存
    saveTask(task);
    
    // 画面に表示
    displayTasks();
    
    // フォームをクリア
    taskInput.value = '';
}

IDの生成

id: Date.now()

Date.now() は現在時刻をミリ秒で返すので、重複しないIDが作れます。

Date.now()  // 1702620000000
Date.now()  // 1702620000001 ← 1ミリ秒後

trim()で空白を削除

const taskText = taskInput.value.trim();

trim() は前後の空白を削除します。これにより、空白だけのタスクが登録されるのを防ぎます。

'  hello  '.trim()  // 'hello'
'   '.trim()        // ''(空文字)

LocalStorageへの保存

LocalStorageは、ブラウザにデータを保存できる機能です。

function saveTask(task) {
    let tasks = getTasks();
    tasks.push(task);
    localStorage.setItem('tasks', JSON.stringify(tasks));
}

なぜJSON変換が必要?

LocalStorageは文字列しか保存できないからです。

//  オブジェクトをそのまま保存
let task = {text: "勉強"};
localStorage.setItem('task', task);
// → "[object Object]" という文字列になってしまう
//  JSON文字列に変換して保存
localStorage.setItem('task', JSON.stringify(task));
// → '{"text":"勉強"}' として保存される

保存するときは JSON.stringify() で文字列に変換し、取り出すときは JSON.parse() でオブジェクトに戻します。

LocalStorageからの取得

function getTasks() {
    let tasks = localStorage.getItem('tasks');
    return tasks ? JSON.parse(tasks) : [];
}

この関数は、LocalStorageからタスクを取得します。データがない場合(初回起動時)は空配列を返します。

// データがある場合
localStorage.getItem('tasks')  // '[{"id":1,"text":"勉強"}]'
JSON.parse(tasks)              // [{id:1, text:"勉強"}]

// データがない場合
localStorage.getItem('tasks')  // null
tasks ? JSON.parse(tasks) : [] // [](空配列)

三項演算子

return tasks ? JSON.parse(tasks) : [];

これは以下と同じ意味です。

if (tasks) {
    return JSON.parse(tasks);
} else {
    return [];
}

タスクの表示

function displayTasks() {
    let tasks = getTasks();
    todoList.innerHTML = '';
    
    tasks.forEach(task => {
        const li = document.createElement('li');
        li.className = 'todo-item';
        li.innerHTML = `
            <span>${task.text}</span>
            <button class="delete-btn" onclick="deleteTask(${task.id})">削除</button>
        `;
        todoList.appendChild(li);
    });
}

innerHTML = ”

既存のタスクをクリアしてから、新しく全タスクを表示します。

todoList.innerHTML = '';  // 一度クリア
// その後、全タスクを追加

forEach()でループ

tasks.forEach(task => {
    // 各タスクごとに実行
});

forEach()は配列の各要素に対して処理を実行します。

テンプレートリテラル

li.innerHTML = `
    <span>${task.text}</span>
    <button class="delete-btn" onclick="deleteTask(${task.id})">削除</button>
`;

バッククォート(`)で囲むことで、変数を埋め込んだHTMLを簡単に作れます。

タスクの削除

function deleteTask(taskId) {
    let tasks = getTasks();
    tasks = tasks.filter(task => task.id !== taskId);
    localStorage.setItem('tasks', JSON.stringify(tasks));
    displayTasks();
}

filter()の使い方

filter() は「条件に合う要素だけ残す」メソッドです。

let tasks = [
    {id: 1, text: "勉強"},
    {id: 2, text: "買い物"},
    {id: 3, text: "運動"}
];

// id が 2 以外を残す
tasks = tasks.filter(task => task.id !== 2);

// 結果:
// [{id: 1, text: "勉強"}, {id: 3, text: "運動"}]

指定したID以外のタスクを残すことで、削除を実現しています。


つまずいたポイント

1. preventDefault()を忘れた

フォーム送信時にページがリロードされる問題に悩まされました。

//  間違い
form.addEventListener('submit', (e) => {
    addTask();
    // → ページがリロードされる
});
//  正解
form.addEventListener('submit', (e) => {
    e.preventDefault();  // これを追加
    addTask();
});

2. JSON変換を忘れた

LocalStorageから取り出したデータが文字列のままになっていました。

//  間違い
let tasks = localStorage.getItem('tasks');
// → tasks = '[{"id":1,"text":"勉強"}]' (文字列)
//  正解
let tasks = localStorage.getItem('tasks');
tasks = JSON.parse(tasks);
// → tasks = [{id:1, text:"勉強"}] (配列)

3. 配列を上書きしていた

既存の配列を取得せずに、新しいタスクだけを保存していました。

//  間違い
function saveTask(task) {
    localStorage.setItem('tasks', JSON.stringify(task));
    // → 毎回上書きされて、1つしか保存されない
}
//  正解
function saveTask(task) {
    let tasks = getTasks();      // 既存の配列を取得
    tasks.push(task);             // 追加
    localStorage.setItem('tasks', JSON.stringify(tasks));
}

4. 初回起動時のエラー

LocalStorageにデータがない状態で JSON.parse() を実行するとエラーになりました。

//  間違い
function getTasks() {
    let tasks = localStorage.getItem('tasks');
    return JSON.parse(tasks);  // tasksがnullだとエラー
}
//  正解
function getTasks() {
    let tasks = localStorage.getItem('tasks');
    return tasks ? JSON.parse(tasks) : [];  // データがなければ空配列
}

まとめ

今回、TODO機能を実装して学んだポイントです。

LocalStorageを使うことで、サーバー不要でデータを永続化できます。ただし、LocalStorageは文字列しか保存できないため、JSON.stringify()JSON.parse() による変換が必須です。

フォーム処理では、preventDefault() でページリロードを防ぐことが重要です。これを忘れると、データを追加してもすぐに消えてしまいます。

配列操作では、push() で追加、filter() で削除を行います。削除の際は、指定したID以外の要素を残すという方法を使います。

初回起動時など、データがない状態での処理も考慮する必要があります。三項演算子を使って、データがあれば処理、なければ空配列を返すという処理を書きます。

TODO機能は基本的ですが、フォーム操作、LocalStorage、配列操作、DOM操作など、JavaScriptの基礎が詰まっています。この実装を理解すれば、より複雑なアプリケーションも作れるようになります。


関連記事:


カテゴリー: 開発記録
タグ: JavaScript, TODO, LocalStorage, フォーム, JSON

コメント

タイトルとURLをコピーしました