HTML5テクニカルノート
Vue + <script setup> + TypeScript: TodoMVC 04 リスト表示する項目を選び出して切り替える
- ID: FN2212002
- Technique: ECMAScript 2015
- Package: Vue 3.2
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.vue
のonMounted()
フックです。コールバックは、コンポーネント(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■選択したフィルタボタンのスタイルが変わる

フッタの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
に定める処理の考え方はつぎのとおりです。
- Todoリストデータの配列
ref
オブジェクト(todos
)は、そのまま変えない。 - リスト表示コンポーネント(
TodoList
)に渡すプロパティをフィルタリングした算出プロパティ(filteredTodos
)に差し替える。
算出プロパティ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項目が切り替わる

コード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シリーズ
- TodoMVC 01 リスト項目の表示と追加
- TodoMVC 02 リスト項目の削除とスタイル変更
- TodoMVC 03 算出プロパティとデータのローカルへの保存
- TodoMVC 04 リスト表示する項目を選び出して切り替える
- TodoMVC 05 チェックをまとめてオン/オフしたり削除する
- TodoMVC 06 項目のテキストをダブルクリックで再編集する
- TodoMVC 07 フォーカスをコントロールする
- TodoMVC 08 要素にアニメーションを加える
- TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける
作成者: 野中文雄
作成日: 2022年12月14日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.