サークルのWebハッカソンで、こねくりTODOアプリを作った(3日間+α)

Created: 2025/08/16

ハッカソンの概要

先日、サークル内で「Webハッカソン」を開催しました。
サークル内でハッカソンをするのは初だったので、結構ぐだった印象でしたが、なんとか...
(シンプルに準備に時間が取れなかったという言い訳...🤫)

IPUT ONEに軽い記事を載せました!

参加してくれたのは、主に1年生で、大学からプログラミングを始めたメンバーでした。
初心者向けというのもあり、流石に、3日間で完成まではいきませんでした🥲

そこで、基本的なcrud処理を網羅したTODOアプリを、この記事でまとめようと思います!

TODOアプリの概要

今回のTODOアプリは、ブラウザ上だけで完結する使用です。
ゆくゆくは、バックエンドを取り入れた、本格的なWebアプリを目指しています😤

そのため、DBっぽいデータ保管をするために、ブラウザのsessionStorageを無理くり利用しました。
sessionStorageをあまり理解せず利用したので、結構手こずりました笑

使用した技術

  • HTML(CSS)
  • JavaScript

とてもシンプルです。
Githubにもコード載せてるので、よかったら

実装過程

1日目

まずは、HTMLから作成しました。

todo.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="todo.css">
    <title>TODOアプリ</title>
    <script src="todo.js"></script>
</head>
<body>
    <h1 id="title">俺のTODOアプリ</h1>

    <p>これは、<strong>俺のTODOアプリだ</strong>、いっぱいTODOを入れろ!</p>

    <label>タスクを入力</label>
    <input type="text" id="input-task">
    <button id="save-button">保存</button>

    <h2>タスク一覧</h2>
    <ul id="task-list"></ul>
</body>
</html>

こんな感じです。
todo.htmlは、ここから変わりません。
特にこだわりはありません笑
formタグ使わずにlabelタグ使ってますが、見なかったことにしてください...🫥

まず、取り掛かるのはcrudのc、createの部分ですね。
createはこんな感じです。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    // ボタンがクリックされた時、タスクの保存
    save_button.addEventListener('click', () => {
        const input_task = document.getElementById('input-task');

        // <li>タグを作成
        const task = document.createElement('li');
        // <li>タグに入力されたテキストデータを挿入
        task.textContent = input_task.value
        // <ul>タグに、作成した<li>を入れ込む
        task_list.append(task);

        // 入力エリアの初期化
        input_task.value = "";
    });
});

作成したタスクを<ul id="task-list"></ul>を使ってリスト表示するところまで作りました。
なので、createとreadを作った感じですかね。
DBの操作とかしてないので、曖昧ですが...

これで、見た目上タスクが追加され、タスクの一覧が表示されるようになりました!
1日目はここまででした。

2日目

2日目は、内定先のインターンのため参加できませんでした。
なので、1日目の帰宅後にcrudのd、deleteを作っておきました。

deleteを作るにあたり、sessionStorageを使って、作成したタスクをDBっぽく保存します。
(sessionStorageは後々、編集機能やリロード時の一覧表示に重宝します)

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    //タスクの管理用(セッションストレージのキーとして利用)
    let task_count = 1;

    //タスクの保存
    save_button.addEventListener('click', () => {
        const input_task = document.getElementById('input-task');
        const task = document.createElement('li');

        //ブラウザのセッションストレージに保存
        sessionStorage.setItem(task_count, input_task.value);

        task.textContent = input_task.value
        task_list.append(task);

        input_task.value = "";
        task_count++;
    });
});

ブラウザのsessionStorageとは、ブラウザ上で一時的にデータを保持する際に利用される機能です。
今回は、これをDBの代わりに使う作戦です。(初見)

sessionStorageのデータは、開発者ツールで確認できます。
画像
画像の用意に、sessionStorageは[キー:値]のような辞書型のデータ構造しか対応していません。

