Tâches: a Vue 3 To-do List with Vite
Tags: vue
Published: 2020-07-20 01:11:30

**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:

```vue
//    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)

```vue
//    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`.

```vue
//    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:

```vue
//    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:

```vue
//    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`:

```vue
//    src/components/TodoList.vue

     // ...
     components: {
         Todo
     }
 }
</script>
```

Speaking of `Todo`, this component looks like this (it's saved in `src/components/Todo.vue`).

```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!