Note: This is my first project while learning Vue 3, so it probably won’t be “best practices”. Still, I hope you can learn something from this post. And I can probably learn something from you as well, just leave some comments below!
See the completed project here: https://taches.surge.sh/
Following the trend of using French names (Vue and Vite), I named the project “Tâches” (tasks). There will be no more French words, I promise!
To begin, create a new Vite project:
npm init vite-app taches
cd taches
npm install
npm run dev
Next, you will want to replace HelloWorld
in src/App.vue
with your own <TodoList />
component:
// src/App.vue
<template>
<TodoList />
</template>
<script>
import TodoList from './components/TodoList.vue'
export default {
name: 'App',
components: {
TodoList
}
}
</script>
Now, let’s write src/components/TodoList.vue
Our template will contain a simple table with 3 columns:
- a checkbox for marking the todo as done
- the description of the todo
- an action button (Add in the first row and Delete in the remaining rows, when done is checked)
// src/components/TodoList.vue
<template>
<h1>Tâches</h1>
<table>
<tr>
<td></td>
<td><input v-model="newDescription" placeholder="Add a description..."></td>
<td><button @click="addTodo">Add Todo</button></td>
</tr>
<Todo v-for="todo in todos" :data="todo" v-on:delete-todo="deleteTodo(todo.id)">
</Todo>
</table>
</template>
There's a lot of Vue-specific attributes above, so let's take a closer look:
-
v-model="newDescription"
ties the text input's value to a variable namednewDescription
that I will show below. -
@click="addTodo"
is a click event listener that will calladdTodo()
when this button is clicked -
v-for="todo in todos"
iterates overtodos
(a list that will be defined below). -
:data="todo"
binds the value oftodo
as a prop to the componentTodo
. -
v-on:delete-todo="deleteTodo(todo.id)
listens to whendelete-todo
is emitted by theTodo
component.
Now, let's tackle the <script>
part of the TodoList
component! In this demo, I will use localStorage
to persist the todos. I just couldn't figure out a simple enough REST API setup that's easy and free to use, so your todos will be stuck to whatever device you used to access this project.
Let's start with the imports. I will use ref
, reactive
, and watch
.
// src/components/TodoList.vue
<template>
// ...
</template>
<script>
import { ref, reactive, watch } from "vue";
import Todo from './Todo.vue';
// ...
</script>
Todo.vue
holds the component source that represents a single todo item. It's very simple and I'll leave it for last.
As mentioned, I use localStorage for data persistence. Loading and saving data is done like this:
// src/components/TodoList.vue
// ...
<script>
import { ref, reactive, watch } from "vue";
import Todo from './Todo.vue';
function loadTodos() {
const localTodos = localStorage.getItem("_taches_todos");
if (localTodos === null) {
return [];
} else {
console.log("loadTodos loaded: " + localTodos);
return JSON.parse(localTodos);
}
}
function saveTodos(todos) {
localStorage.setItem("_taches_todos", JSON.stringify(todos));
}
// ...
todos
is a list of todo items, where an item is an object like this:
{"id":1595202004079,"done":true,"description":"write a blog post"}
id
is a timestamp created withDate.now()
. Since we use localStorage and assuming the system time is never changed, it should be unique.done
is the state of the checkbox, representing whether the todo is completed or not.description
describes the todo.
So let's get to the Vue part already!
Right after the localStorage functions, add these lines:
// src/components/TodoList.vue
// ...
export default {
setup() {
const newDescription = ref('');
const todos = reactive(loadTodos());
function addTodo() {
todos.push({ id: Date.now(), done: false, description: newDescription.value });
newDescription.value = '';
}
function deleteTodo(id) {
console.log("Delete todo with id: " + id);
for (let i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
}
watch(todos, (newTodos, prevTodos) => {
saveTodos(newTodos);
});
return {
todos, newDescription, addTodo, deleteTodo
}
},
components: {
Todo
}
}
</script>
I am using the new setup()
, and inside I define the reactive variables newDescription
and todos
.
Note: The proper use of ref
and reactive
is not clear to me. Some people claim to always use one or the other. They both must have their proper use cases, please search around for more info.
From what I gathered, ref
is used for scalar types (primitives), while reactive
is more appropriate for objects and arrays.
newDescription
is used to create new todos, while todos
is an array that holds all the data.
addTodo()
appends a new todo object to the todos
array. Note that I don't pass any arguments--the value of the reactive variable newDescription
, tied to the text input, is used.
deleteTodo
takes the id
associated with the <Todo>
element, and the todos
array is spliced so that the chosen todo object is removed.
I am specifically using todos
in watch
instead of watchEffect
, because I only care about todos
changing.
Inside watch
, I save the current state of the todos
array in localStorage.
Finally, we return the pieces used in the template: todos
, newDescription
, addTodo
and deleteTodo
.
The Todo
component is also used, so it must be added to components
:
// src/components/TodoList.vue
// ...
components: {
Todo
}
}
</script>
Speaking of Todo
, this component looks like this (it's saved in src/components/Todo.vue
).
<template>
<tr>
<td><input type="checkbox" v-model="data.done" :checked="data.done"></td>
<td>{{ data.description }}</td>
<td v-if="data.done"><button @click="$emit('delete-todo')">Delete</button></td>
</tr>
</template>
<script>
export default {
props: {
data: Object
}
}
</script>
The information used in each Todo
is passed as a data
prop from TodoList
.
The checkbox tracks
data.done
, and itschecked
attribute matches the state ofdata.done
as well.The value of the description is obtained with
{{ data.description }}
The Delete button only appears if
data.done
istrue
, controlled byv-if
.$emit
sends'delete-todo'
to its parent,TodoList
, which will then calldeleteTodo(todo.id)
.
Finally, we just need to say that data
is an Object inside props: { ... }
.
A minor detail in the default CSS will center the text of each description. You may edit src/index.css
and remove text-align: center;
if it annoys you (it did annoy me).
And that's all! The source code is available at https://github.com/heitorchang/taches and again, a built page is available at https://taches.surge.sh .
To build your project, just run npm run build
. Until next time!
Top comments (0)