前回の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>
- データプロパティ:
localTodo
は、親コンポーネントから受け取ったtodo
のローカルコピーを作成します。これにより、編集中に元のデータに影響を与えることなく変更を加えることができます。
- メソッド:
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アイテムに関連する操作を親コンポーネントに伝えるためのメソッドが定義されています。これらのメソッドは、特定の操作が発生したことを示すイベントを親コンポーネントに発火します。
以上の編集を行うことで一覧表示の部分をコンポーネント化することができます。
コメント