サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 05: チェックをまとめてオン/オフしたり削除する


Vue.js公式サイトの「TodoMVC の例」を単一ファイルコンポーネント(.vue)でつくるシリーズの第5回で加えるのは、リスト項目をまとめて処理するふたつの機能です。ひとつは、リスト項目すべてのチェックを一度にオン/オフできるようにします。もうひとつは、チェックされた項目をまとめて削除できるボタンです。

01 項目のチェックをまとめてオン/オフする

まず、リスト項目の処理済みチェックを、まとめてオン/オフできるようにしましょう。リスト表示のコンポーネント(src/components/TodoList.vue)には、先頭にまだ使っていないチェックボックスの<input>要素(type属性checkbox)がすでに加えてありました。このチェックボックスを親のアプリケーションのプロパティ(allDone)と、つぎのように双方向でデータバインディングします。チェックのプロパティ(checked)をバインディングするのも忘れないでください。そして、<input type="checkbox">要素には、id属性が与えられています。対応する<label>要素を加えると、index.cssによりチェックサインが表れるはずです(後掲図001参照)。

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>

	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {

	props: {

		allDone: Boolean
	},
	methods: {

		onInput() {
			this.$emit('allDone', !this.allDone);
		}
	}
}
</script>

親アプリケーション(src/App.vue)のバインディング先は、算出プロパティ(allDone)です。算出プロパティは、通常は値を返すgetter関数として扱われます。けれど、オブジェクトにしてメソッドget()set()をそれぞれ定めることもできるのです(「算出Setter関数」参照)。すると、算出プロパティの値はもちろん、他のデータを書き替えたり、さらに別の処理も加えられます。つぎの算出プロパティの値は、すべての項目に処理済みのチェックがされているかどうか、getterが返すブール(論理)値です。そして、setterはチェックボックスから渡された引数値(value)を、リストデータ(todos)のすべての項目のプロパティ(completed)に定めます。

src/App.vue

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

		<todo-list

			:allDone="allDone"

			@allDone="onAllDone">
		</todo-list>

	</section>
</template>

<script>

export default {

	computed: {

		allDone: {
			get() {
				return this.remaining === 0;
			},
			set(value) {
				this.todos.forEach((todo) =>
					todo.completed = value
				);
			}
		}
	},

	methods: {

		onAllDone(done) {
			this.allDone = done;
		}
	}
}
</script>

リスト左上のチェックボックスで、リスト項目すべてのチェックがまとめて変えられるようになりました。未処理の項目があれば、クリックですべての項目に処理済みのチェックがつきます(図001)。未処理がなくなっている場合は、逆にチェクが全部外されるということです。

図001■リスト左上のチェックボックスですべての項目のチェックがまとめて変えられる

図001

02 チェック済みの項目すべてをリストから除く

チェック済みの項目すべてをリストから除くボタン(<button>要素)は、以下のようにフッターのコンポーネント(src/components/TodoController.vue)に加えます(図002)。v-on:click(省略記法@click)イベントに定めたメソッド(removeCompleted())は、お約束どおり$emit()メソッドで親アプリケーションにイベント(removeCompleted)を送る役割です。また、v-showディレクティブは、リストの全項目数より残り(チェックなし)項目数(算出プロパティremaining)が少ないことを条件にしているので、チェックされている項目がなければ表示されません。

イベント(removeCompleted)を受け取ったアプリケーション(src/App.vue)は、ハンドラメソッド(removeCompleted())を呼び出し、フィルタのメソッド(filters.active())により未処理の項目を取り出したうえで、リスト項目のデータ(todos)を書き替えればよいでしょう。こうして、処理済みの項目はデータから除かれることになるのです。

src/App.vue

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

		<todo-controller

			@removeCompleted="removeCompleted">
		</todo-controller>
	</section>
</template>

<script>

export default {

	methods: {

		removeCompleted() {
			this.todos = filters.active(this.todos);
		}
	}
}
</script>
src/components/TodoController.vue

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

		<button class="clear-completed"
			v-show="todos.length > remaining"
			@click="removeCompleted">
			Clear completed
		</button>
	</footer>
</template>

<script>
export default {

	methods: {
		removeCompleted() {
			this.$emit('removeCompleted');
		}
	}
}
</script>

図002■チェックした処理済みの項目がまとめて除かれる

図002

ここまで書き上げたアプリケーション(src/App.vue)とリスト表示(src/components/TodoController.vue)およびフッター(src/components/TodoController.vue)のコンポーネント(VUE)ファイルの中身を、つぎのコード001にまとめました。他のコンポーネントも含めたアプリケーションのファイル全体については、CodeSandboxに公開した以下のサンプル001をご参照ください。

コード001■未処理の項目数を示す

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<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>

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>
		<ul class="todo-list">
			<li v-for="todo in filteredTodos"
				class="todo"
				:key="todo.id"
				:class="{completed: todo.completed}">
				<todo-item
					:todo="todo"
					@remove-todo="removeTodo"
					@done="done">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem
	},
	props: {
		todos: Array,
		filteredTodos: Array,
		allDone: Boolean
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		},
		done(todo, completed) {
			this.$emit('done', todo, completed);
		},
		onInput() {
			this.$emit('allDone', !this.allDone);
		}
	}
}
</script>

<style scoped>
[v-cloak] {
	display: none;
}
</style>

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>
		<span class="todo-count">
			<strong>{{remaining}}</strong> {{remaining | 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>
		<button class="clear-completed"
			v-show="todos.length > remaining"
			@click="removeCompleted">
			Clear completed
		</button>
	</footer>
</template>

<script>
export default {
	name: 'TodoController',
	filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	},
	props: {
		todos: Array,
		remaining: Number,
		visibility: String
	},
	methods: {
		removeCompleted() {
			this.$emit('removeCompleted');
		}
	}
}
</script>

サンプル001■vue-todo-mvc-05

Vue.js + CLI入門


作成者: 野中文雄
更新日: 2019年10月14日 src/App.vueのコードを一部修正。
更新日: 2019年09月13日 本文に一部加筆。
更新日: 2019年09月06日 サンプル001を追加。
更新日: 2019年03月23日 ブラウザの違いによる問題に対応。
作成日: 2019年01月02日


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