なので、このsessionStorageをこねくり回してTODOアプリを作っていきます。
(ちなみに、ローカルストレージが気になる人はこの辺をチェック)

続いて、削除機能を作っていく。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    //タスクの管理用(変数名変更)
    let task_id = 1;

    //タスクの保存
    save_button.addEventListener('click', () => {
        const input_task = document.getElementById('input-task');
        const task = document.createElement('li');
        task.setAttribute('id', `task-${task_id}`);

        //削除ボタン
        const delete_button = document.createElement('p');
        delete_button.setAttribute('id', task_id);
        delete_button.setAttribute('class', "delete");
        delete_button.textContent = "削除";


        //削除機能
        delete_button.addEventListener('click', (event) => {
            //クリックされた要素のidを取得
            const delete_task_id = event.currentTarget.id;

            //セッションに保存したデータの削除
            sessionStorage.removeItem(delete_task_id);

            //選択したタスクの要素を削除
            document.getElementById(`task-${delete_task_id}`).remove();
            event.currentTarget.remove();
        });

        //ブラウザのセッションストレージに保存
        sessionStorage.setItem(task_id, input_task.value);

        task.textContent = input_task.value;

        task_list.append(task);
        task_list.append(delete_button);

        input_task.value = "";
        task_id++;
    });
});

削除機能は、タスクが作成された時点で作りました。
削除ボタンをpタグて作っているのは、なんとなく、cssでボタンっぽくできるし、クリックできたらなんでも良いかなと思って...(結局、最終的にbuttonにしました😗)

正直、ここまではsessionStorageを使っていません笑
ただデータを保存しているだけで、sessionStorageを使って一覧を表示しているわけではないので...
次の編集機能やページのリロード時に活用していくので、この時点でsessionStorageを利用しました。

2日目はここまででした。

3日目

3日目は、crudのu、updateを作りました。
編集機能ですね。
編集機能は、edit_todo.htmlを作成し、新たに編集ページを作ることにしました。
最初は、todo.html内で作ることを考えましたが、todo.jsが複雑になりそう?
あと、せっかくなのでsessionStorageを有効活用するためにページを分けました。
sessionStorageはページリロードされてもデータを保持できるので、相性は良いと思いました。

まずは、edit_todo.html

edit_todo.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="edit.js"></script>
    <title>TODOの編集</title>
</head>
<body>
    <h1>編集ページ</h1>
    <input type="text" id="edit-task">
    <button type="submit" id="edit-button">変更を保存</button>
</body>
</html>

まぁ、シンプルです。
あとは、edit_todo.htmlをロードする際に、選択したタスクのキーを取得できれば、編集できそう

ということで、todo.jsに編集機能を追加します。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    //タスクの管理用
    let task_id = 1;

    //タスクの保存
    save_button.addEventListener('click', () => {
        const input_task = document.getElementById('input-task');
        const task = document.createElement('li');
        task.setAttribute('id', `task-${task_id}`);

        //編集ボタン
        const edit_button = document.createElement('p');
        edit_button.setAttribute('id', `edit-${task_id}`);
        edit_button.setAttribute('class', "edit");
        edit_button.textContent = "編集";

        //編集機能
        edit_button.addEventListener('click', (event) => {
            const edit_task_id = event.currentTarget.id;
            // クリックしたタスクidを取得して
            // 新たに['edit_task_id': task_id]というデータを追加
            sessionStorage.setItem('edit_task_id', edit_task_id[5]);
            // edit_todo.htmlをロード(遷移)
            window.location.href = 'edit_todo.html';
        });

        // 削除ボタン
        割愛

        // 削除機能
        割愛

        //ブラウザのセッションストレージに保存
        sessionStorage.setItem(task_id, input_task.value);

        task.textContent = input_task.value;

        task_list.append(task);
        task_list.append(edit_button);
        task_list.append(delete_button);

        input_task.value = "";
        task_id++;
    });
});

