← Teacher guide

Final challenge: a to-do app

Students match the parts of a finished to-do app to the course's pieces, find a count that goes stale, build the app in three milestones with fading support, then add a feature of their own.

Apps with Vue · Final challenge: a to-do app · about 100 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Plan 10 min · Milestone 1 15 min · Milestone 2 25 min · Milestone 3 30 min · Make it yours 20 min (two lessons)

Before class

Read the challenge primer first, then build milestone 1 yourself. There is nothing new here apart from one line: v-model on a tick box joins it to true or false. Each milestone recalls a lesson: adding (events and inputs, lesson 3), ticking and counting (computed, lesson 4), the component (lesson 5). Support fades on purpose: milestone 1 gives most of the code, milestone 2 an outline as comments, milestone 3 only the goals. Finishing this lesson marks Challenge complete on the course page.

The count and Clear done use a for loop over the list, which students know from p5.js and the DOM course. Real Vue code would often use filter, which is fine as a stretch. The tick box inside the component changes task.done on the object it was given: it works because the parent and the card share the same object, but in larger apps a component sends an event up instead (emit), which this course leaves out.

Running the lesson

  1. Plan: match the parts, then write the three milestones on the board as a checklist.
  2. Milestone 1: everyone should finish this in the first lesson. Check an empty box adds nothing.
  3. Milestone 2: the outline is in the comments. Students who are stuck use Reset to this task's start.
  4. Milestone 3: goals only. Ask students what the component's prop is before they write it.
  5. Make it yours: ask each student to name the Vue pieces their feature uses.

The hard part (on purpose)

Milestone 3 is meant to be hard: turning working code into a component. Ask "what does one card need to be given?" before showing anything.

When students get stuck

What you will seeA question to askThe fix
The count does not change when a box is ticked.Is left a computed, and does the tick box have v-model on t.done?Wrap the count in computed(() => { … }) and join the box with v-model="t.done".
Add puts an empty task in the list.What does addTask check before pushing?if (newTask.value !== "") { … }
The component shows nothing.Is it registered before app.mount, is task in its props, and does the tag pass :task="t"?All three: register, list the prop, pass it with a colon.

