HTML5テクニカルノート
Vue + <script setup> + TypeScript: TodoMVC 02 リスト項目の削除とスタイル変更
- ID: FN2211002
- Technique: ECMAScript 2015
- Package: Vue 3.2
Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズの第2回です。お題は大きくふたつあります。まず、Todoリストを項目ごとに削除できるようにすることです。つぎに、項目には完了したかどうかを示すチェックボックスが加えてありました。このチェックのあるなしによって、項目のスタイルを変えてみましょう。
01 Todo項目をボタンで削除する
各Todo項目を削除する処理の流れはつぎのとおりです。
- 各Todo項目に削除ボタンを加える。
- ボタンクリックで、削除のイベントとその項目の参照を、親コンポーネントにバケツリレーで送る。
- ルートコンポーネントが、Todoリストの配列から項目を除く。
Todo項目単体のモジュール(src/components/TodoItem.vue
)に差し込んだボタン(<button>
要素)には、クリックイベント(@click
)のハンドラ関数(removeTodo()
)を定めます。親コンポーネントにイベントを送るdefineEmits()
関数もインタフェース(Emits
)とともに加えなければなりません。ハンドラ関数は、イベント(event: 'removeTodo'
)に添えて、削除するTodo項目の参照(todo
)を渡します。このとき参照を取り出すプロパティ(props
)が、defineProps()
関数の戻り値です。
src/components/TodoItem.vue
<script setup lang="ts"> interface Emits { (event: 'removeTodo', todo: Todo): void; } // defineProps<Props>(); const props = defineProps<Props>(); const emit = defineEmits<Emits>(); const removeTodo = () => { emit('removeTodo', props.todo); }; </script> <template> <div class="view"> <button class="destroy" @click="removeTodo" /> </div> </template>
親コンポーネントのモジュール(src/components/TodoList.vue
)は、子(TodoItem
)から送られたイベントをさらにその親にバケツリレーしなければなりません。イベントハンドラ(removeTodo()
)やdefineEmits()
の定めは、基本的に子コンポーネントと同じです。
src/components/TodoList.vue
<script setup lang="ts"> interface Emits { (event: 'removeTodo', todo: Todo): void; } const emit = defineEmits<Emits>(); const removeTodo = (todo: Todo) => { emit('removeTodo', todo); }; </script> <template> <section class="main"> <ul class="todo-list"> <li v-for="todo in todos" :key="todo.id"> <!-- <TodoItem :todo="todo" /> --> <TodoItem :todo="todo" @removeTodo="removeTodo" /> </li> </ul> </section> </template>
バケツリレーのアンカーであるルートモジュール(src/App.vue
)がremoveTodo()
イベントハンドラで、Todoリストの配列ref
オブジェクト(todos
)から項目(todo
)を除きます。これで、各Todo項目がボタンで削除できるようになりました(図001)。
src/App.vue
<script setup lang="ts"> const removeTodo = (todo: Todo) => { todos.value = todos.value.filter((item) => item !== todo); }; </script> <template> <section id="app" class="todoapp"> <!-- <TodoList :todos="todos" /> --> <TodoList :todos="todos" @removeTodo="removeTodo" /> </section> </template>
図001■Todo項目をボタンで削除する