編集機能はこんな感じです。
工夫点は、sessionStorage.setItem('edit_task_id', edit_task_id[5]);ですね。
新たに、edit_task_idをキーに持つデータをsessionStorageに追加することで、edit_todo.htmlに遷移後に、編集するタスクを取得できます。

そして、edit.jsを作成します。

edit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
document.addEventListener('DOMContentLoaded', () => {
    const edit_task = document.getElementById('edit-task');
    const edit_button = document.getElementById('edit-button');

    // 編集するタスクIDの取得
    const task_id = sessionStorage.getItem('edit_task_id');
    // 編集するタスクの値を取得
    const current_task = sessionStorage.getItem(task_id);
    // 編集前のタスクを入力エリアに表示
    edit_task.value = current_task;

    edit_button.addEventListener('click', () => {
        const input_task = edit_task.value;

        // タスクの上書き
        sessionStorage.setItem(task_id, input_task);
        // edit_task_idは編集完了後に必要ないため削除
        sessionStorage.removeItem('edit_task_id');
        // todo.htmlに戻る
        window.location.href = 'todo.html';
    });

});

`edit.jsはこんな感じです。
ページを分けたので、jsがかなりシンプルで、すっきりしたと思います。
別のページでも、データを扱えるのは便利ですね。

ここで問題が発生します。
ブラウザでsessionStorageを確認すると、データの更新はうまく行っています。
なので、編集機能はできています。
しかし、window.location.href = 'todo.html';で、戻るとタスクの一覧が消えています

これは、todo.htmlをロードしているためです。
todo.htmlを確認すると

todo.html
1
2
3
4
    ...
    <h2>タスク一覧</h2>
    <ul id="task-list"></ul>
    ...

タスクの一覧を表示する<ul id="task-list"></ul>部分が空の状態です。
なので、ロードすると、<ul id="task-list"></ul>部分が空の状態でロードされ、今まで作成したタスクが消えるのです。

なので、todo.htmlのロード時に、すでにタスクがあれば、そのタスクを表示する機能を作る必要があります。
これが、crudのr、readの部分になります。
タスク一覧の読み込みですね。

それでは、todo.jsに下記の関数を用意しました。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function indexTaskList(task_id_list) {
    const task_list = document.getElementById('task-list');

    for( let i=0; i<task_id_list.length; i++){
        //タスク
        const task = document.createElement('li');
        task.setAttribute('id', task_id_list[i]);
        task.textContent = sessionStorage.getItem(task_id_list[i]);

        //編集ボタン
        const edit_button = document.createElement('p');
        edit_button.setAttribute('id', `edit-${task_id_list[i]}`);
        edit_button.setAttribute('class', "edit");
        edit_button.textContent = "編集";

        //削除ボタン
        const delete_button = document.createElement('p');
        delete_button.setAttribute('id', `delete-${task_id_list[i]}`);
        delete_button.setAttribute('class', "delete");
        delete_button.textContent = "削除";

        //編集機能
        edit_button.addEventListener('click', (event) => {
            const edit_task_id = event.currentTarget.id;
            sessionStorage.setItem('edit_task_id', edit_task_id[5]);
            window.location.href = 'edit_todo.html';
        });

        //削除機能
        delete_button.addEventListener('click', (event) => {
            //クリックされた要素のidを取得
            const delete_task_id = event.currentTarget.id;
            //保存済みタスク
            let task_id_list = sessionStorage.getItem('task_id_list');
            sessionStorage.setItem('task_id_list', task_id_list.replace(task_id_list[i], ""));

            //セッションに保存したデータの削除
            sessionStorage.removeItem(task_id_list[i]);

            //選択したタスクの要素を削除
            document.getElementById(delete_task_id[7]).remove();
            document.getElementById(`edit-${delete_task_id[7]}`).remove();
            event.currentTarget.remove();
        });

        task_list.append(task);
        task_list.append(edit_button);
        task_list.append(delete_button);
    }
}

この関数をtodo.htmlのロード時に実行すれば、sessionStorageにあるタスクを表示できるははずです。
そこで、sessionStorageにtask_id_listというキーを持つデータを作成します。
このtask_id_listに、保存されているtask_idを全て保存します。

さらに、todo.jsに追加していく。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    //タスクの管理用
    let task_id = 1;

    // 初回ロード時
    if (sessionStorage.getItem('task_id_list') === null) {
        sessionStorage.setItem('task_id_list', '');
    // 2回目以降のロード時
    } else {
        let current_task_id_list = sessionStorage.getItem('task_id_list');
        indexTaskList(current_task_id_list);
    }

    //タスクの保存
    save_button.addEventListener('click', () => {
        // 現在のタスク一覧を取得
        let current_task_id_list = sessionStorage.getItem('task_id_list');

        ...

        // 追加するtask_idをlistに追加(文字列として扱っている)
        current_task_id_list = current_task_id_list + task_id;

        //ブラウザのセッションストレージに保存
        sessionStorage.setItem(task_id, input_task.value);
        sessionStorage.setItem('task_id_list', current_task_id_list);

        task_id++;
    });
});

メインの処理はこんな感じです。
これで、画像の様に、task_id_listを使って、リロードしてもタスクの一覧が表示できる様になりました。
画像

あとは、削除するときに、task_id_listから、削除したtask_idを取り除く処理を書けば良い

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        //削除機能
        delete_button.addEventListener('click', (event) => {
            //クリックされた要素のidを取得
            const delete_task_id = event.currentTarget.id;
            //保存済みタスク
            let task_id_list = sessionStorage.getItem('task_id_list');
            // 削除したidをtask_id_listから取り除く
            sessionStorage.setItem('task_id_list', task_id_list.replace(delete_task_id[7], ""));

            //セッションに保存したデータの削除
            sessionStorage.removeItem(delete_task_id[7]);

            //選択したタスクの要素を削除
            document.getElementById(delete_task_id[7]).remove();
            document.getElementById(`edit-${delete_task_id[7]}`).remove();
            event.currentTarget.remove();
        });

これで、crudの基本的な機能をtodoアプリに実装できました!
ここまでが、3日目でした。

お気づきの通り、このtodoアプリは致命的なバグがあります
そこを、+αで改善していきます...

まず、致命的なバグは、task_idの管理周りです。
現状、sessionStorageで管理しているtask_id_listは単純な文字列で管理しています。
そのため、task_idが2桁の数値になった時にロジックが破綻します。
画像
画像の様に、タスク10が作られると、バグります。
リロードする前は、

この様に、綺麗に表示されています。
しかし、ページをリロードすると

「タスク10」がバグります。
これは、indexTaskList8910という文字列を引数として渡し、
8910を、一つずつtask_id_list[i]でsessionStorageから値を取得しているためである。

todo.js
1
2
3
4
5
6
7
8
9
function indexTaskList(task_id_list) {
    const task_list = document.getElementById('task-list');

    for( let i=0; i<task_id_list.length; i++){
        //タスク
        const task = document.createElement('li');
        task.setAttribute('id', task_id_list[i]);
        task.textContent = sessionStorage.getItem(task_id_list[i]);
        ...

これを解決するために、task_id_listを違う文字列で保存する必要があります。
そこで、数値ごとに.で区切る方法を考えました。
1.2.3.9.10.12.101この様に区切ることで2桁以上でもtask_idを取得可能です。

まず、task_id_listの計算部分を

js
1
2
3
4
5
        if (current_task_id_list === "") {
            current_task_id_list = String(task_id);
        } else {
            current_task_id_list = current_task_id_list + "." + String(task_id);
        }

この様に修正しました。
次は、`1.2.3.9.10この文字列を.で分割し、新たに[1,2,3,9,10]という配列にして処理したいです。
そこで、.で分割して、配列を作る関数を用意しました。

