サイトトップ

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

HTML5テクニカルノート

Vue + <script setup> + TypeScript: TodoMVC 05 チェックをまとめてオン/オフしたり削除する


Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズ第5回が扱うのはふたつのお題です。まず、Todoリストの項目すべてのチェックをまとめてオン/オフします。つぎに、終了したTodo項目つまりチェック済みの要素を調べて、リストデータから除く断捨離の機能です。

01 Todo項目のチェックをまとめてオン/オフする

Todo項目のチェックをまとめてオン/オフするボタンは、リスト表示のモジュールsrc/components/TodoList.vueにチェックボックス(<input type="checkbox">要素)で加えます。

src/components/TodoList.vue

<template<
	<section class="main">
		<input id="toggle-all" type="checkbox" class="toggle-all" />
		<label for="toggle-all">Mark all as complete</label>

	</section>
</template>

ルートモジュールsrc/App.vueに加えるチェックボックスの処理はつぎのとおりです。

そこで、すべて処理済みかどうかを算出プロパティ(allDone)として新たに加えます。このプロパティをデータバインディングするのが、表示リストの子コンポーネント(TodoList)です。また、子コンポーネントがチェックボックスを切り替えると、イベント(toggleAll)とともに、checkedプロパティ値が送られます。そのイベントハンドラとしてtoggleAll()を加えました。

src/App.vue

<script setup lang="ts">

const allDone = computed(() => remaining.value === 0);

const toggleAll = (checked: boolean) => {
	todos.value.forEach((todo) => (todo.completed = checked));
};

</script>

<template>
	<section id="app" class="todoapp">

		<TodoList
			:allDone="allDone"

			@toggleAll="toggleAll"
		/>

	</section>
</template>

リスト表示のモジュールsrc/components/TodoList.vueに戻って、定めるのは親コンポーネントとのデータバインディングです。受け取るプロパティ(allDone)と送るイベント(event: 'toggleAll')の型はinterfaceに加えます。チェックボックス(<input type="checkbox">)の:checkedにデータバインディングするのがプロパティ、イベントを送信するのは@changeのハンドラメソッド(onChange())です。Todoリスト左上のチェックボックスで、すべての項目のチェックがまとめて変えられるようになりました(図001)。

src/components/TodoList.vue

<script setup lang="ts">

interface Props {
	allDone: boolean;

}
interface Emits {

	(event: 'toggleAll', checked: boolean): void;
}

const onChange = ({ target }: Event) => {
	if (!(target instanceof HTMLInputElement)) return;
	emit('toggleAll', target.checked);
};
</script>

<template>
	<section class="main">
		<!-- <input id="toggle-all" type="checkbox" class="toggle-all" /> -->
		<input
			id="toggle-all"
			type="checkbox"
			class="toggle-all"
			:checked="allDone"
			@change="onChange"
		/>

	</section>
</template>

図001■Todoリスト左上のチェックボックスですべての項目のチェックがまとめて変えられる

図001

Todoリスト表示のモジュールsrc/components/TodoList.vueの記述全体は、つぎのコード001にまとめたとおりです。

コード001■Todoリスト表示のモジュール

src/components/TodoList.vue

<script setup lang="ts">
import type { Todo } from '../App.vue';
import TodoItem from './TodoItem.vue';

interface Props {
	allDone: boolean;
	filteredTodos: Todo[];
}
interface Emits {
	(event: 'removeTodo', todo: Todo): void;
	(event: 'done', todo: Todo, completed: boolean): void;
	(event: 'toggleAll', checked: 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);
};
const onChange = ({ target }: Event) => {
	if (!(target instanceof HTMLInputElement)) return;
	emit('toggleAll', target.checked);
};
</script>

<template>
	<section class="main">
		<input
			id="toggle-all"
			type="checkbox"
			class="toggle-all"
			:checked="allDone"
			@change="onChange"
		/>
		<label for="toggle-all">Mark all as complete</label>
		<ul class="todo-list">
			<li
				v-for="todo in filteredTodos"
				:class="['todo', { completed: todo.completed }]"
				:key="todo.id"
			>
				<TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" />
			</li>
		</ul>
	</section>
</template>

02 チェック済みの項目すべてをリストデータから除く

Todoリストデータからチェック済みの項目すべてを除くボタン(<button>要素)は、フッタのTodoコントローラモジュールsrc/components/TodoController.vueに加えましょう。@clickイベントで親コンポーネントにイベント(event: 'removeCompleted')を送るのがremoveCompleted()ハンドラメソッドです。なお、Todoリストの項目すべてが未処理なら、断捨離できません。その場合、v-showディレクティブでボタンは非表示にしました。

