PR
スポンサーリンク

Vue.jsでTodoアプリを作る(一覧表示のコンポーネント化)

前回のVue.jsでTodoアプリを作る(フォーム部分のコンポーネント化)でフォーム入力の箇所をコンポーネント化しました。今回は一覧表示されている箇所をコンポーネント化して親コンポーネントのApp.vueをよりすっきりとした形にしていきたいと思います。

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

細かいcssなどの修正したのでその部分は変化していますが、大きな変化は見られないと思います。

次に、VSCode上でのファイル構造を見てみましょう。

前回からの変更点としては新たにTodoItem.vueとTodoList.vueが追加されたことになります。

では、実際に各ファイルの中身を見ていきましょう。

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>
    <todo-input @add-todo="addTodo" ref="todoInput" />
    <todo-list
      :todos="todos"
      :originalTodos="originalTodos"
      @toggle-completed="toggleCompleted"
      @edit-todo="editTodo"
      @delete-todo="deleteTodo"
      @save-edit="saveEdit"
      @sort-todos="sortTodos"
    />
  </div>
</template>



<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';

export default {
  components: {
    TodoInput,
    TodoList
  },
  data() {
    return {
      newTodo: '',
      newTodoDueDate: '',
      todos: [],
      originalTodos: [],
      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(newTodo) {
      this.todoErrors = [];
      if (!newTodo.text.trim()) {
        this.todoErrors.push('Todoを入力してください');
      }
      if (!newTodo.dueDate) {
        this.todoErrors.push('期限を入力してください');
      }
      if (this.todoErrors.length === 0) {
        this.todos.push({
          id: this.todos.length + 1,
          text: newTodo.text,
          dueDate: newTodo.dueDate,
          completed: false,
          editing: false,
          buttonText: '完了'
        });
        this.saveTodos();
        this.$refs.todoInput.resetInput(); 
      }
    },
    toggleCompleted(todoId) {
      const todo = this.todos.find(todo => todo.id === todoId);
      if (todo) {
        const updatedTodo = { ...todo, completed: !todo.completed };
        this.updateTodo(updatedTodo);
      }
    },
    deleteTodo(todoId) {
      // IDを基にして正確なインデックスを見つける
      const index = this.todos.findIndex(todo => todo.id === todoId);
      if (index !== -1) {
        // 確認ダイアログを表示
        if (window.confirm("本当に削除しますか?")) {
          // 配列から正確な位置のTodoを削除
          this.todos.splice(index, 1);
          this.saveTodos();
        }
      }
    },
    editTodo(todoId) {
      const todo = this.todos.find(todo => todo.id === todoId);
      if (todo) {
        const updatedTodo = { ...todo, editing: true };
        this.updateTodo(updatedTodo);
      }
    },
    saveEdit(updatedTodo) {
      const editedTodo = { ...updatedTodo, editing: false };
      this.updateTodo(editedTodo);
    },
    isPastDue(dueDate) {
      const today = new Date();
      today.setHours(0, 0, 0, 0); // 時間をリセットして今日の日付のみにする
      const due = new Date(dueDate);
      return due < today; //期限が今日より前であればtrueを返す
    },
    sortTodos(sortKey) {
      this.sortKey = sortKey // 受け取った sortKey を保存
      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
      }
    },
    updateTodo(updatedTodo) {
      const index = this.todos.findIndex(todo => todo.id === updatedTodo.id);
      if (index !== -1) {
        this.todos.splice(index, 1, updatedTodo);
        this.saveTodos();
      }
    },
  }
}
</script>

※css部分は省略しています。

C:/npm_sample/src/components/TodoItem.vue

<template>
    <li :class="{ 'completed': todo.completed, 'pastDue': isPastDue(todo.dueDate), 'editing': todo.editing }">
      <div class="todo-container" v-if="todo.editing">
        <input type="text" v-model="localTodo.text">
        <input type="date" v-model="localTodo.dueDate">
        <button @click="saveEdit">保存</button>
      </div>
      <div class="todo-content" v-else>
        <span>{{ todo.text }}</span>
        <span class="due-date">- 期限: {{ todo.dueDate }}</span>
        <div class="buttons">
          <button :class="{ 'completeButton': !todo.completed, 'completed': todo.completed }" @click="$emit('toggle-completed', todo.id)">
            {{ todo.completed ? '未完了に戻す' : '完了' }}
          </button>
          <button @click="$emit('edit-todo', todo.id)">編集</button>
          <button @click="$emit('delete-todo', todo.id)" class="deleteButton">削除</button>
        </div>
      </div>
    </li>
