L08 ch05 — リスト + フォーム (TODO アプリ)
テクノロジ系 / L08 vue-basic — Vue 3 入門
L08 ch05 — リスト + フォーム (TODO アプリ)
v-model で双方向バインド + リスト操作
ゴール
v-modelで input ↔ 変数を双方向バインド- TODO アプリの 4 機能 (追加/削除/完了切替/集計)
- computed で派生値
v-model
<script setup>
import { ref } from 'vue'
const title = ref('')
</script>
<template>
<input v-model="title" placeholder="タスク名" />
<p>入力中: {{ title }}</p>
</template>→ input の値と変数が 常に同期
追加機能
<script setup>
const tasks = ref([])
const newTitle = ref('')
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({
id: Date.now(),
title: newTitle.value,
done: false,
})
newTitle.value = ''
}
</script>
<template>
<input v-model="newTitle" />
<button @click="addTask">追加</button>
</template>削除機能
<script setup>
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<template>
<li v-for="t in tasks" :key="t.id">
{{ t.title }}
<button @click="removeTask(t.id)">削除</button>
</li>
</template>完了切替 (チェックボックス)
<li v-for="t in tasks" :key="t.id">
<input type="checkbox" v-model="t.done" />
<span :style="{ textDecoration: t.done ? 'line-through' : 'none' }">
{{ t.title }}
</span>
</li>computed で派生値
<script setup>
import { computed } from 'vue'
const doneCount = computed(() => tasks.value.filter(t => t.done).length)
const undoneCount = computed(() => tasks.value.length - doneCount.value)
</script>
<template>
<p>未完了: {{ undoneCount }} / 全 {{ tasks.length }}</p>
</template>→ tasks が変わると自動再計算
リストから項目を削除する
「id で消す (filter)」と「位置で消す (splice)」の 2 パターンがある。
<script setup>
const tasks = ref([
{ id: 1, title: '買い物' },
{ id: 2, title: '掃除' },
])
// パターン A: index 指定で 1 件抜く (破壊的)
function removeByIndex(index) {
tasks.value.splice(index, 1)
}
// パターン B: 条件で絞り込んで新配列に置き換え (非破壊)
function removeById(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<template>
<li v-for="(t, i) in tasks" :key="t.id">
{{ t.title }}
<button @click="removeByIndex(i)">削除</button>
</li>
</template>splice(index, 1)→ その位置の要素 1 件を抜く。v-forの第 2 引数iを渡せば素直filter→ 条件に合うものだけ残す。id を持っているなら可読性が高い- どちらでも動くが、Vue では
tasks.value自体を更新するか、spliceで中身を直接いじるかの違い
drill
drill/ — fill-task-app, add-task-app
ハンズオン
ch05-list-form 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-task-app | v-model + push - フォーム入力でリスト追加 | 🔧 穴埋め | ★ |
| 2 | 02-add-task-app | v-model + バリデーション - タスク追加機能の拡張 | ➕ 機能追加 | ★ |
| 3 | 03-build-task-app | v-model + push + splice + v-for - タスク管理アプリを白紙から | 🛠️ ゼロ実装 | ★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: v-model + push - フォーム入力でリスト追加
ドリル名: 01-fill-task-app
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、入力したタスクを追加・削除できるアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.___({ id: Date.now(), title: newTitle.value }) <!-- ___1: 配列末尾に追加するメソッド -->
newTitle.value = ''
}
function removeTask(id) {
tasks.value = tasks.value.___(t => t.id !== id) <!-- ___2: 配列から条件で除外するメソッド -->
}
</script>
<template>
<input v-___="newTitle" placeholder="タスク名" /> <!-- ___3: 双方向バインディング -->
<button @click="addTask">追加</button>
<ul>
<li v-___="task in tasks" :___="task.id"> <!-- ___4: 配列ループ / ___5: 識別キー -->
{{ task.title }}
<button @click="removeTask(task.id)">削除</button>
</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- テキストを入力→「追加」でリストに表示される
- 「削除」ボタンで該当タスクが消える
- F12 コンソールに警告が出ないこと
ドリル 1: ヒント
ドリル名: 01-fill-task-app
> ⚠️ 自力で考えてから読んでください
___1: 配列末尾に要素を追加する JavaScript の配列メソッド___2: 配列から条件に合う要素だけを残す JavaScript の配列メソッド___3/___4: Vue のディレクティブはv-モデル/v-繰り返し___5:v-forの各要素を識別するための属性(:から始まる)
ドリル 2: v-model + バリデーション - タスク追加機能の拡張
ドリル名: 02-add-task-app
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
ベースコード(base/App.vue)は「追加」だけ動く状態。以下の機能を追加せよ。
追加する機能
- 完了切り替え: チェックボックスをクリックすると
doneが反転して、タイトルに打ち消し線がつく - 削除: 「削除」ボタンをクリックすると該当タスクがリストから消える
- 件数表示: 「未完了 X 件 / 全 Y 件」を画面下部に表示する
ベースコード(base/App.vue)
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({ id: Date.now(), title: newTitle.value, done: false })
newTitle.value = ''
}
// ここに toggleDone / deleteTask を追加する
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<!-- ここに完了切り替え・削除を追加する -->
{{ task.title }}
</li>
</ul>
<!-- ここに件数表示を追加する -->
</template>確認方法
- タスクを2〜3件追加する
- チェックボックスをクリックすると打ち消し線がつく
- 「削除」ボタンで消える
- 件数が正しく更新される
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
localStorageにタスクを保存して、ページをリロードしてもタスクが残るようにする (watchでtasksを JSON 化)- 「完了したタスクをすべて削除」ボタンを追加して、
tasks.value = tasks.value.filter(...)の感覚をつかむ - 完了済みタイトルの文字色を CSS
transitionでフェードさせてみる (UX 改善)
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-task-app
> ⚠️ 自力で考えてから読んでください
> {{HINT}}
ドリル 3: v-model + push + splice + v-for - タスク管理アプリを白紙から
ドリル名: 03-build-task-app
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
タスク管理アプリを Vue で 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジックはすべて自由ですが、下記の画面仕様を満たすこと。
仕様
画面要件
- テキスト入力欄 (空文字でない値が入力できる)
- 「追加」ボタン — クリックすると入力欄の値を新しいタスクとしてリストに追加
- タスク一覧表示 — 追加されたタスクのタイトルを画面に表示
- 「削除」ボタン (各タスクごと) — クリックすると該当タスクをリストから削除
初期状態
- タスク一覧は空
- 入力欄も空
動作
| 操作 | 期待される結果 |
|---|---|
| 入力欄に「牛乳」と入力 → 「追加」クリック | 一覧に「牛乳」が表示される |
| さらに「卵」を追加 | 一覧に「牛乳」「卵」の2件が表示される |
| 「牛乳」の「削除」ボタンクリック | 一覧から「牛乳」が消え、「卵」だけ残る |
| 空文字で「追加」クリック | 何も追加されない (バリデーション) |
確認方法
- 起動.bat (Windows) / 起動.sh (Mac) をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
<input>をv-modelでrefに紐付ける /@inputで手動取得する- タスクを
ref<string[]>で持つ /ref<{ id, text }[]>で持つ - 削除を
splice/filter/findIndex+spliceで実装 - ボタンに class や id を付けるか付けないかも自由
守る (テストが見ている)
- 「追加」「削除」というテキストのボタンが画面にある
<input type="text">がある- 追加・削除の動作が仕様どおり
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
v-model で input とリアクティブ変数を双方向バインド | ch05 学習目標 |
tasks.value.push() で配列に追加 | ch05 学習目標 |
tasks.value.splice(index, 1) で配列から削除 | ch05 slide「リストから項目を削除する」 |
| フォーム送信時のバリデーション + リセット | ch05 学習目標 |
v-for でリストレンダリング | ch03-v-for |
このドリルは 「ユーザー操作 → state 変化 → 画面再描画」のリアクティビティを1人で完結させる 経験。Vue の核心です。
ドリル 3: ヒント
ドリル名: 03-build-task-app
> ⚠️ 自力で考えてから読んでください
段階1: データを保持する
import { ref } from 'vue'
const tasks = ref([]) // タスク一覧
const newTask = ref('') // 入力欄の値段階2: 入力欄を作る
<input v-model="newTask" type="text" />
段階3: 追加ボタン
<button @click="addTask">追加</button>
function addTask() {
// 空文字チェック
// tasks.value.push する
// newTask.value をクリア
}段階4: 一覧表示
<ul>
<li v-for="(task, i) in tasks" :key="i">
{{ task }}
<button @click="removeTask(i)">削除</button>
</li>
</ul>段階5: 削除
function removeTask(index) {
tasks.value.splice(index, 1)
}考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ch05-list-form 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-task-app | v-model + push - フォーム入力でリスト追加 | 🔧 穴埋め | ★ |
| 2 | 02-add-task-app | v-model + バリデーション - タスク追加機能の拡張 | ➕ 機能追加 | ★ |
| 3 | 03-build-task-app | v-model + push + splice + v-for - タスク管理アプリを白紙から | 🛠️ ゼロ実装 | ★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: v-model + push - フォーム入力でリスト追加
ドリル名: 01-fill-task-app
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、入力したタスクを追加・削除できるアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.___({ id: Date.now(), title: newTitle.value }) <!-- ___1: 配列末尾に追加するメソッド -->
newTitle.value = ''
}
function removeTask(id) {
tasks.value = tasks.value.___(t => t.id !== id) <!-- ___2: 配列から条件で除外するメソッド -->
}
</script>
<template>
<input v-___="newTitle" placeholder="タスク名" /> <!-- ___3: 双方向バインディング -->
<button @click="addTask">追加</button>
<ul>
<li v-___="task in tasks" :___="task.id"> <!-- ___4: 配列ループ / ___5: 識別キー -->
{{ task.title }}
<button @click="removeTask(task.id)">削除</button>
</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- テキストを入力→「追加」でリストに表示される
- 「削除」ボタンで該当タスクが消える
- F12 コンソールに警告が出ないこと
ドリル 1: ヒント
ドリル名: 01-fill-task-app
> ⚠️ 自力で考えてから読んでください
___1: 配列末尾に要素を追加する JavaScript の配列メソッド___2: 配列から条件に合う要素だけを残す JavaScript の配列メソッド___3/___4: Vue のディレクティブはv-モデル/v-繰り返し___5:v-forの各要素を識別するための属性(:から始まる)
ドリル 2: v-model + バリデーション - タスク追加機能の拡張
ドリル名: 02-add-task-app
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
ベースコード(base/App.vue)は「追加」だけ動く状態。以下の機能を追加せよ。
追加する機能
- 完了切り替え: チェックボックスをクリックすると
doneが反転して、タイトルに打ち消し線がつく - 削除: 「削除」ボタンをクリックすると該当タスクがリストから消える
- 件数表示: 「未完了 X 件 / 全 Y 件」を画面下部に表示する
ベースコード(base/App.vue)
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({ id: Date.now(), title: newTitle.value, done: false })
newTitle.value = ''
}
// ここに toggleDone / deleteTask を追加する
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<!-- ここに完了切り替え・削除を追加する -->
{{ task.title }}
</li>
</ul>
<!-- ここに件数表示を追加する -->
</template>確認方法
- タスクを2〜3件追加する
- チェックボックスをクリックすると打ち消し線がつく
- 「削除」ボタンで消える
- 件数が正しく更新される
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
localStorageにタスクを保存して、ページをリロードしてもタスクが残るようにする (watchでtasksを JSON 化)- 「完了したタスクをすべて削除」ボタンを追加して、
tasks.value = tasks.value.filter(...)の感覚をつかむ - 完了済みタイトルの文字色を CSS
transitionでフェードさせてみる (UX 改善)
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-task-app
> ⚠️ 自力で考えてから読んでください
> {{HINT}}
ドリル 3: v-model + push + splice + v-for - タスク管理アプリを白紙から
ドリル名: 03-build-task-app
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
タスク管理アプリを Vue で 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジックはすべて自由ですが、下記の画面仕様を満たすこと。
仕様
画面要件
- テキスト入力欄 (空文字でない値が入力できる)
- 「追加」ボタン — クリックすると入力欄の値を新しいタスクとしてリストに追加
- タスク一覧表示 — 追加されたタスクのタイトルを画面に表示
- 「削除」ボタン (各タスクごと) — クリックすると該当タスクをリストから削除
初期状態
- タスク一覧は空
- 入力欄も空
動作
| 操作 | 期待される結果 |
|---|---|
| 入力欄に「牛乳」と入力 → 「追加」クリック | 一覧に「牛乳」が表示される |
| さらに「卵」を追加 | 一覧に「牛乳」「卵」の2件が表示される |
| 「牛乳」の「削除」ボタンクリック | 一覧から「牛乳」が消え、「卵」だけ残る |
| 空文字で「追加」クリック | 何も追加されない (バリデーション) |
確認方法
- 起動.bat (Windows) / 起動.sh (Mac) をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
<input>をv-modelでrefに紐付ける /@inputで手動取得する- タスクを
ref<string[]>で持つ /ref<{ id, text }[]>で持つ - 削除を
splice/filter/findIndex+spliceで実装 - ボタンに class や id を付けるか付けないかも自由
守る (テストが見ている)
- 「追加」「削除」というテキストのボタンが画面にある
<input type="text">がある- 追加・削除の動作が仕様どおり
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
v-model で input とリアクティブ変数を双方向バインド | ch05 学習目標 |
tasks.value.push() で配列に追加 | ch05 学習目標 |
tasks.value.splice(index, 1) で配列から削除 | ch05 slide「リストから項目を削除する」 |
| フォーム送信時のバリデーション + リセット | ch05 学習目標 |
v-for でリストレンダリング | ch03-v-for |
このドリルは 「ユーザー操作 → state 変化 → 画面再描画」のリアクティビティを1人で完結させる 経験。Vue の核心です。
ドリル 3: ヒント
ドリル名: 03-build-task-app
> ⚠️ 自力で考えてから読んでください
段階1: データを保持する
import { ref } from 'vue'
const tasks = ref([]) // タスク一覧
const newTask = ref('') // 入力欄の値段階2: 入力欄を作る
<input v-model="newTask" type="text" />
段階3: 追加ボタン
<button @click="addTask">追加</button>
function addTask() {
// 空文字チェック
// tasks.value.push する
// newTask.value をクリア
}段階4: 一覧表示
<ul>
<li v-for="(task, i) in tasks" :key="i">
{{ task }}
<button @click="removeTask(i)">削除</button>
</li>
</ul>段階5: 削除
function removeTask(index) {
tasks.value.splice(index, 1)
}ch05-list-form 演習解説
⚠️ 自力でドリルを解いてから読んでください
ドリル 1: v-model + push - フォーム入力でリスト追加 — 模範解答
ドリル名: 01-fill-task-app
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({ id: Date.now(), title: newTitle.value })
newTitle.value = ''
}
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<template>
<input v-model="newTitle" placeholder="タスク名" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
<button @click="removeTask(task.id)">削除</button>
</li>
</ul>
</template>ドリル 1: v-model + push - フォーム入力でリスト追加 — 解説
ドリル名: 01-fill-task-app
| 空欄 | 解答 |
|---|---|
___1 | push |
___2 | filter |
___3 | model(v-model) |
___4 | for(v-for) |
___5 | key(:key) |
観察ポイント
___1をspliceなどで書いた受講生は配列メソッドの理解度確認。動けば OK___5を忘れた場合 F12 コンソールに警告が出るので「警告を読む」習慣の指導機会removeTask(id)の id ベース filter は安全。splice(index, 1)でも動くが配列インデックスの不整合事故を経験させる材料に
ドリル 2: v-model + バリデーション - タスク追加機能の拡張 — 模範解答
ドリル名: 02-add-task-app
<script setup>
import { ref, computed } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({ id: Date.now(), title: newTitle.value, done: false })
newTitle.value = ''
}
function toggleDone(task) {
task.done = !task.done
}
function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
const doneCount = computed(() => tasks.value.filter(t => t.done).length)
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>
</ul>
<p>未完了 {{ tasks.length - doneCount }} 件 / 全 {{ tasks.length }} 件</p>
</template>ドリル 2: v-model + バリデーション - タスク追加機能の拡張 — 解説
ドリル名: 02-add-task-app
{{ANSWER_CODE}}ドリル 3: v-model + push + splice + v-for - タスク管理アプリを白紙から — 模範解答
ドリル名: 03-build-task-app
<script setup>
import { ref } from 'vue'
const newTask = ref('')
const tasks = ref([])
function addTask() {
const trimmed = newTask.value.trim()
if (!trimmed) return
tasks.value.push(trimmed)
newTask.value = ''
}
function removeTask(index) {
tasks.value.splice(index, 1)
}
</script>
<template>
<div>
<input v-model="newTask" type="text" placeholder="タスクを入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="(task, i) in tasks" :key="i">
{{ task }}
<button @click="removeTask(i)">削除</button>
</li>
</ul>
</div>
</template>ドリル 3: v-model + push + splice + v-for - タスク管理アプリを白紙から — 解説
ドリル名: 03-build-task-app
Vue でゼロから書く思考プロセス
- データを ref で持つ — リアクティブにする
<input>を v-model で双方向バインド — 入力値を取り出す- イベントハンドラ —
@click="addTask"のように関数を呼ぶ v-forで配列をレンダリング — タスク一覧を出すkey属性 — 各要素を一意に識別 (削除時のリアクティビティのため)
実装が複数あり得る点
- データ型:
ref<string[]>(シンプル) /ref<{ id, text }[]>(拡張しやすい) - 追加:
push/[...tasks.value, newTask.value](immutable) - 削除:
splice/filter/findIndex + splice - key: index / id (タスクごと採番)
- バリデーション:
trim()で空白チェック /if (!val)だけ
テストは「画面の挙動」だけを見ているので、内部実装はどれでも OK。
ID/class について
このアプリでは id/class を付けても付けなくてもテストは通る。テストが要素を取得する方法:
<input type="text">で input を見つける<button>のテキスト「追加」「削除」で見つける<li>をfindAllで取得
なので「追加」「削除」というボタンの 表示テキスト だけは固定する。
Q: タスクリスト(追加・削除)の空欄を埋める
問題
___ を埋めて、入力したタスクを追加・削除できるアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.___({ id: Date.now(), title: newTitle.value }) <!-- ___1: 配列末尾に追加するメソッド -->
newTitle.value = ''
}
function removeTask(id) {
tasks.value = tasks.value.___(t => t.id !== id) <!-- ___2: 配列から条件で除外するメソッド -->
}
</script>
<template>
<input v-___="newTitle" placeholder="タスク名" /> <!-- ___3: 双方向バインディング -->
<button @click="addTask">追加</button>
<ul>
<li v-___="task in tasks" :___="task.id"> <!-- ___4: 配列ループ / ___5: 識別キー -->
{{ task.title }}
<button @click="removeTask(task.id)">削除</button>
</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- テキストを入力→「追加」でリストに表示される
- 「削除」ボタンで該当タスクが消える
- F12 コンソールに警告が出ないこと
ヒント
___1: 配列末尾に要素を追加する JavaScript の配列メソッド___2: 配列から条件に合う要素だけを残す JavaScript の配列メソッド___3/___4: Vue のディレクティブはv-モデル/v-繰り返し___5:v-forの各要素を識別するための属性(:から始まる)
演習: タスクアプリを完成させる
ベースコード(base/App.vue)は「追加」だけ動く状態。以下の機能を追加せよ。
追加する機能
- 完了切り替え: チェックボックスをクリックすると
doneが反転して、タイトルに打ち消し線がつく - 削除: 「削除」ボタンをクリックすると該当タスクがリストから消える
- 件数表示: 「未完了 X 件 / 全 Y 件」を画面下部に表示する
ベースコード(base/App.vue)
<script setup>
import { ref } from 'vue'
const newTitle = ref('')
const tasks = ref([])
function addTask() {
if (!newTitle.value.trim()) return
tasks.value.push({ id: Date.now(), title: newTitle.value, done: false })
newTitle.value = ''
}
// ここに toggleDone / deleteTask を追加する
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<!-- ここに完了切り替え・削除を追加する -->
{{ task.title }}
</li>
</ul>
<!-- ここに件数表示を追加する -->
</template>確認方法
- タスクを2〜3件追加する
- チェックボックスをクリックすると打ち消し線がつく
- 「削除」ボタンで消える
- 件数が正しく更新される
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
localStorageにタスクを保存して、ページをリロードしてもタスクが残るようにする (watchでtasksを JSON 化)- 「完了したタスクをすべて削除」ボタンを追加して、
tasks.value = tasks.value.filter(...)の感覚をつかむ - 完了済みタイトルの文字色を CSS
transitionでフェードさせてみる (UX 改善)
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
> {{HINT}}
解答
{{ANSWER_CODE}}Q: タスク管理アプリを ゼロから実装する
問題
タスク管理アプリを Vue で 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジックはすべて自由ですが、下記の画面仕様を満たすこと。
仕様
画面要件
- テキスト入力欄 (空文字でない値が入力できる)
- 「追加」ボタン — クリックすると入力欄の値を新しいタスクとしてリストに追加
- タスク一覧表示 — 追加されたタスクのタイトルを画面に表示
- 「削除」ボタン (各タスクごと) — クリックすると該当タスクをリストから削除
初期状態
- タスク一覧は空
- 入力欄も空
動作
| 操作 | 期待される結果 |
|---|---|
| 入力欄に「牛乳」と入力 → 「追加」クリック | 一覧に「牛乳」が表示される |
| さらに「卵」を追加 | 一覧に「牛乳」「卵」の2件が表示される |
| 「牛乳」の「削除」ボタンクリック | 一覧から「牛乳」が消え、「卵」だけ残る |
| 空文字で「追加」クリック | 何も追加されない (バリデーション) |
確認方法
- 起動.bat (Windows) / 起動.sh (Mac) をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
<input>をv-modelでrefに紐付ける /@inputで手動取得する- タスクを
ref<string[]>で持つ /ref<{ id, text }[]>で持つ - 削除を
splice/filter/findIndex+spliceで実装 - ボタンに class や id を付けるか付けないかも自由
守る (テストが見ている)
- 「追加」「削除」というテキストのボタンが画面にある
<input type="text">がある- 追加・削除の動作が仕様どおり
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
v-model で input とリアクティブ変数を双方向バインド | ch05 学習目標 |
tasks.value.push() で配列に追加 | ch05 学習目標 |
tasks.value.splice(index, 1) で配列から削除 | ch05 slide「リストから項目を削除する」 |
| フォーム送信時のバリデーション + リセット | ch05 学習目標 |
v-for でリストレンダリング | ch03-v-for |
このドリルは 「ユーザー操作 → state 変化 → 画面再描画」のリアクティビティを1人で完結させる 経験。Vue の核心です。
ヒント
段階1: データを保持する
import { ref } from 'vue'
const tasks = ref([]) // タスク一覧
const newTask = ref('') // 入力欄の値段階2: 入力欄を作る
<input v-model="newTask" type="text" />
段階3: 追加ボタン
<button @click="addTask">追加</button>
function addTask() {
// 空文字チェック
// tasks.value.push する
// newTask.value をクリア
}段階4: 一覧表示
<ul>
<li v-for="(task, i) in tasks" :key="i">
{{ task }}
<button @click="removeTask(i)">削除</button>
</li>
</ul>段階5: 削除
function removeTask(index) {
tasks.value.splice(index, 1)
}解説
Vue でゼロから書く思考プロセス
- データを ref で持つ — リアクティブにする
<input>を v-model で双方向バインド — 入力値を取り出す- イベントハンドラ —
@click="addTask"のように関数を呼ぶ v-forで配列をレンダリング — タスク一覧を出すkey属性 — 各要素を一意に識別 (削除時のリアクティビティのため)
実装が複数あり得る点
- データ型:
ref<string[]>(シンプル) /ref<{ id, text }[]>(拡張しやすい) - 追加:
push/[...tasks.value, newTask.value](immutable) - 削除:
splice/filter/findIndex + splice - key: index / id (タスクごと採番)
- バリデーション:
trim()で空白チェック /if (!val)だけ
テストは「画面の挙動」だけを見ているので、内部実装はどれでも OK。
ID/class について
このアプリでは id/class を付けても付けなくてもテストは通る。テストが要素を取得する方法:
<input type="text">で input を見つける<button>のテキスト「追加」「削除」で見つける<li>をfindAllで取得
なので「追加」「削除」というボタンの 表示テキスト だけは固定する。