src/components/TodoController.vue

<script setup lang="ts">

interface Emits {
	(event: 'removeCompleted'): void;
}
const emit = defineEmits<Emits>();

const removeCompleted = () => {
	emit('removeCompleted');
};
</script>

<template>
	<footer class="footer" v-show="todos.length">

		<button
			class="clear-completed"
			v-show="todos.length > remaining"
			@click="removeCompleted"
		>
			Clear completed
		</button>
	</footer>
</template>

子コンポーネント(TodoController)からのイベント(removeCompleted)を受け取るのが、親モジュールsrc/App.vueのハンドラメソッドremoveCompleted()です。未処理のTodo項目リストを取り出す関数(getActive())はすでに定めてあるので、改めておおもとの配列refオブジェクト(todos)のデータに上書きすれば済みます。Todoリストのデータは断捨離されて、残るのは未処理項目のみです(図002)。

src/App.vue

<script setup lang="ts">

const removeCompleted = () => {
	todos.value = getActive(todos.value);
};

</script>

<template>
	<section id="app" class="todoapp">

		<TodoController


			@removeCompleted="removeCompleted"
		/>
	</section>
</template>

図002■チェック済みの終了項目をまとめて削除できる

図002

書き改めたTodoコントローラsrc/components/TodoController.vueとルートモジュールsrc/App.vueの記述全体は、つぎのコード002にまとめました。併せて公開するGitHubのコードが、以下のソース01です。

コード002■Todoコントローラとルートモジュール

src/components/TodoController.vue

<script setup lang="ts">
import { computed } from 'vue';
import type { Todo } from '../App.vue';

interface Props {
	todos: Todo[];
	remaining: number;
	visibility: string;
}
interface Emits {
	(event: 'removeCompleted'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const pluralize = computed(() => (props.remaining === 1 ? 'item' : 'items'));
const removeCompleted = () => {
	emit('removeCompleted');
};
</script>

<template>
	<footer class="footer" v-show="todos.length">
		<span class="todo-count">
			<strong>{{ remaining }}</strong> {{ pluralize }} left
		</span>
		<ul class="filters">
			<li>
				<a href="#/all" :class="{ selected: visibility === 'all' }">All</a>
			</li>
			<li>
				<a href="#/active" :class="{ selected: visibility === 'active' }"
					>Active</a
				>
			</li>
			<li>
				<a href="#/completed" :class="{ selected: visibility === 'completed' }"
					>Completed</a
				>
			</li>
		</ul>
		<button
			class="clear-completed"
			v-show="todos.length > remaining"
			@click="removeCompleted"
		>
			Clear completed
		</button>
	</footer>
</template>

src/App.vue

<script setup lang="ts">
import { computed, onMounted, ref, watchEffect } from 'vue';
import { fetch, getNewId, save } from './TodoStorage';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';

export interface Todo {
	id: number;
	title: string;
	completed: boolean;
}
const todos = ref(fetch());
const visibility = ref('all');
const remaining = computed(() => getActive(todos.value).length);
const allDone = computed(() => remaining.value === 0);
const filteredTodos = computed((): Todo[] => {
	switch (visibility.value) {
		case 'all':
			return todos.value;
		case 'active':
			return todos.value.filter((todo) => !todo.completed);
		case 'completed':
			return todos.value.filter((todo) => todo.completed);
		default:
			return todos.value;
	}
});
watchEffect(() => save(todos.value));
const addTodo = (todoTitle: string) => {
	if (!todoTitle) return;
	todos.value.push({
		id: getNewId(),
		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;
};
const getActive = (todos: Todo[]) => {
	return todos.filter((todo) => !todo.completed);
};
const onHashChange = () => {
	visibility.value = window.location.hash.replace(/#\/?/, '');
};
const removeCompleted = () => {
	todos.value = getActive(todos.value);
};
const toggleAll = (checked: boolean) => {
	todos.value.forEach((todo) => (todo.completed = checked));
};
onMounted(() => {
	window.addEventListener('hashchange', onHashChange);
});
</script>

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<TodoInput @addTodo="addTodo" />
		</header>
		<TodoList
			:allDone="allDone"
			:filteredTodos="filteredTodos"
			:todos="todos"
			@removeTodo="removeTodo"
			@done="done"
			@toggleAll="toggleAll"
		/>
		<TodoController
			:todos="todos"
			:remaining="remaining"
			:visibility="visibility"
			@removeCompleted="removeCompleted"
		/>
	</section>
</template>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.4.2/index.css");
</style>

ソース01■TodoMVC 05 チェックをまとめてオン/オフしたり削除する

Vue + <script setup> + TypeScript: TodoMVCシリーズ


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


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