サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 08: 要素にアニメーションを加える


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のtransitionanimationなどのプロパティで、要素にアニメーションを与えます。たとえば、つぎのCSSが要素をフェードアウトする定めです。タイトルをクリックしてフェードアウトすることが確かめられたら、このCSSもクリックで要素を消す前掲のコードも削除してもとに戻してください。

src/App.vue

<style scoped>
.v-leave-active {
	transition: 1s ease-in;
}
.v-leave-to {
	opacity: 0;
}
</style>

02 要素をフェードインで登場させる

動的に追加・削除することなく、予め置いてある要素も、はじめにページが描かれるときアニメーションさせることができます。そのちために コンポーネントに加えるのがappear属性です(「初期描画時のトランジション」参照)。デフォルトではenterleaveのトランジションクラスが使えます。モジュール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

コード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入門


作成者: 野中文雄
作成日: 2019年10月02日


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