サイトトップ

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

HTML5テクニカルノート

Vue + <script setup> + TypeScript: TodoMVC 04 リスト表示する項目を選び出して切り替える


Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズ第4回です。今回は、Todoリストで表示する項目を選び出して切り替えます。いわゆるフィルタリングの機能です。項目リストすべて(All)と未処理(Active)あるいは処理済み(Completed)の3つから、表示を選択できるようにします。

01 フッタに定めたリンクの選択に応じてスタイルを変える

先に、フィルタリングの3つのボタンをフッタのコントローラモジュール(src/components/TodoController.vue)に<a>要素で加えましょう。切り替えを識別するのは、href属性のハッシュ(#)です。

src/components/TodoController.vue

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

		<ul class="filters">
			<li>
				<a href="#/all">All</a>
			</li>
			<li>
				<a href="#/active">Active</a>
			</li>
			<li>
				<a href="#/completed">Completed</a>
			</li>
		</ul>
	</footer>
</template>

ハッシュが切り替わったことはhashchangeイベントで捉えます。リスナーメソッド(onHashChange())を定めるのは、ルートモジュールsrc/App.vueonMounted()フックです。コールバックは、コンポーネント(App)がマウントされたあと呼び出されます。

切り替わるハッシュの識別キーワードを収めるために加えたのがrefオブジェクトvisibilityです。hashchangeイベントのリスナーメソッド(onHashChange())は、ハッシュからキーワードを取り出して定めます。そして、フッタのTodoコントローラコンポーネント(<TodoController>)にデータバインディングしました。

src/App.vue

<script setup lang="ts">
// import { computed, ref, watchEffect } from 'vue';
import { computed, onMounted, ref, watchEffect } from 'vue';

const visibility = ref('all');

const onHashChange = () => {
	visibility.value = window.location.hash.replace(/#\/?/, '');
};
onMounted(() => {
	window.addEventListener('hashchange', onHashChange);
});
</script>

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

		<!-- <TodoController :todos="todos" :remaining="remaining" /> -->
		<TodoController
			:todos="todos"
			:remaining="remaining"
			:visibility="visibility"
		/>
	</section>
</template>

フッタのTodoコントローラモジュールsrc/components/TodoController.vueは、親コンポーネント(App)から受け取った識別のプロパティ(visibility)により、どのフィルタボタン(<a>要素)が選ばれたのかわかります。CSS(index.css)には選択されたボタンのクラス(selected)がすでに定められているので、クラスバインディング(:class)しましょう。これで、クリックしたボタンのスタイルが変わります(図001)。まだ、フィルタリングの機能は備わっていません。

src/components/TodoController.vue

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

		<ul class="filters">
			<li>
				<!-- <a href="#/all">All</a> -->
				<a href="#/all" :class="{ selected: visibility === 'all' }">All</a>
			</li>
			<li>
				<!-- <a href="#/active">Active</a> -->
				<a href="#/active" :class="{ selected: visibility === 'active' }"
					>Active</a
				>
			</li>
			<li>
				<!-- <a href="#/completed">Completed</a> -->
				<a href="#/completed" :class="{ selected: visibility === 'completed' }"
					>Completed</a
				>
			</li>
		</ul>
	</footer>
</template>

図001■選択したフィルタボタンのスタイルが変わる

図001

フッタのTodoコントローラモジュールsrc/components/TodoController.vueの記述全体は、つぎのコード001にまとめたとおりです。

コード001■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;
}
const props = defineProps<Props>();
const pluralize = computed(() => (props.remaining === 1 ? 'item' : 'items'));
</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>
	</footer>
</template>

02 データからフィルタリングしたリスト項目を表示する

いよいよ、フィルタリングの機能を加えます。ルートモジュールsrc/App.vueに定める処理の考え方はつぎのとおりです。

算出プロパティfilteredTodosの配列は、Todoコントロールコンポーネント(TodoController)のフィルタボタン(<a>要素)から得た識別refオブジェクト(visibility)の値により、つぎのように条件分けして返しました。<template>はこの算出プロパティ値を、リスト表示コンポーネント(TodoList)に与え、Todoリストデータの配列refオブジェクト(todos)と置き替えるだけです。

src/App.vue

<script setup lang="ts">

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;
	}
});

</script>

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

		<!-- <TodoList :todos="todos" @removeTodo="removeTodo" @done="done" /> -->
		<TodoList
			:filtered-todos="filteredTodos"
			:todos="todos"
			@removeTodo="removeTodo"
			@done="done"
		/>

	</section>
</template>

書き改めたルートモジュールsrc/App.vueの記述全体を、つぎのコード002にまとめましょう。

コード002■ルートモジュール

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 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(/#\/?/, '');
};
onMounted(() => {
	window.addEventListener('hashchange', onHashChange);
});
</script>

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

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

Todoリスト表示のモジュールsrc/components/TodoList.vueは、親コンポーネントから受け取るプロパティをTodoリストデータ(todos)からフィルタリングされた算出プロパティ(filteredTodos)に差し替えればでき上がりです。フッタのTodoコントローラコンポーネントのフィルタボタンで、リスト表示されるTodo項目が切り替わります(図002)。モジュールの記述全体は、以下のコード003のとおりです。それぞれのモジュールのコードはGitHubに上げたソース01をご覧ください。

src/components/TodoList.vue

<script setup lang="ts">

interface Props {
	// todos: Todo[];
	filteredTodos: Todo[];
}

</script>

<template>
	<section class="main">
		<ul class="todo-list">
			<!-- <li v-for="todo in todos" ...> -->
			<li
				v-for="todo in filteredTodos"
				:class="['todo', { completed: todo.completed }]"
				:key="todo.id"
			>

			</li>
		</ul>
	</section>
</template>

図002■フィルタボタンでリスト表示されるTodo項目が切り替わる

図002

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

src/components/TodoList.vue

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

interface Props {
	filteredTodos: 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 filteredTodos"
				:class="['todo', { completed: todo.completed }]"
				:key="todo.id"
			>
				<TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" />
			</li>
		</ul>
	</section>
</template>

ソース01■TodoMVC 04 リスト表示する項目を選び出して切り替える

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


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


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