PR
スポンサーリンク

Vue.jsでTodoアプリを作る(ソート機能の追加)

前回、Vue.jsでTodoアプリを作る(バリデーションの追加)でバリデーションの追加を行いました。今回は期限や完了・未完了でソートができる機能を追加しています。

全体のコードを載せるので参考にしてください。

イメージは以下の画像の通りです。

全体のコードは以下の通りです。

C:/npm_sample/src/App.vue

<template>
  <div id="app">
    <h1>ToDo List</h1>
    <ul>
      <li v-for="todoError in todoErrors" :key="todoError" class="errorDescription">
        {{ todoError }}
      </li>
    </ul>
    <div class="input-area">
      <input type="text" v-model="newTodo" @keyup.enter="addTodo">
      <input type="date" v-model="newTodoDueDate">
      <button @click="addTodo">追加</button>
    </div>

  <div>
    <select v-model="sortKey" @change="sortTodos">
      <option value="default">デフォルト</option>
      <option value="completed">完了順</option>
      <option value="dueDate">期限順</option>
    </select>
  </div>

    <ul>
      <li v-for="(todo, index) in todos" :key="index" 
        :class="{ 'completed': todo.completed, 'pastDue': isPastDue(todo.dueDate), 'editing': todo.editing }">
        <div class="todo-container">
          <template v-if="todo.editing">
            <input type="text" v-model="todo.text">
            <input type="date" v-model="todo.dueDate">
            <button @click="saveEdit(todo)">保存</button>
          </template>
          <template v-else>
            <div class="todo-content">
              <span>{{ todo.text }}</span>
              <span class="due-date">- 期限: {{ todo.dueDate }}</span>
            </div>
            <div class="buttons">
              <button @click="toggleCompleted(index)"
                :class="{'completeButton': true, 'completed': todo.completed}">{{ todo.buttonText }}</button>
              <button @click="editTodo(todo)">編集</button>
              <button @click="deleteTodo(index)" class="deleteButton">削除</button>
            </div>
          </template>
        </div>
      </li>
    </ul>
  </div>
</template>



<script>
export default {
  data() {
    return {
      newTodo: '',
      newTodoDueDate: '',
      todos: [],
      todoErrors: [],
      sortKey: 'default' // ソートキーの初期値
    }
  },
  created() {
    this.loadTodos();
  },
  methods: {
    loadTodos() {
      const todos = localStorage.getItem('todos');
      if (todos) {
        this.todos = JSON.parse(todos);
        this.originalTodos = JSON.parse(todos);  // ロード時にオリジナルのリストを保存
      }
    },
    saveTodos() {
      localStorage.setItem('todos', JSON.stringify(this.todos));
      this.originalTodos = [...this.todos];  // 保存時にもオリジナルを更新
    },
    addTodo() {
      this.todoErrors = [];  // 新しいTodoを追加する前にエラー配列をクリア
      if (!this.newTodo.trim()) {
        this.todoErrors.push('Todoを入力してください');
      }
      if (!this.newTodoDueDate) {
        this.todoErrors.push('期限を入力してください');
      }
      if (this.todoErrors.length === 0) { // エラーがない場合のみTodoを追加
        // 新しいToDo項目を追加するとき、buttonTextプロパティを含めます
        this.todos.push({ 
          text: this.newTodo,
          completed: false,
          buttonText: '完了',
          dueDate: this.newTodoDueDate,
          editing: false
        });
        this.newTodo = ''; // フィールドをリセット
        this.newTodoDueDate = ''; // 期限日フィールドをリセット
        this.saveTodos()
      } 
    },
    toggleCompleted(index) {
      const todo = this.todos[index];
      todo.completed = !todo.completed; // 完了状態を切り替え
      todo.buttonText = todo.completed ? '未完了' : '完了'; // ボタンテキストも更新
      this.saveTodos();
    },
    deleteTodo(index) {
      if (window.confirm("本当に削除しますか?")) {
        this.todos.splice(index, 1);
        this.saveTodos()
      }
    },
    editTodo(todo) {
      todo.editing = true;
    },
    saveEdit(todo) {
      this.todoErrors = [];
      if (!todo.text) {
        this.todoErrors.push('Todoを入力してください');
      }
      if (!todo.dueDate) {
        this.todoErrors.push('期限を入力してください')
      }
      if (this.todoErrors.length === 0) {
        todo.editing = false
        this.saveTodos()
      }
    },
    isPastDue(dueDate) {
      const today = new Date();
      today.setHours(0, 0, 0, 0); // 時間をリセットして今日の日付のみにする
      const due = new Date(dueDate);
      return due < today; //期限が今日より前であればtrueを返す
    },
    sortTodos() {
      if (this.sortKey === 'default') {
        this.todos = [...this.originalTodos];  // デフォルト時はオリジナルのリストを復元
      } else {
        let sortedTodos = [...this.originalTodos];  // ソートはオリジナルから始める
        if (this.sortKey === 'completed') {
          sortedTodos.sort((a, b) => (a.completed === b.completed ? 0 : a.completed ? 1 : -1));
        } else if (this.sortKey === 'dueDate') {
          sortedTodos.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
        }
        this.todos = sortedTodos;  // ソートされた配列を元の配列に再代入
      }
    },
  }
}
</script>