</template>

  
  <script>
  export default {
    props: ['todo'],
    data() {
      return {
        localTodo: {...this.todo}  // ローカルの変更可能なコピーを作成
      };
    },
    methods: {
      isPastDue(dueDate) {
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        return new Date(dueDate) < today;
      },
      saveEdit() {
        this.$emit('save-edit', this.localTodo);  // 更新されたtodoをemit
      }
    }
  }
  </script>
  <style scoped>
  .todo-container {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .buttons {
    display: flex;
    align-items: center;
    justify-content: flex-end; /* ボタンを右側に配置 */
  }
  
  button {
    margin-left: 10px;
  }
  
  .completed {
    background-color: #5e626f; /* 完了時のボタンの背景色 */
  }

  .completeButton {
    background-color: rgb(24, 130, 243); /* 青色のボタン */
    color: white; /* テキスト色 */
    padding: 10px 15px; /* パディング */
    border: none; /* 枠線なし */
    border-radius: 4px; /* 角を丸く */
    cursor: pointer; /* マウスカーソルを指に */
    font-size: 16px;
  }
  
  .completeButton:hover {
    background-color: #1a82e2; /* ホバー時に少し暗くする */
  }
  
  .completed {
    background-color: #d4edda; /* 完了項目の背景色 */
    color: #155724; /* 完了項目のテキスト色 */
  }
  
  .editButton, .deleteButton {
    background-color: #4CAF50; /* 緑色のボタン */
    color: white; /* テキスト色 */
  }
  
  .deleteButton {
    background-color: rgb(243, 24, 24); /* 赤色のボタン */
  }
  
  .editButton:hover, .deleteButton:hover {
    opacity: 0.85; /* ホバー時に少し透明にする */
  }
  </style>
  1. データプロパティ:
    • localTodoは、親コンポーネントから受け取ったtodoのローカルコピーを作成します。これにより、編集中に元のデータに影響を与えることなく変更を加えることができます。
  2. メソッド:
    • isPastDueメソッドは、指定された期限日が今日の日付より前かどうかを判断します。
    • saveEditメソッドは、編集されたToDoデータを親コンポーネントに送信し、更新を求めます。

C:/npm_sample/src/componentsTodoList.vue

<template>
    <div>
      <select v-model="sortKey" @change="sortTodos">
        <option value="default">デフォルト</option>
        <option value="completed">完了順</option>
        <option value="dueDate">期限順</option>
      </select>
      <ul>
        <todo-item
          v-for="todo in todos"
          :key="todo.id"
          :todo="todo"
          @toggle-completed="toggleCompleted"
          @edit-todo="editTodo"
          @delete-todo="deleteTodo"
          @save-edit="saveEdit"
        ></todo-item>
      </ul>
    </div>
  </template>
  
  <script>
  import TodoItem from './TodoItem.vue';
  
  export default {
    components: {
      TodoItem
    },
    props: ['todos', 'originalTodos'],
    data() {
      return {
        sortKey: 'default'
      };
    },
    methods: {
      sortTodos() {
        this.$emit('sort-todos', this.sortKey);
      },
      toggleCompleted(id) {
        this.$emit('toggle-completed', id);
      },
      editTodo(id) {
        this.$emit('edit-todo', id);
      },
      deleteTodo(id) {
        this.$emit('delete-todo', id);
      },
      saveEdit(todo) {
        this.$emit('save-edit', todo);
      }
    }
  }
  </script>
  

selectタグの箇所では、リストを異なる基準でソートするためのオプションをユーザーに提供します。ユーザーがオプションを選択すると、sortKey データプロパティが更新され、sortTodos メソッドが呼ばれます。このメソッドは選択されたソートオプションに基づいてリストをソートするイベントを親コンポーネントに伝えます。

TodoItem コンポーネントをインポートし、ローカルコンポーネントとして登録しています。これにより、TodoList コンポーネント内で todo-item タグを使用できるようになります。

data メソッドは、このコンポーネントのローカル状態である sortKey を返します。これは選択されたソートオプションを保持します。

methods オブジェクトには、各ToDoアイテムに関連する操作を親コンポーネントに伝えるためのメソッドが定義されています。これらのメソッドは、特定の操作が発生したことを示すイベントを親コンポーネントに発火します。

以上の編集を行うことで一覧表示の部分をコンポーネント化することができます。

コメント

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