サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue + <script setup> + TypeScript: TodoMVC 02 リスト項目の削除とスタイル変更


Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズの第2回です。お題は大きくふたつあります。まず、Todoリストを項目ごとに削除できるようにすることです。つぎに、項目には完了したかどうかを示すチェックボックスが加えてありました。このチェックのあるなしによって、項目のスタイルを変えてみましょう。

01 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項目をボタンで削除する

図001

02 チェックした項目にスタイルを割り当てる

Todoリストの各項目には、先頭にチェックボックスがあります。けれど、チェックのオン/オフができるだけで、ルートコンポーネント(App)のTodoリストの配列refオブジェクト(todos)と同期もされていません。そこで、completedプロパティとデータバインディングしたうえで、チェックのあるなしに応じて項目タイトルのスタイルを変えてみましょう。

まずは、チェックボックスの状態を親子コンポーネント間で双方向にデータバインディングしなければなりません。ただし、片道ずつ分けて考えます。<input type="checkbox"> 要素の場合はつぎのとおりです(「フォーム入力バインディング」参照)。もちろんともに、親子間でおなじみバケツリレーが繰り広げられます。

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■チェックした項目にスタイルを割り当てる

図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シリーズ


作成者: 野中文雄
作成日: 2022年12月09日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.