<style>
#app {
  max-width: 600px;
  margin: 0 auto;
  font-family: 'Helvetica Neue', Arial, sans-serif;
}

.input-area {
  display: flex;
  margin-bottom: 20px;
}

input[type="text"], input[type="date"] {
  flex: 1;
  padding: 10px;
  border: 2px solid #ccc;
  border-radius: 4px;
  margin-right: 5px;
}

/* フォーカス時のスタイル */
input[type="date"]:focus {
  outline: none;
  border-color: #4CAF50;
}

button {
  background-color: #4CAF50;
  border: none;
  color: white;
  padding: 10px 15px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  cursor: pointer;
  border-radius: 4px;
  margin-left: 5px;
}

.todo-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.todo-content {
  display: flex;
  flex-direction: column;
}

.due-date {
  color: #666;
  font-size: 0.9em;
}

.buttons {
  display: flex;
  align-items: center;
}

button {
  background-color: #4CAF50; /* 緑色のボタン */
  border: none; /* 枠線なし */
  color: white; /* テキスト色 */
  padding: 10px 15px; /* パディング */
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  transition-duration: 0.4s; /* ホバー効果のためのトランジション */
  cursor: pointer; /* マウスカーソルを指に */
  border-radius: 4px; /* 角を丸く */
}

button:hover {
  background-color: #45a049; /* ホバー時に少し暗く */
}

.completeButton {
  background-color: rgb(24, 130, 243); /* 青色のボタン */
  border: none; /* 枠線なし */
  color: white; /* テキスト色 */
  padding: 10px 15px; /* パディング */
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  transition-duration: 0.4s; /* ホバー効果のためのトランジション */
  cursor: pointer; /* マウスカーソルを指に */
  border-radius: 4px; /* 角を丸く */
}

.deleteButton {
  background-color: rgb(243, 24, 24); /* 赤色のボタン */
  border: none; /* 枠線なし */
  color: white; /* テキスト色 */
  padding: 10px 15px; /* パディング */
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  transition-duration: 0.4s; /* ホバー効果のためのトランジション */
  cursor: pointer; /* マウスカーソルを指に */
  border-radius: 4px; /* 角を丸く */
}

/* ホバー時のスタイル */
.completeButton:hover,
.deleteButton:hover {
  opacity: 0.85; /* ボタンを少し透明にすることでホバー効果を強調 */
}

.completeButton, .deleteButton {
  margin-left: 10px;
}

.completed {
  background-color: #d4edda; /* 完了項目の背景色 */
  color: #155724; /* 完了項目のテキスト色 */
  text-decoration: line-through; /* 完了項目に取り消し線を追加 */
}

/* 完了ボタンのスタイルを条件によって変更するためのスタイルを追加 */
.completeButton.completed {
  color: white;
  background-color: #2846a7; /* 完了時のボタンの背景色 */
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  margin: 8px 0;
  background-color: #f3f3f3;
  padding: 10px;
  border-radius: 4px;
}

.pastDue {
  color: red;
}

.errorDescription {
  color: red;
}
</style>

以下からソート機能の部分だけを切り出して解説をします。

<select v-model="sortKey" @change="sortTodos">
  <option value="default">デフォルト</option>
  <option value="completed">完了順</option>
  <option value="dueDate">期限順</option>
</select>

この部分では、select エレメントを使用してソートオプションを提供します。v-model は選択されたソートオプションを sortKey というデータプロパティにバインドします。@change イベントはユーザーがオプションを選択するたびに sortTodos メソッドをトリガーします。

data() {
  return {
    sortKey: 'default' // ソートキーの初期値
  }
},
methods: {
  sortTodos() {
    if (this.sortKey === 'default') {
      this.todos = [...this.originalTodos]; // デフォルト時はオリジナルのリストを復元
    } else {
      let sortedTodos = [...this.originalTodos]; // ソートはオリジナルから始める
      if (this.sortKey === 'completed') {
        sortedTodos.sort((a, b) => (a.completed === b.completed ? 0 : a.completed ? 1 : -1));
      } else if (this.sortKey === 'dueDate') {
        sortedTodos.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
      }
      this.todos = sortedTodos; // ソートされた配列を元の配列に再代入
    }
  }
}

sortTodos メソッドは、選択された sortKey に基づいて異なるソートアルゴリズムを実行します。デフォルトの場合、元のToDoリストをそのまま表示します。完了順の場合、完了した項目が未完了の項目より後ろに来るように並び替えます。期限順の場合、期限が近い項目から遠い項目へと並び替えます。

コメント

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