HTML5テクニカルノート
Vue.js + CLI入門 08: 要素にアニメーションを加える
- ID: FN1910001
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
Vue.jsで要素を動的に加えたり、除いたりするとき、アニメーションで変化させることができます。ここでは、基本的な操作についてご説明しましょう。さらに詳しくは、「Enter/Leaveとトランジション一覧」をお読みください 。
01 要素をフェードアウトさせる
先に、アニメーションを試すための簡単な動きをつくります。リストのタイトル(<h1>
要素)をクリックしたら、v-if
ディレクティブで消してしまいましょう。モジュールsrc/App.vue
に、@click
イベントのハンドラメソッド(hide()
)やフラグとするデータ(show
)を、つぎのように定めてください。タイトルは、クリックした瞬間に消えるはずです。
src/App.vue<template> <section id="app" class="todoapp"> <header class="header"> <!-- <h1>todos</h1> --> <h1 v-if="show" @click="hide">todos</h1> </header> </section> </template> <script> export default { data() { return { show: true, } }, methods: { hide() { this.show = false; }, } } </script>
変化する要素にアニメーションを与えるのは、<transition>
ラッパーコンポーネントです(「単一要素/コンポーネントのトランジション」参照)。アニメーションさせたい要素(<h1>
)を、つぎのように<transition>
コンポーネントで包んでください。
src/App.vue<template> <section id="app" class="todoapp"> <header class="header"> <transition> <h1 v-if="show" @click="hide">todos</h1> </transition> </header> </section> </template>
すると、CSSでアニメーションさせるための特別のクラスが自動的に使われます(「トランジションクラス」参照)。終わりの状態を定めるクラスがv-leave-to
です。そして、v-leave-active
にCSSのtransition
やanimation
などのプロパティで、要素にアニメーションを与えます。たとえば、つぎのCSSが要素をフェードアウトする定めです。タイトルをクリックしてフェードアウトすることが確かめられたら、このCSSもクリックで要素を消す前掲のコードも削除してもとに戻してください。
src/App.vue<style scoped> .v-leave-active { transition: 1s ease-in; } .v-leave-to { opacity: 0; } </style>
02 要素をフェードインで登場させる
動的に追加・削除することなく、予め置いてある要素も、はじめにページが描かれるときアニメーションさせることができます。そのちために
appear
属性です(「初期描画時のトランジション」参照)。デフォルトではenter
とleave
のトランジションクラスが使えます。モジュールsrc/App.vue
のテンプレートには、つぎのように<transition>
コンポーネントにname
属性を与えたことにご注目ください。
src/App.vue<template> <section id="app" class="todoapp"> <header class="header"> <transition appear name="todo-head"> <h1>todos</h1> </transition> </header> </section> </template>
name
属性の値は、トランジションクラスのデフォルトの接頭辞v-
に替えられるのです。すると、複数の<transition>
コンポーネントに、それぞれ異なるアニメーションが定められます。始まりの状態を定めるクラスはenter
です。そして、enter-active
にCSSのtransition
を定めました。これでタイトルのテキストが、フェードインしながら下りてくるでしょう(図001)。モジュールsrc/App.vue
のコード全体は、以下にまとめたとおりです(コード001)。
src/App.vue<style scoped> .todo-head-enter-active { transition: 1s; } .todo-head-enter { opacity: 0; transform: translateY(-40px); } </style>
図001■テキストがフェードインしながら下りてくる
コード001■タイトルがフェードインで登場する
src/App.vue
<template>
<section id="app" class="todoapp">
<header class="header">
<transition appear name="todo-head">
<h1>todos</h1>
</transition>
<todo-input
@add-todo="addTodo" />
</header>
<todo-list
:todos="todos"
:filtered-todos="filteredTodos"
:allDone="allDone"
@remove-todo="removeTodo"
@done="done"
@allDone="onAllDone">
</todo-list>
<todo-controller
:todos="todos"
:remaining="remaining"
:visibility="visibility"
@removeCompleted="removeCompleted">
</todo-controller>
</section>
</template>
<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';
const STORAGE_KEY = 'todos-vuejs-2.6';
const todoStorage = {
fetch() {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
todos.forEach(function(todo, index) {
todo.id = index;
});
todoStorage.uid = todos.length;
return todos;
},
save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
};
const filters = {
all(todos) {
return todos;
},
active(todos) {
return todos.filter((todo) =>
!todo.completed
);
},
completed(todos) {
return todos.filter((todo) =>
todo.completed
);
}
};
export default {
name: 'app',
components: {
TodoInput,
TodoList,
TodoController
},
data() {
return {
todos: todoStorage.fetch(),
visibility: 'all'
}
},
computed: {
filteredTodos() {
return filters[this.visibility](this.todos);
},
remaining() {
const todos = filters.active(this.todos);
return todos.length;
},
allDone: {
get() {
return this.remaining === 0;
},
set(value) {
this.todos.forEach((todo) =>
todo.completed = value
);
}
}
},
watch: {
todos: {
handler(todos) {
todoStorage.save(todos);
},
deep: true
}
},
mounted() {
window.addEventListener('hashchange', this.onHashChange);
},
methods: {
addTodo(todoTitle) {
const newTodo = todoTitle && todoTitle.trim();
if (!newTodo) {
return;
}
this.todos.push({
id: todoStorage.uid++,
title: newTodo,
completed: false
});
},
removeTodo(todo) {
this.todos = this.todos.filter((item) => item !== todo);
},
done(todo, completed) {
todo.completed = completed;
},
onHashChange() {
const visibility = window.location.hash.replace(/#\/?/, '');
this.visibility = visibility;
},
onAllDone(done) {
this.allDone = done;
},
removeCompleted() {
this.todos = filters.active(this.todos);
}
}
}
</script>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>
<style scoped>
.todo-head-enter-active {
transition: 1s;
}
.todo-head-enter {
opacity: 0;
transform: translateY(-40px);
}
</style>
03 リスト項目の追加と削除でアニメーションさせる
アニメーションさせたい要素がv-for
でつくられるリストのような場合には、<transition-group>
コンポーネントを用います(「リストトランジション」参照)。コンポーネント<transition-group>
は、<transition>
と異なり、ラップするのでなく実際の要素として描画されます。要素の種類を定めるのはtag
属性です(デフォルトは<span>
)。v-for
でつくるリストの親要素として加わります。なお、<transition-group>
コンポーネントを使うとき、v-for
で差し込む要素には必ずkey
特別属性をv-bind
ディレクティブ(省略記法:
)で一意の値にバインドしなければなりません(「Vue.js: v-forで項目インデックスをkey属性にしていいのか」参照)。また、アニメーション(transition
)を定めるクラス(todo-item
)は、テンプレートのクラスバインディング(:class
)に配列構文で加えました(「Vue.js: 複数のクラスをバインディングする場合どのような書き方があるか」の「配列構文を使う」参照)[*01]。
src/components/TodoList.vue<template> <section class="main" v-show="todos.length" v-cloak> <!-- <ul class="todo-list"> --> <!-- class="todo" --> <transition-group class="todo-list" name="todo-item" tag="ul"> <li v-for="todo in filteredTodos" :key="todo.id" :class="['todo-item', {completed: todo.completed, editing: todo == editedTodo}]"> </li> <!-- </ul> --> </transition-group> </section> </template> <style scoped> .todo-item { transition: 1s; } .todo-item-enter, .todo-item-leave-to { opacity: 0; transform: translateX(200px); } .todo-item-leave-active { position: absolute; } </style>
これで、リストの項目を加えたり除いたりすると、水平に移動しつつフェードイン・フェードアウトするようになります。コンポーネントsrc/components/TodoEdit.vue
のコード全体は、つぎにまとめたとおりです。
コード002■要素の追加と削除でアニメーションさせる
src/components/TodoList.vue
<template>
<section class="main" v-show="todos.length" v-cloak>
<input id="toggle-all" class="toggle-all" type="checkbox"
:value="allDone"
:checked="allDone"
@change="onInput">
<label for="toggle-all"></label>
<transition-group class="todo-list" name="todo-item" tag="ul">
<li v-for="todo in filteredTodos"
:key="todo.id"
:class="['todo-item', {completed: todo.completed, editing: todo == editedTodo}]">
<todo-item
:todo="todo"
@remove-todo="removeTodo"
@done="done"
@edit-todo="editTodo">
</todo-item>
<todo-edit
:todo="todo"
:editedTodo="editedTodo"
@done-edit="doneEdit"
@cancel-edit="cancelEdit">
</todo-edit>
</li>
</transition-group>
</section>
</template>
<script>
import TodoItem from './TodoItem.vue';
import TodoEdit from './TodoEdit.vue';
export default {
name: 'TodoList',
components: {
TodoItem,
TodoEdit
},
props: {
todos: Array,
filteredTodos: Array,
allDone: Boolean
},
data() {
return {
editedTodo: null
};
},
methods: {
removeTodo(todo) {
this.$emit('remove-todo', todo);
},
done(todo, completed) {
this.$emit('done', todo, completed);
},
onInput() {
this.$emit('allDone', !this.allDone);
},
editTodo(todo) {
this.editedTodo = todo;
},
doneEdit(todoTitle) {
if (!this.editedTodo) {
return;
}
const title = todoTitle.trim();
if (title) {
this.editedTodo.title = title;
} else {
this.removeTodo(this.editedTodo);
}
this.editedTodo = null;
},
cancelEdit() {
this.editedTodo = null;
}
}
}
</script>
<style scoped>
[v-cloak] {
display: none;
}
.todo-item {
transition: 1s;
}
.todo-item-enter, .todo-item-leave-to {
opacity: 0;
transform: translateX(200px);
}
.todo-item-leave-active {
position: absolute;
}
</style>
サンプル001■vue-todo-mvc-08
[*01] class
属性に与えられていたクラス(todo
)は、CSS(index.css
)に定めがなかったので除きました。
Vue.js + CLI入門
- Vue.js + CLI入門 01: リスト項目の表示と追加
- Vue.js + CLI入門 02: リスト項目の削除とスタイル変更
- Vue.js + CLI入門 03: データの計算処理とローカルへの保存
- Vue.js + CLI入門 04: リスト表示する項目を選び出して切り替える
- Vue.js + CLI入門 05: チェックをまとめてオン/オフしたり削除する
- Vue.js + CLI入門 06: 項目のテキストをダブルクリックで再編集する
- Vue.js + CLI入門 07: フォーカスをコントロールする
- Vue.js + CLI入門 08: 要素にアニメーションを加える
作成者: 野中文雄
作成日: 2019年10月02日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.