Habit Tracker

Welcome to the last lesson of the Vue course! In it, you'll build a whole app, step by step: a habit tracker. Habits are small things you want to do every day, such as drinking enough water or going for a walk. The app lists them, and you check off what you've done today. It shows your progress with a bar, lets you add your own habits and remove the ones you don't want, and remembers everything, even how many days in a row you've done them all.

You'll use what you've learned in the whole course: components with props and events, data from a file, computed values, a form and a composable. Each exercise continues where the previous one ended (its editor starts with the previous exercise's finished code) and adds one piece.

The plan. The habits live in App.vue, in a ref with an array of objects. Each habit has an id, a name, an icon (an emoji) and done, which says whether you've done it today. Showing one habit is the job of the component in the second tab, HabitItem.vue. It gets the habit as a prop, and it already crosses out a habit that's done: that's what <li :class="{ done: habit.done }"> and its <style scoped> are for.

When you check a habit off, HabitItem doesn't change the habit itself. The habits belong to App.vue, so the component asks its parent with an event, as in the lesson Component Events. The checkbox shows the value with :checked, and when you click it, its change event sends the event toggle with the habit's id:

<input type="checkbox" :checked="habit.done" @change="emit('toggle', habit.id)">

App.vue listens with @toggle="toggle". Its function toggle(id) finds the habit with that id and changes its done:

function toggle(id) {
    const habit = habits.value.find((item) => item.id === id);
    habit.done = !habit.done;
}

find() gives back the habit's object itself, not a copy, so changing its done changes the habit in habits. Vue then updates the page, and HabitItem shows the habit crossed out. So the data lives in one place and changes in one place, and the components only show it.

Why not v-model="habit.done", as in the lesson Forms? It would change the parent's data from inside HabitItem, and in a bigger app, you'd soon lose track of where your data changes. As in the lesson Component Events, the data goes down to the component in props, and the component asks for changes with events.

Instructions

  1. In App.vue, import the component under the other import (import HabitItem from "./HabitItem.vue";) and, in place of the comment in the ul, show each habit with it: <HabitItem v-for="habit in habits" :key="habit.id" :habit="habit" />. The page shows the three habits.

  2. In HabitItem.vue, add const emit = defineEmits(["toggle"]); under defineProps, and put the checkbox from above at the start of the label. In App.vue, write the function toggle(id) from above under habits, and add @toggle="toggle" to HabitItem. Click a habit: it's crossed out. Click it again, and it isn't.