02 チェックした項目にスタイルを割り当てる
Todoリストの各項目には、先頭にチェックボックスがあります。けれど、チェックのオン/オフができるだけで、ルートコンポーネント(App
)のTodoリストの配列ref
オブジェクト(todos
)と同期もされていません。そこで、completed
プロパティとデータバインディングしたうえで、チェックのあるなしに応じて項目タイトルのスタイルを変えてみましょう。
まずは、チェックボックスの状態を親子コンポーネント間で双方向にデータバインディングしなければなりません。ただし、片道ずつ分けて考えます。<input type="checkbox">
要素の場合はつぎのとおりです(「フォーム入力バインディング」参照)。もちろんともに、親子間でおなじみバケツリレーが繰り広げられます。
checked
プロパティ: 親から子へのデータの同期。change
イベント: 子から親へのイベントとデータの送信。
src/components/TodoItem.vue
<script setup lang="ts"> interface Emits { (event: 'done', todo: Todo, completed: boolean): void; } const onChange = () => { emit('done', props.todo, !props.todo.completed); }; </script> <template> <div class="view"> <!-- <input type="checkbox" class="toggle" /> --> <input type="checkbox" class="toggle" :checked="todo.completed" @change="onChange" /> </div> </template>
書き改めたTodo項目単体のモジュールsrc/components/TodoItem.vue
の記述全体は、つぎのコード001のとおりです。
コード001■Todo項目単体のモジュール
src/components/TodoItem.vue
<script setup lang="ts">
import type { Todo } from '../App.vue';
interface Props {
todo: Todo;
}
interface Emits {
(event: 'removeTodo', todo: Todo): void;
(event: 'done', todo: Todo, completed: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const removeTodo = () => {
emit('removeTodo', props.todo);
};
const onChange = () => {
emit('done', props.todo, !props.todo.completed);
};
</script>
<template>
<div class="view">
<input
type="checkbox"
class="toggle"
:checked="todo.completed"
@change="onChange"
/>
<label>{{ todo.title }}</label>
<button class="destroy" @click="removeTodo" />
</div>
</template>
バケツリレーの間に立ったsrc/components/TodoList.vue
モジュールが、子コンポーネント(TodoItem
)に同期するデータを渡し、親(App
)にイベント送信するのはこれまでどおりです。
ここではさらに、チェックボックスのオン/オフによって項目のスタイルを変える処理が加わります。対象はリスト項目の<li>
要素です。CSSのクラスはふたつ用意されていて、ひとつ(todo
)は固定です。もうひとつ(completed
)が、チェックボックスのオン/オフによって動的に変わるクラスです。このような場合には、クラスバインディングを使います。
クラスのバインディングの基本はオブジェクトへのバインディングです。v-bind:class
(省略記法:class
)ディレクティブに与えるのはオブジェクトで、プロパティはクラス名にして、値となる式を定めます。式のブール(論理)値評価がtureなら、クラスが適用される仕組みです。
:class="{ completed: todo.completed }"
では、固定のクラス(todo
)はバインディングしていないclass
属性に普通に定めてよいのでしょうか。構いません。ふたつは排他的ではないからです。
class="todo"
とはいえ、まとめてしまった方が、見やすく管理もしやすいでしょう。このようなとき用いるのが配列へのバインディングで、クラス(:class
)に渡すのはオブジェクトでなく配列です。固定のクラスはそのまま文字列で要素に加えてください。要素にはオブジェクト構文のクラスも含められます(さらに詳しくは、「Vue.js: 複数のクラスをバインディングする場合どのような書き方があるか」をお読みください)。
:class="['todo', { completed: todo.completed }]"
src/components/TodoList.vue
<script setup lang="ts"> interface Emits { (event: 'done', todo: Todo, completed: boolean): void; } const done = (todo: Todo, completed: boolean) => { emit('done', todo, completed); }; </script> <template> <section class="main"> <ul class="todo-list"> <!-- <li v-for="todo in todos" :key="todo.id"> --> <li v-for="todo in todos" :class="['todo', { completed: todo.completed }]" :key="todo.id" > <!-- <TodoItem :todo="todo" @removeTodo="removeTodo" /> --> <TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" /> </li> </ul> </section> </template>
バケツリレーアンカーのルートモジュール(src/App.vue
)の仕事は、子コンポーネント(TodoItem
)から受け取ったイベント(@done
)とデータに応じて、Todo項目(todo
)のプロパティ(completed
)値を書き替えることです(done()
ハンドラ)。これで、Todo項目の子コンポーネントでチェックボックスのオン/オフを切り替えると、ルートコンポーネントのTodoリストの配列ref
オブジェクト(todos
)にデータバインディングされ、またクラスバインディングにより項目のスタイルも動的に変わります(図002)。
src/App.vue
<script setup lang="ts"> const done = (todo: Todo, completed: boolean) => { todo.completed = completed; }; </script> <template> <section id="app" class="todoapp"> <!-- <TodoList :todos="todos" @removeTodo="removeTodo" /> --> <TodoList :todos="todos" @removeTodo="removeTodo" @done="done" /> </section> </template>
図002■チェックした項目にスタイルを割り当てる

Todoリスト表示(src/components/TodoList.vue
)とルート(src/App.vue
)のふたつのモジュールの記述全体は、つぎのコード002にまとめたとおりです。併せて、GitHubに公開したコードをソース01に上げました。
コード002■Todoリスト表示とルートのモジュール
src/components/TodoList.vue
<script setup lang="ts">
import type { Todo } from '../App.vue';
import TodoItem from './TodoItem.vue';
interface Props {
todos: Todo[];
}
interface Emits {
(event: 'removeTodo', todo: Todo): void;
(event: 'done', todo: Todo, completed: boolean): void;
}
defineProps<Props>();
const emit = defineEmits<Emits>();
const removeTodo = (todo: Todo) => {
emit('removeTodo', todo);
};
const done = (todo: Todo, completed: boolean) => {
emit('done', todo, completed);
};
</script>
<template>
<section class="main">
<ul class="todo-list">
<li
v-for="todo in todos"
:class="['todo', { completed: todo.completed }]"
:key="todo.id"
>
<TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" />
</li>
</ul>
</section>
</template>
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
export interface Todo {
id: number;
title: string;
completed: boolean;
}
const todos = ref<Todo[]>([]);
const uid = ref(0);
const addTodo = (todoTitle: string) => {
if (!todoTitle) return;
todos.value.push({
id: uid.value++,
title: todoTitle,
completed: false,
});
};
const removeTodo = (todo: Todo) => {
todos.value = todos.value.filter((item) => item !== todo);
};
const done = (todo: Todo, completed: boolean) => {
todo.completed = completed;
};
</script>
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<TodoInput @addTodo="addTodo" />
</header>
<TodoList :todos="todos" @removeTodo="removeTodo" @done="done" />
</section>
</template>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.4.2/index.css");
</style>
ソース01■TodoMVC 02 リスト項目の削除とスタイル変更
Vue + <script setup> + TypeScript: TodoMVCシリーズ
- TodoMVC 01 リスト項目の表示と追加
- TodoMVC 02 リスト項目の削除とスタイル変更
- TodoMVC 03 算出プロパティとデータのローカルへの保存
- TodoMVC 04 リスト表示する項目を選び出して切り替える
- TodoMVC 05 チェックをまとめてオン/オフしたり削除する
- TodoMVC 06 項目のテキストをダブルクリックで再編集する
- TodoMVC 07 フォーカスをコントロールする
- TodoMVC 08 要素にアニメーションを加える
- TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける
作成者: 野中文雄
作成日: 2022年12月09日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.