I have put together a To-do Application with the Slim framework on the back-end (API) and a Vue 3 front-end. I added a demo on my YouTube channel.
In the main App.vue file I have:
<template>
<div id="app">
<Header
title="My todo list"
:unsolvedTodos = unsolvedTodos
/>
<List
:todos="todos"
:dataIsLoaded=dataIsLoaded
@delete-todo="deleteTodo"
@toggle-todo="toggleTodo" />
<Footer
:isValidInput=isValidInput
newTitle = ""
placeholder= "+ Add new todo"
validationMsg = "Please add at least 3 characters"
@add-todo="addTodo"
/>
</div>
</template>
<script>
import axios from 'axios'
import '@fortawesome/fontawesome-free/js/all.js';
import Header from './components/Header.vue'
import List from './components/List.vue'
import Footer from './components/Footer.vue'
export default {
name: 'App',
components: {
Header,
List,
Footer
},
data() {
return {
apiUrl: "http://todo.com/api",
dataIsLoaded: false,
isValidInput: true,
todos: [],
unsolvedTodos: [],
}
},
methods: {
showTodos: function(){
axios.get(`${this.apiUrl}/todos`)
.then((response) => {
this.todos = response.data;
})
.then(this.getUnsolvedTodos)
.then(this.dataIsLoaded = true);
},
getUnsolvedTodos: function(){
this.unsolvedTodos = this.todos.filter(todo => {
return todo.completed == 0;
});
},
toggleTodo: function(todo) {
let newStatus = todo.completed == "0" ? 1 : 0;
axios.put(`${this.apiUrl}/todo/update/${todo.id}`, {
title: todo.title,
completed: newStatus
})
},
deleteTodo: function(id) {
axios.delete(`${this.apiUrl}/todo/delete/${id}`)
},
addTodo: function(newTitle){
const newToDo = {
title: newTitle,
completed: 0
}
if(newTitle.length > 2){
this.isValidInput = true;
axios.post(`${this.apiUrl}/todo/add`, newToDo);
} else {
this.isValidInput = false;
}
}
},
created() {
this.showTodos();
},
watch: {
todos() {
this.showTodos();
}
}
}
</script>
In Header.vue:
<template>
<header>
<span class="title">{{title}}</span>
<span class="count" :class="{zero: unsolvedTodos.length === 0}">{{unsolvedTodos.length}}</span>
</header>
</template>
<script>
export default {
props: {
title: String,
unsolvedTodos: Array
},
}
</script>
In Footer.vue:
<template>
<footer>
<form @submit.prevent="addTodo()">
<input type="text" :placeholder="placeholder" v-model="newTitle">
<span class="error" v-if="!isValidInput">{{validationMsg}}</span>
</form>
</footer>
</template>
<script>
export default {
name: 'Footer',
props: {
placeholder: String,
validationMsg: String,
isValidInput: Boolean
},
data () {
return {
newTitle: '',
}
},
methods: {
addTodo() {
this.$emit('add-todo', this.newTitle)
this.newTitle = ''
}
}
}
</script>
The to-do list (List.vue):
<template>
<transition-group name="list" tag="ul" class="todo-list" v-if=dataIsLoaded>
<TodoItem v-for="(todo, index) in todos"
:key="todo.id"
:class="{done: Boolean(Number(todo.completed)), current: index == 0}"
:todo="todo"
@delete-todo="$emit('delete-todo', todo.id)"
@toggle-todo="$emit('toggle-todo', todo)"
/>
</transition-group>
<div class="loader" v-else></div>
</template>
<script>
import TodoItem from "./TodoItem.vue";
export default {
name: 'List',
components: {
TodoItem,
},
props: {
dataIsLoaded: Boolean,
todos: Array
},
emits: [
'delete-todo',
'toggle-todo'
]
}
</script>
The single to-do item (TodoItem.vue):
<template>
<li>
<input type="checkbox" :checked="Boolean(Number(todo.completed))" @change="$emit('toggle-todo', todo)" />
<span class="title">{{todo.title}}</span>
<button @click="$emit('delete-todo', todo.id)">
<i class="fas fa-trash-alt"></i>
</button>
</li>
</template>
<script>
export default {
name: 'TodoItem',
props: {
todo: Object
}
}
</script>
Questions / concerns:
- Is the application well-structured?
- Could the code be significantly "shortened"?
v-bind="$attrs"
? \$\endgroup\$