todo.js
1
2
3
4
5
6
function getCurrentTaskIdList () {
    let current_task_id_list = sessionStorage.getItem('task_id_list');
    current_task_id_list = current_task_id_list.split(".");

    return current_task_id_list;
}

そして、ページロード時の処理を修正しました。

todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
document.addEventListener('DOMContentLoaded', () => {
    const save_button = document.getElementById('save-button');
    const task_list = document.getElementById('task-list');

    let task_id;

    // 初回ロード時
    if (sessionStorage.getItem('task_id_list') === null) {
        sessionStorage.setItem('task_id_list', '');
        task_id = 1;

    // リロード時
    } else {
        task_id = parseInt(sessionStorage.getItem('next_task_id'));

        // 配列でtask_idを取得
        let current_task_id_list = getCurrentTaskIdList();
        // current_task_id_list = ["1", "2", "10", "30"]の配列
        indexTaskList(current_task_id_list);
    }
    ...
            // そして、ちゃっかりtask_idのバグも修正
            task_id++;
            sessionStorage.setItem('next_task_id', String(task_id));
        }
    });
});

task_id_listに関するバグの修正は、こんな感じです。
sessionStorage.setItem('next_task_id', String(task_id));などで、リロード時にtask_idがリセットされるバグも修正しました。

