👩‍💻

Vue.js3でTODOリストを実装➀

に公開

勉強の備忘録として記事を書いてます。
Vue.jsでTODOリストを開発していきます。Geminiでコード生成して、不明点や疑問点はchatGPTを頼り、補足してもらうような形で開発を進めました。
アウトプットして自分の中の知識を固めたいという思いで書いていますが、誰かのために役に立てれば嬉しいです。


🌟 今回のTODOリストでやりたいこと

・タスクの入力フォームは「+」で追加
・追加したタスクは「完了」「編集」「削除」で管理
・「完了」:タスクの背景が暗くなる
・「編集」:選択したタスクを編集・保存
・「削除」:モーダルで警告を表示してYESなら削除
・タスクを「ALL」「UNFINISHED」「COMPLETE」で分類
・設定ボタンの設置
・設定ボタンからテーマカラー「白」「黒」「オレンジ」変更


📁 コンポーネントの構成

App.vue
components
TodoInput.vue タスクの入力フォーム
TodoList.vue タスクの一覧表示と操作(完了・編集・削除)
TodoFilter.vue タスクの分類
ConfirmModal.vue 削除前の確認モーダル
SettingMenu.vue 設定(テーマカラー)


🌷 初期設定と必要なフォルダとファイルの準備

main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
<script setup></script>
<template></template>
<style scoped></style>

👽 vueの独自イベントとファイルのインポート

App.vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoFilter from './components/TodoFilter.vue';
import ConfirmModal from './components/ConfirmModal.vue';
import SettingMenu from './components/SettingMenu.vue';
</script>

<template>
    <SettingMenu />
    <h1>TODO LIST</h1>
    <TodoInput />
    <TodoFilter />
    <TodoList />
    <ConfirmModal />
</template>
App.vue
import { ref, computed, watch, onMounted } from 'vue';

ref:リアクティブな変数をつくる
computed:何かの値を自動で計算するリアクティブな変数(常に最新の情報を計算してくれる)
watch:リアクティブな変数の値が変わった瞬間に処理(ターゲットを見張る)自動保存・バリデーションなどで使用
onMounted:コンポーネントがマウントしたときに1回だけ実行したい処理(画面が開いた瞬間に発動)初期データの読み込み・APIの呼び出しなど


🔑 ローカルブラウザに保存 データのキー設定

App.vue
const STORAGE_KEY = 'todo-list-data';
const THEME_KEY = 'todo-list-theme';

STORAGE_KEY:TODOリストのデータを保存するための名前
THEME_KEY:テーマ設定を保存するための名前

constで定義しておくことで管理を一元化することができるため管理しやすくなる。キー名は他のWEBアプリと衝突しないようにプロジェクト名などを含めた名前+何を保存してるのかわかりやすいようにする。


🏄 リアクティブ変数の登録

App.vue
<script setup>
const todos = ref([]);
const filter = ref('all');
const theme = ref('orange');
const isModalVisible = ref(false);
const todoDelete = ref(null);
</script>

const todos = ref([]);:タスクの一覧を管理。追加・編集・削除に応じて画面更新をする。初期値は空のまま。
const filter = ref('all');:タスクを分類するためのフィルター。初期値はallでリスト全てを表示する。
const theme = ref('orange');:選択中のテーマカラーを管理。初期値はorangeで背景色設定しておく。
const isModalVisible = ref(false);:削除確認モーダルの表示・非表示を管理。削除ボタンをクリックするとtrueにしたいため初期値はfalseで非表示にしておく。
const todoDelete = ref(null);:タスクを削除するためにタスクにIDを保持させる。モーダルと連携して削除操作に使う。


📃 タスクを追加する処理を定義

App.vue
<script setup>
const addTodo = (newTitle) => {
  if (newTitle.trim()) {
    todos.value.push({
      id: Date.now(),
      title: newTitle,
      completed: false,
      isEditing: false,
    });
  }
};
</script>

