DEV Community

Heitor Chang
Heitor Chang

Posted on

Tâches: a Vue 3 To-do List with Vite

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 named newDescription that I will show below.
  • @click="addTodo" is a click event listener that will call addTodo() when this button is clicked
  • v-for="todo in todos" iterates over todos (a list that will be defined below).
  • :data="todo" binds the value of todo as a prop to the component Todo.
  • v-on:delete-todo="deleteTodo(todo.id) listens to when delete-todo is emitted by the Todo 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 with Date.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 its checked attribute matches the state of data.done as well.

  • The value of the description is obtained with {{ data.description }}

  • The Delete button only appears if data.done is true, controlled by v-if. $emit sends 'delete-todo' to its parent, TodoList, which will then call deleteTodo(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)