サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 04: リスト表示する項目を選び出して切り替える


Vue.js公式サイトの「TodoMVC の例」を単一ファイルコンポーネント(.vue)でつくるシリーズの第4回、加える機能はデータのフィルタリングです。チェックをつけた処理済みと、まだついていない未処理の項目を切り替えて表示できるようにします。

01 リンクに定めたハッシュからキーワードを取り出す

全表示と未処理、処理済み、3つの切り替えは、リンクの<a>要素で行うことにしましょう。フッターのコンポーネント(src/components/TodoController.vue)に、つぎのようにリスト(<ul>要素)の項目(<li>要素)に加えます(図001)。 href属性にハッシュ#で与えたのが、切り替えの処理に用いるキーワードです。

src/components/TodoController.vue: <template>

<footer class="footer" v-show="todos.length" v-cloak>
	<span class="todo-count">

	</span>
	<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>

図001■フッターに表示切り替えのリンクが加わった

図001

ハッシュが切り替わったことを捉えるのはhashchangeイベントです。リスナーメソッドは、mountedのオプションで加えることにします。呼び出されるのは、Vueインスタンスのルートがドキュメントの要素として組み込まれるときです。

ハッシュの文字列(DOMString)は、プロパティwindow.locationからLocation.hashで得られます。要らない記号(#/)は正規表現によりつぎのようにString.replace()メソッドで除いて、取り出したのがリンクのキーワード(visibility)です。console.log()メソッドでその文字列を確かめます。リンクをクリックすれば、そのキーワードがコンソールに出力されるはずです。

src/App.vue: <script>

export default {

	mounted() {
		window.addEventListener('hashchange', this.onHashChange);
	},
	methods: {

		onHashChange() {
			const visibility = window.location.hash.replace(/#\/?/, '');
			console.log(visibility);  // 確認用
		}
	}
}

02 データからリスト項目をフィルタリングして切り替える

リスト項目の表示が切り替えられるよう、実はすでに用いるデータには仕込みがしてありました。リスト表示のコンポーネント(src/components/TodoList.vue)で、項目の<li>要素はつぎのようにv-forディレクティブでつくっています。注目していただきたいのは、データを取り出すプロパティです。項目データ(todos)そのものでなく、親アプリケーション(src/App.vue)から算出プロパティ(filteredTodos)が与えてあります。今は、返す値は項目データです。けれど、この戻り値を変えれば、もとデータはそのままで、リスト表示する項目が変えられるのです(「フィルタ」参照)。

src/App.vue
	
<template>
	<section id="app" class="todoapp">

		<todo-list

			:filtered-todos="filteredTodos"

		>
		</todo-list>

	</section>
</template>

<script>

export default {

	data() {
		return {
			todos: todoStorage.fetch()
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		},

	},

}
</script>

src/components/TodoList.vue

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

		<ul class="todo-list">
			<li v-for="todo in filteredTodos"

			>
				<todo-item

				>
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>

export default {

	props: {

		filteredTodos: Array
	},

}
</script>

切り替えは、全表示(all)と未処理( active)、および処理済み(completed)の3つでした(前掲図001)。算出プロパティ(filteredTodos)には、これらに応じた3つの関数を用意しなければなりません。そこで、新たなオブジェクト(filters)をつくって与えたのが3つのメソッドです。メソッド名は、リンクのハッシュキーワードと揃えました。3つの表示のどれが今選ばれているかは、Vueインスタンスのdataにプロパティ(visibility)としてとっておきます。

hashchangeイベントのリスナー関数(onHashChange())がハッシュのキーワードを表示のプロパティ(visibility)値に与えると、算出プロパティ(filteredTodos)はフィルタのオブジェクト(filters)から対応するメソッドを選んで表示する項目を改めるという流れになります。メソッドはArray.filter()により、リストのデータから抜き出した項目を配列にして返しています。戻り値は新たな配列なので、リスト項目のもとデータ(todos)はそのまま変わりません。

src/App.vue: <script>

const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.completed
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.completed
		);
	}
};
export default {

	data() {
		return {

			visibility: 'all'
		}
	},
	computed: {
		filteredTodos() {
			// return this.todos;
			return filters[this.visibility](this.todos);
		},

	},

	methods: {

		onHashChange() {

			// console.log(visibility);  // 確認用
			this.visibility = visibility;
		}
	}
}

03 スクリプトを整理する

フィルタの機能を加えたことにより、少しスクリプトに無駄ができましたので整理しましょう。フィルタのオブジェクト(filters)に備えたチェックのない未処理項目の配列を返すメソッド(active())は、前にVueインスタンスに定めてあったメソッド(getActive())と同じ処理です。したがって、つぎのようにmethodsから除き、フィルタのメソッドからの呼び出しに一本化します。

src/App.vue

const filters = {

	active(todos) {
		return todos.filter((todo) =>
			!todo.completed
		);
	},

};

export default {

	computed: {

		remaining() {
			// const todos = this.getActive(this.todos);
			const todos = filters.active(this.todos);

		}
	},

	methods: {

		/* getActive(todos) {
			return todos.filter((todo) =>
				!todo.completed
			);
		}, */

	}
}

04 選んだリンクのスタイルを変える

フィルタを切り替えるリンク(<a>要素)にはマウスポインタを重ねたとき(:hover擬似クラス)のスタイルは、インポートしたCSSファイル(index.css)から<li>要素にすでに与えられています。クリックして選んだときのスタイル(selected)を、つぎのようにv-bind:class(省略記法:class)構文で定めましょう(「Vue.js + CLI入門 02」03「チェックした項目にスタイルを割り当てる」参照)。これで選んだフィルタのリンクに、スタイルが割り当てられます(図002)。

src/components/TodoController.vue: <template>

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

	<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>

図002■選んだリンクのスタイルが変わる

図002

ここまで書き上げたアプリケーション(src/App.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"
			@remove-todo="removeTodo"
			@done="done">
		</todo-list>
		<todo-controller
			:todos="todos"
			:remaining="remaining"
			:visibility="visibility">
		</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;
		}
	},
	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;
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</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>
	</footer>
</template>

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

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

Vue.js + CLI入門


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


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