引数のnewTitleには追加したいタスクの文字列が入る。.trim()をつけることで文字列の前後の空白や空文字・スペースを排除する。
タスクをtodos(タスク一覧)に.push(追加)する。
オブジェクトの中身について
id:値にはタスクの識別子Date.now()で細かい数値を出すことで他と重複しない値を作れるため識別しやすい。
title:ユーザーが入力したタスク
completed:タスクの完了状態(初期値はfalse)
isEditing:編集モードの状態(初期値はfalse)

App.vue
<template>
<TodoInput
    @add-todo="addTodo" />
</template>

🤖 computedを使って各フィルターに基づくタスク一覧を表示

App.vue
<script setup>
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active': return todos.value.filter(todo => !todo.completed);
    case 'completed': return todos.value.filter(todo => todo.completed);
    case 'all': default: return todos.value;
  }
});
</script>

computed(() => {});:中身の値(ここではfilter.valuetodos.value)が変わると自動で再計算されて常に最新の結果が反映される。(TodoFilter.vueの<templete>内で使用する)

switch:computedと組み合わせることでフィルターの種類に応じてタスクを抽出する処理をしてくれる。

switch (値) {
  case 値1:
    // 値が値1のときの処理
    break;
  case 値2:
    // 値が値2のときの処理
    break;
  default:
    // どの case にも当てはまらない場合の処理
}

switchの後に判定したい値(filter.value)を書く
caseごとに値を比較する。今回は「全て」「未完了」「完了」の3つあるため3つで比較。それぞれcaseに一致したときの処理を実行。

filter()メソッド:配列.filter(要素 => 条件)として使う。今回はtodos.value.filter(todo => 条件)配列のtodos.valueから条件に合う要素だけを新しい配列として返す。今回要素に当てはまるtodofilterメソッドが渡してくれる配列の要素。

todo.completedは、上記のタスクの追加で定義しているプロパティとして作成したもので、タスク作成時にref内で初期化しているプロパティ

return todos.value.filter(todo => !todo.completed);:「未完了」のみ抽出。todo.completedfalseのものだけ返す。

return todos.value.filter(todo => todo.completed);:「完了」のみ抽出。todo.completedtrueのものだけ返す。

「全て」はどちらにも当てはまらないため全タスクを返す。そのままにしておくとundefinedを返してエラーになったり意図しない表示になる可能性があるのでdefaultをいれることでそれを防ぐ。

App.vue
<template>
<TodoFilter
    :current-filter="filter"
    @set-filter="filter = $event" />
</template>

🌻 完了状態の切り替え・タスクの編集状態の切り替え

App.vue
<script setup>
// 完了状態の切り替え
const toggleComplete = (id) => {
  const todo = todos.value.find(t => t.id === id);
  if (todo) todo.completed = !todo.completed;
};
</script>

引数idは、どのタスクを完了・未完了にするかを指定する。タスクの追加で定義したaddTodoid: Date.now()と連携する。
todos.valueは全部のタスクが入ってる配列。.find()メソッドで配列の中からidが一致するタスクを1つだけ探してくる。

.find()配列.find(要素 => 条件)として使用する。配列の中身を1つずつ取り出して、条件がtrueになった最初の要素を返してくれる。

todos.value.find(t => t.id === id);部分を要約すると、t配列のオブジェクトを順番に受け取るための変数がt.idそのタスクのidが引数のidと一致するタスクを返す。

一致しているならif (todo) todo.completed = !todo.completed;部分で、todoが存在すれば処理を開始し、false(未完了)がtrue(完了)に。truefalseに反転する。

App.vue
<script setup>
// タスクの編集
const editTodo = ({ id, newTitle }) => {
  const todo = todos.value.find(t => t.id === id);
  if (!todo) return;

  if (!newTitle.trim()) {
    todo.isEditing = false;
    return;
  }

  todo.title = newTitle.trim();
  todo.isEditing = false;
};

</script>