Answers to the checks

  1. Name the parts: Match each part of the app with how it is built.
    The box → D. an input with v-model on a ref; Add → C. a button that runs a function, which pushes to the list; The tasks → F. v-for over the list, one todo-item each; The tick boxes → B. v-model on each task's done; 1 left to do → A. a computed that counts the tasks not done; Clear done → E. a function that keeps only the tasks not done
    Six parts, five lessons. Build them in order: adding tasks, then ticking and counting, then the component and Clear done.
  2. Spot the bug: Clicking Tick the first should show 1 left to do, but it still says 2. There is no error. Click the line that is wrong.
    line 12
    left is counted once, when the page starts. A count worked out from the list must be a computed, so it is counted again when the list changes.
  3. Where this leads: React and Svelte are other frameworks for building apps. What do you expect they share with Vue?
    Data the page follows, worked-out values, events and components, written differently
    The ideas carry over; the syntax and the update rules differ. The optional next lesson builds this same kind of app in Vue, Preact (React's small cousin) and Svelte.

Solutions

One possible solution for each task. Other correct programs are fine.

Build the app › Milestone 1: add tasks

<div id="app">
  <h1>To do</h1>
  <input v-model="newTask" placeholder="Something to do">
  <button @click="addTask">Add</button>
  <ul>
    <li v-for="t in tasks">{{ t.text }}</li>
  </ul>
</div>

<script src="vue.js"></script>
<script>
  const { createApp, ref, computed } = Vue;
  const app = createApp({
    setup() {
      const tasks = ref([
        { text: "Feed the cat", done: false },
        { text: "Pack my bag", done: true }
      ]);
      const newTask = ref("");
      function addTask() {
        if (newTask.value !== "") {
          tasks.value.push({ text: newTask.value, done: false });
          newTask.value = "";
        }
      }
      return { tasks, newTask, addTask };
    }
  });
  app.mount("#app");
</script>

Build the app › Milestone 2: tick and count

<div id="app">
  <h1>To do</h1>
  <input v-model="newTask" placeholder="Something to do">
  <button @click="addTask">Add</button>
  <ul>
    <li v-for="t in tasks"><label><input type="checkbox" v-model="t.done"> {{ t.text }}</label></li>
  </ul>
  <p>{{ left }} left to do</p>
</div>

<script src="vue.js"></script>
<script>
  const { createApp, ref, computed } = Vue;
  const app = createApp({
    setup() {
      const tasks = ref([
        { text: "Feed the cat", done: false },
        { text: "Pack my bag", done: true }
      ]);
      const newTask = ref("");
      function addTask() {
        if (newTask.value !== "") {
          tasks.value.push({ text: newTask.value, done: false });
          newTask.value = "";
        }
      }
      const left = computed(() => {
        let count = 0;
        for (const t of tasks.value) {
          if (!t.done) {
            count++;
          }
        }
        return count;
      });
      return { tasks, newTask, left, addTask };
    }
  });
  app.mount("#app");
</script>

Build the app › Milestone 3: a component and Clear done

<div id="app">
  <h1>To do</h1>
  <input v-model="newTask" placeholder="Something to do">
  <button @click="addTask">Add</button>
  <ul>
    <todo-item v-for="t in tasks" :task="t"></todo-item>
  </ul>
  <p>{{ left }} left to do</p>
  <button @click="clearDone">Clear done</button>
</div>

<script src="vue.js"></script>
<script>
  const { createApp, ref, computed } = Vue;
  const app = createApp({
    setup() {
      const tasks = ref([
        { text: "Feed the cat", done: false },
        { text: "Pack my bag", done: true }
      ]);
      const newTask = ref("");
      const left = computed(() => {
        let count = 0;
        for (const t of tasks.value) {
          if (!t.done) {
            count++;
          }
        }
        return count;
      });
      function addTask() {
        if (newTask.value !== "") {
          tasks.value.push({ text: newTask.value, done: false });
          newTask.value = "";
        }
      }
      function clearDone() {
        const keep = [];
        for (const t of tasks.value) {
          if (!t.done) {
            keep.push(t);
          }
        }
        tasks.value = keep;
      }
      return { tasks, newTask, left, addTask, clearDone };
    }
  });
  app.component("todo-item", {
    props: ["task"],
    template: `<li><label><input type="checkbox" v-model="task.done"> {{ task.text }}</label></li>`
  });
  app.mount("#app");
</script>

Make it yours

<div id="app">
  <h1>To do</h1>
  <input v-model="newTask" placeholder="Something to do">
  <button @click="addTask">Add</button>
  <ul>
    <todo-item v-for="t in tasks" :task="t"></todo-item>
  </ul>
  <p>{{ left }} left to do</p>
  <button @click="clearDone">Clear done</button>
  <p v-if="tasks.length > 0 && left === 0">All done! 🎉</p>
</div>

<script src="vue.js"></script>
<script>
  const { createApp, ref, computed } = Vue;
  const app = createApp({
    setup() {
      const tasks = ref([
        { text: "Feed the cat", done: false },
        { text: "Pack my bag", done: true }
      ]);
      const newTask = ref("");
      const left = computed(() => {
        let count = 0;
        for (const t of tasks.value) {
          if (!t.done) {
            count++;
          }
        }
        return count;
      });
      function addTask() {
        if (newTask.value !== "") {
          tasks.value.push({ text: newTask.value, done: false });
          newTask.value = "";
        }
      }
      function clearDone() {
        const keep = [];
        for (const t of tasks.value) {
          if (!t.done) {
            keep.push(t);
          }
        }
        tasks.value = keep;
      }
      return { tasks, newTask, left, addTask, clearDone };
    }
  });
  app.component("todo-item", {
    props: ["task"],
    template: `<li><label><input type="checkbox" v-model="task.done"> {{ task.text }}</label></li>`
  });
  app.mount("#app");
</script>

Stretch and support

Stretch: Save the tasks between visits with localStorage (watch the list, then JSON.stringify it); use filter for the count; send a delete event up from the component with emit.

Support: Pair students for milestone 3: one writes the component, one writes Clear done.

Exit ticket

Why is the number left a computed and not a ref?

It is worked out from the list, so it must be counted again whenever a task is added, ticked or cleared; computed does that, a ref would go out of step.

What each step is for

Where it leads

The optional lesson builds the same app in Vue, Preact and Svelte. Real projects use a build tool (npm create vue@latest) and single-file .vue components; the ideas stay the same. Assessment idea: students annotate a printout of their app with the lesson each part came from.