そして、編集、削除機能も細かな修正を行いました。

todo.js(edit)
1
2
3
4
5
6
7
8
9
        //編集機能
        edit_button.addEventListener('click', (event) => {
             // -で分割して、正しくidを取得(id="edit-10")
            const edit_task = event.currentTarget.id.split("-");
             // edit_task = ["edit", "10"] 
            const edit_id = edit_task[1]
            sessionStorage.setItem('edit_task_id', edit_id);
            window.location.href = 'edit_todo.html';
        });
todo.js(delete)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//削除処理
function deleteTaskId(current_task_id_list, delete_id) {
    // 削除するtask_idを除外した配列を作成
    delete_task_id_list = current_task_id_list.filter((id) => id != delete_id);
    new_task_id_list = "";

    for (let i=0; i<delete_task_id_list.length; i++) {
        if (new_task_id_list === "") {
            new_task_id_list = delete_task_id_list[i];
        } else {
            new_task_id_list = new_task_id_list + "." + delete_task_id_list[i];
        }
    }

    return new_task_id_list;
}
document.addEventListener('DOMContentLoaded', () => {
        ...
        //削除機能
        delete_button.addEventListener('click', (event) => {
            // -で分割して、正しくidを取得(id="delete-10")
            const delete_task = event.currentTarget.id.split("-");
             // edit_task = ["delete", "10"]
            const delete_task_id = parseInt(delete_task[1]);

            const current_task_id_list = getCurrentTaskIdList();
            // 選択したtask_idを削除する関数を用意
            const new_task_id_list = deleteTaskId(current_task_id_list, delete_task_id);
            sessionStorage.setItem('task_id_list', new_task_id_list);

            //セッションデータの削除
            sessionStorage.removeItem(delete_task_id);

            //選択したタスクの要素を削除
            document.getElementById(delete_task_id).remove();
            document.getElementById(`edit-${delete_task_id}`).remove();
            event.currentTarget.remove();
        });

これで、全体的なバグ修正は完了です。
やっと、sessionStorageをこねくり回したtodoアプリができました!
(軽く動作確認はしたけど、まだバグあるかもです...)

まとめ

今回は、web開発の初歩ということで簡単なtodoアプリを作ってみました。
ハッカソンでは、最後まで作りきることはできませんでしたが、+αでなんとか...

sessionStorageはDBとしては使えないことがわかりました笑
まぁ、そもそもDB目的じゃないので当然ではありますが...

でも、試行錯誤したらなんとかなるもんですね!
こねくりtodoアプリはgithubにあげてるので興味があれば

夏休みは、まだあるので、バックエンドまで踏み込んだweb開発もやりたいですね〜
(卒研が...😵‍💫)