タスクの編集の切り替えも同様に、addTodoidnewTitle(追加したタスク)を取得し条件が一致しているなら処理を開始。しかしここでは「編集」を行いたいためnewTitleを上書きしたい。なのでtodoの条件が当てはまるなら、todo.title = newTitle;でタイトルの上書きとtodo.isEditing = false;編集をtrueからfalseにする。

if (!todo) return;:該当するタスク(id)が存在しなければ処理を行わずに終了させる
if (!newTitle.trim()) { ... return; }:空白や未入力状態なら(!newTitle.trim())保存せずに編集を喜屋武節して元の表示に戻す
有効な入力の場合のみtodo.titleを更新し、todo.isEditing = false;で編集モードを終了する

App.vue
<template>
<TodoList
    :todos="filteredTodos"
    @toggle-complete="toggleComplete"
    @edit-todo="editTodo"
    @delete-todo="openModal" />
</template>

🚮 タスク削除のモーダル表示

App.vue
<script setup>
// 削除モーダルを開く
const openModal = (id) => {
  todoDelete.value = id;
  isModalVisible.value = true;
};

// モーダルを閉じる
const closeModal = () => {
  isModalVisible.value = false;
  todoDelete.value = null;
};

// 削除確定
const deleteTodoConfirmed = () => {
  todos.value = todos.value.filter(t => t.id !== todoDelete.value);
  closeModal();
};
</script>

・削除モーダルを開く
引数idはどのタスクを削除するのかを見つけるもの。その削除したいタスクのidを一時的に保存するためにtodoDelete.value = id;で定義する。isModalVisible.value = trueにすることでモーダルが表示される。

・削除モーダルを閉じる
isModalVisible.value = false;モーダルを非表示にする。todoDelete.value = nullに戻すことで削除対象をリセットする。

・削除確定
実際にタスクを削除する処理。.filter()で削除対象以外のタスクだけを残す新しい配列を作成。t.idtodoDelete.valueと一致しないものだけを残す=一致したものだけが消える。最後にcloseMdal()を呼んでモーダルを閉じる。

App.vue
<template>
<ConfirmModal
    :is-visible="isModalVisible"
    @confirm="deleteTodoConfirmed"
    @cancel="closeModal" />
</template>

💡 テーマカラーの定義

App.vue
<script setup>
const THEME_COLORS = {
  white: {
    bg: '#fffafa',
    text: '#2c3e50',
  },
  black: {
    bg: '#000',
    text: '#fff',
  },
  orange: {
    bg: '#f6d55c',
    text: '#2c3e50',
  }
};
</script>

ここでテーマカラーである「白」「黒」「オレンジ」を定義。それぞれ個別の設定としてbackgroundとtextColorを指定する。
後述するが、HTML要素に対してカラーを適用させるためアプリ全体で共通に使うものとして大文字表記する。

これも後述するが、その値('white','black','orange')を受け取ってTHEME_COLORSからその各テーマの色データを取り出す。


🎨 選ばれたテーマカラーに応じてサイトの色を変える関数を定義

App.vue
<script setup>
const applyThemeColors = (currentTheme) => {
  const colors = THEME_COLORS[currentTheme];
  if (!colors) return;

  const root = document.documentElement;
  root.style.setProperty('--theme-bg-color', colors.bg);
  root.style.setProperty('--theme-text-color', colors.text);
};
</script>

引数currentThemeを定義してテーマカラーを受け取る。登録してない色が指定されてエラーがでないようにif (!colors) return;で処理を終了することを定義しておく。document.documentElement<html>タグを取得する変数を定義。

.style.setProperty(プロパティ名, 値);今回はプロパティ名にCSS変数・値に定義してるcolors(THEME_COLORS)の中身のbgとtextを適用している状態。


🐌 CSS変数をbody要素で適用する

App.vue
<style>
body {
  background-color: var(--theme-bg-color);
  color: var(--theme-text-color);
  margin: 0;
  min-height: 100vh;
  transition: background-color 0.3s, color 0.3s;
}
/* body要素以外は任意のスタイリングをしてください */
</style>

