HTML5テクニカルノート
Vue + <script setup> + TypeScript: TodoMVC 08 要素にアニメーションを加える
- ID: FN2301001
- Technique: ECMAScript 2015
- Package: Vue 3.2
Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズ第8回です。実は、Vue公式サイトの作例と同じ動きは、すでにでき上がりました。今回は番外編で、作例にアニメーションを加えてみましょう。
01 要素をフェードインで登場させる
アニメーションに用いるのは<Transition>
コンポーネントです。CSSでトランジションを定めます(「CSSでのトランジション」参照)。アニメーションさせたい要素(<h1>
)を、以下のように<Transition>
コンポーネントで包んでください。appear
は、初期レンダリングのときにトランジションさせたい場合に加えるプロパティです(「出現時のトランジション」参照)。
ここでは、ヘッダのタイトル(<h1>
要素)を画面の上部から、フェードインで登場させます。このアニメーションを与えるのが、CSSの「トランジションクラス」です。クラスは大きく、enter
とleave
をそれぞれキーワードとするふたつのトランジションに分かれます。本来のスタイルに対して、別のスタイルからトランジションするのがenter
です。leave
は逆に、もともとのスタイルから別のスタイルに変わります。はじめは見えない要素を、フェードインで登場させるのはenter
です。つぎのふたつのトランジションクラスを用います。
v-enter-active
:enter
が適用された要素のレンダリングをとおして有効なアクティブ状態。enter
のトランジションを定める。v-enter-from
:enter
が開始されたときのCSSの設定を与える。
場合によっては、ひとつのコンポーネントモジュールの中に、異なるトランジションが加わるかもしれません。そのときは、<Transition>
コンポーネントにname
プロパティで名前(つぎのコードでは"todo-head"
)をつけて分けられます(「名前付きトランジション」参照)。CSSのトランジションクラスには、v
に替えてつぎのようにこの名前を接頭辞として添えてください。
src/App.vue
<template> <section id="app" class="todoapp"> <header class="header"> <Transition appear name="todo-head"> <h1>todos</h1> </Transition> </header> </section> </template> <style scoped> .todo-head-enter-active { transition: 1s ease-in; } .todo-head-enter-from { opacity: 0; transform: translateY(-40px); } </style>
これで、ヘッダのタイトルがフェードインしながら下りてきます(図001)。書き改めたルートモジュールsrc/App.vue
の記述全体は、以下のコード001のとおりです。
図001■ヘッダのタイトルがフェードインしながら下りてくる

コード001■ルートモジュール
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">
<Transition appear name="todo-head">
<h1>todos</h1>
</Transition>
<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 scoped>
.todo-head-enter-active {
transition: 1s ease-in;
}
.todo-head-enter-from {
opacity: 0;
transform: translateY(-40px);
}
</style>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.4.2/index.css");
</style>
ノート01■後方互換性を破るトランジションクラスの変更
トランジションクラスについて、Vue 2からVue 3で後方互換性を破るつぎのような名前の変更が行われました。-to
に対して-from
が新たに定義されたことにより、CSSの記述はわかりやすくなったでしょう。けれど、後方互換性がなくなることにご注意ください。
v-enter
トランジションクラスはv-enter-from
へ、そしてv-leave
トランジションクラスはv-leave-from
へと名前が変更されました(「トランジションクラスの変更」)。
02 Todoリスト項目の追加と削除でアニメーションさせる
Todoリスト項目の追加と削除もアニメーションで表示しましょう。
アニメーションさせたい要素がv-for
でつくられるリストの場合には、<TransitionGroup>
コンポーネントを用います(「トランジショングループ」参照)。コンポーネント<Transition>
と異なるのは、ラップしてレンダリングする要素を<TransitionGroup>
のtag
プロパティで定めることです。したがって、これまで親要素だった<ul>
は置き替えるかたちで、つぎのようにtag
プロパティに"ul"
を与えます。v-forでつくるリストの親要素として加わります。なお、とくにトランジション/アニメーションするときは、v-for
で差し込む要素にはkey
特別属性を一意の不変な値にデータバインディング(:v-bind
)しなければなりません(「Vue.js: v-forで項目インデックスをkey属性にしていいのか」参照)。また、トランジションを定めるCSSのクラス(todo-item
)は、<template>
でクラスバインディング(:class
)した配列構文で要素を差し替えました(「配列構文を使う」参照)。
src/components/TodoList.vue
<template> <section class="main"> <!-- <ul class="todo-list"> --> <TransitionGroup class="todo-list" name="todo-item" tag="ul"> <li v-for="todo in filteredTodos" :class="[ // 'todo', 'todo-item', { completed: todo.completed, editing: todo == editedTodo }, ]" :key="todo.id" > <!-- </ul> --> </TransitionGroup> </section> </template> <style scoped> .todo-item { transition: 1s; } .todo-item-enter-from, .todo-item-leave-to { opacity: 0; transform: translateX(200px); } .todo-item-leave-active { position: absolute; } </style>
これで、リストの項目を加えたり除いたりすると、テキストが水平に移動しつつフェードイン・フェードアウトするようになります。モジュールsrc/components/TodoList.vue
の記述全体は、つぎのコード002にまとめたとおりです。
コード002■Todoリスト表示のモジュール
src/components/TodoList.vue
<script setup lang="ts">
import { ref } from 'vue';
import type { Todo } from '../App.vue';
import TodoEdit from './TodoEdit.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 editedTodo = ref<Todo | null>(null);
const removeTodo = (todo: Todo) => {
emit('removeTodo', todo);
};
const done = (todo: Todo, completed: boolean) => {
emit('done', todo, completed);
};
const editTodo = (todo: Todo) => {
editedTodo.value = todo;
};
const cancelEdit = () => {
editedTodo.value = null;
};
const doneEdit = (todoTitle: string) => {
if (!editedTodo.value) return;
const title = todoTitle.trim();
if (title) {
editedTodo.value.title = title;
} else {
removeTodo(editedTodo.value);
}
editedTodo.value = null;
};
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>
<TransitionGroup class="todo-list" name="todo-item" tag="ul">
<li
v-for="todo in filteredTodos"
:class="[
'todo-item',
{ completed: todo.completed, editing: todo == editedTodo },
]"
:key="todo.id"
>
<TodoItem
:todo="todo"
@removeTodo="removeTodo"
@done="done"
@editTodo="editTodo"
/>
<TodoEdit
:editedTodo="editedTodo"
:todo="todo"
@cancelEdit="cancelEdit"
@doneEdit="doneEdit"
/>
</li>
</TransitionGroup>
</section>
</template>
<style scoped>
.todo-item {
transition: 1s;
}
.todo-item-enter-from,
.todo-item-leave-to {
opacity: 0;
transform: translateX(200px);
}
.todo-item-leave-active {
position: absolute;
}
</style>
ソース01■TodoMVC 08 要素にアニメーションを加える
Vue + <script setup> + TypeScript: TodoMVCシリーズ
- TodoMVC 01 リスト項目の表示と追加
- TodoMVC 02 リスト項目の削除とスタイル変更
- TodoMVC 03 算出プロパティとデータのローカルへの保存
- TodoMVC 04 リスト表示する項目を選び出して切り替える
- TodoMVC 05 チェックをまとめてオン/オフしたり削除する
- TodoMVC 06 項目のテキストをダブルクリックで再編集する
- TodoMVC 07 フォーカスをコントロールする
- TodoMVC 08 要素にアニメーションを加える
- TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける
作成者: 野中文雄
作成日: 2023年01月10日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.