これまでTodoアプリを作成し、App.vueに全機能を記載してからコンポーネント化するような流れで作成してきました。実際の工程では最初からコンポーネント化してしまった方が楽な部分も多いので、最初からコンポーネント化してしまうケースが多いように感じています。
今回はデザインを凝ったりはしていないので基本的なTodoアプリの機能を盛り込んだものですが、このアプリのコードを理解できればVue.jsの機能の基本的な部分は理解できるようになると思います。
以下に完成イメージとファイル構造、全体のコードを記載しておくので参考にしてみてください。
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>
<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; /* ホバー時に少し暗く */
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin: 8px 0;
background-color: #f3f3f3;
padding: 10px;
border-radius: 4px;
}
.pastDue {
background-color: rgb(237, 176, 176);
}
.errorDescription {
color: red;
}
</style>
C:/npm_sample/src/componentsTodoInput.vue
<template>
<div class="input-area">
<input type="text" v-model="newTodo" @keyup.enter="addTodo">
<input type="date" v-model="newTodoDueDate">
<button @click="addTodo">追加</button>
</div>
</template>
<script>
export default {
data() {
return {
newTodo: '',
newTodoDueDate: ''
};
},
methods: {
addTodo() {
this.$emit('add-todo', {
text: this.newTodo,
dueDate: this.newTodoDueDate
});
},
resetInput() {
this.newTodo = '';
this.newTodoDueDate = '';
}
}
}
</script>
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>
C:/npm_sample/src/components/TodoList.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>
コメント