--theme-bg-color--theme-text-color(CSS変数)を使うためにbody要素に値として登録しておくとJSで指定しているプロパティとして適用される。


🙏 CSS変数の保険としてクラス変更でカラーテーマ変更の定義

App.vue
<script setup>
const themeClass = computed(() => `theme-${theme.value}`);
</script>

theme.valueには選択中のテーマ名がはいる。computed()で値を計算することで常に選択中(最新)のテーマに応じたクラス名を返す。
今回はCSS変数で設定してるためここは利用しませんが、保険として記述を残しておくのが一般的だそうです。


⏰ テーマカラーの更新

App.vue
<script setup>
const updateTheme = (newTheme) => {
  theme.value = newTheme;
  applyThemeColors(newTheme);
};
</script>

適用させたいカラーを選択したら変更される部分の定義。refで管理してるテーマカラーを選択されたものに更新する。
applyThemeColorsでCSS変数を定義しているため選択したカラーによって値が書き換わる仕組み。

🧏‍♀️ テーマカラーのまとめ
JSでCSS変数(--変数名)に色をセットする
CSSでvar(--変数名)を指定
これによってJSの値が変わるとCSSの値を自動で反映する
ページの背景や文字色が即時に切り替わる


👀 テーマカラーと入力したタスクをローカルストレージに保存

App.vue
<script setup>
// themeが変わったらローカルストレージに保存
watch(theme, (newTheme) => {
  localStorage.setItem(THEME_KEY, newTheme);
});

// todosが変わったらローカルストレージに保存
watch(todos, (newTodos) => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(newTodos));
}, { deep: true });
</script>

watch(変数, コールバック関数)を使ってリアクティブ変数を監視し変更された値をローカルストレージに保存します。

テーマカラーの保存
themeが変わるたびにコールバックを実行する。コールバックの引数newThemeには常に最新のテーマ名がはいる。
localStorage.setItem(THEME_KEY, newTheme);:ブラウザのローカルストレージに保存することでページをリロードしてもnewThemeが保存されているためその状態が維持される。

タスクの保存
todosが変わるたびにコールバックを実行。コールバックの引数newTodosには最新のタスクの配列がはいる。
JSON.stringify(newTodos):配列やオブジェクトを文字列に変換する処理してlocalStorage.setItem()でブラウザに保存。
{ deep: true }:配列やオブジェクトの変更を監視するためのwacthのオプション。デフォルトの状態だろ変数の変更しか監視されないため、このコードをいれることで内部まで監視してくれる。


⛰ onMounted

App.vue
<script setup>
onMounted(() => {
 // タスクの読み込み
  const savedTodos = localStorage.getItem(STORAGE_KEY);
  if (savedTodos) {
    todos.value = JSON.parse(savedTodos);
  }

 // テーマの読み込み
  const savedTheme = localStorage.getItem(THEME_KEY);
  if (savedTheme) {
    theme.value = savedTheme;
  }

  applyThemeColors(theme.value);
});
</script>

onMounted(() => {});:コンポーネントがマウントされた直後に処理を実行できる。今回は画面が表示されると同時にデータを読み込むために使用。

localStorage.getItem(STORAGE_KEY):ブラウザのローカルストレージから保存済みのタスクを取得。JSON.parseで文字列で保存していたものを配列オブジェクトに戻すことで画面にタスクが反映される。

テーマカラーについても上記と同じでlocalStorage.getItem()でテーマを取得し、反映する。

if (savedTodos)``if (savedTheme)でif文を使う理由は、localStorage.getItem(キー名)はキーが存在しない場合nullを返してしまうため、値があるときだけ代入するためにif文を使って「保存されていない状態」を避けるために使用している。

applyThemeColors(theme.value);applyThemeColorsは上記で定義したCSS変数を適用する関数。theme.valueに入っているテーマをCSS変数に反映することでロード・リロード時に即時に適用される。

App.vue
<template>
<SettingMenu
    @change-theme="updateTheme" />
</template>

Discussion