サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6: TodoMVCをつくる 03 ー 表示する項目をフィルタで切り替える


Vue.js + ES6: TodoMVCをつくる 02 ー データをローカルに保存する」(以下「Vue.js + ES6: TodoMVCをつくる 02」)では、データをローカルで扱って、項目リストを読み込み、保存できるようにしました。今回は、そのデータから表示する項目をフィルタ機能で切り替えます。

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

リストの項目のうち、チェックがついて済んだものとそうでないものを、切り替えて表示できるようにしましょう。選択はリンク(<a>要素)にして、つぎのようにフッター(<footer>要素)に<li>要素で加えます(図001)。 href属性にハッシュ#で与えたのが、切り替えのキーワードです。

<body>要素

<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

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

script.js

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

02 データの項目をフィルタで切り替える

リスト項目の表示が切り替えられるよう、実はすでに<body>要素には仕込みがしてありました。項目の<li>要素はつぎのようにv-forディレクティブでつくっています。注目していただきたいのは、データを取り出すプロパティです。項目データ(todos)そのものでなく、算出プロパティ(filteredTodos)が与えてあります。算出プロパティのメソッドの処理を変えれば、もとデータはそのままで、リスト表示する項目が変えられるということです(「フィルタ」参照)。

<body>要素

<section class="main" v-show="todos.length" v-cloak>
	<ul class="todo-list">
		<li v-for="todo in filteredTodos"

			:class="{completed: todo.completed}">

		</li>
	</ul>
</section>

表示を切り替えるリンクはフッターに3つ設けました(前掲図001)。算出プロパティ(filteredTodos)に定める関数も3つ用意しなければなりません。これらは以下のように、新たなオブジェクト(filters)にメソッドとして与えます。メソッド名(allactiveおよびcompleted)は、リンクのハッシュのキーワードと揃えました。3つの表示のどれが今選ばれているかは、Vue()コンストラクタに渡すオブジェクトのdataにプロパティ(visibility)として加えます。

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

script.js

const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.completed
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.completed
		);
	}
};
const app = new Vue({
	data: {

		visibility: 'all'
	},

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

	},

});
function onHashChange() {
	const visibility = window.location.hash.replace(/#\/?/, '');
	// console.log(visibility);
	if (filters[visibility]) {
		app.visibility = visibility;
	} else {
		window.location.hash = '';
		app.visibility = 'all';
	}
}

03 スクリプトを整理する

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

script.js

const app = new Vue({

	computed: {

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

	},

	methods: {

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

});

もうひとつ、window.onhashchangeイベントのリスナー関数(onHashChange())は、はじめに1度初期化のために呼び出すことにしました。

script.js

function onHashChange() {

}
window.addEventListener('hashchange', onHashChange);
onHashChange();

04 選ばれたフィルタのリンクにスタイルを与える

フィルタを切り替えるリンク(<a>要素)にはマウスポインタを重ねたとき(:hover擬似クラス)のスタイルは与えられています。クリックして選んだときのスタイルを、つぎのようにv-bind:class(省略記法:class)構文で定めましょう(「Vue.js + ES6入門 02」03「バインディングでクラス属性を動的に変える」参照)。これで選んだフィルタのリンクに、スタイルが与えられます(図002)。以下のコード001には、HTMLドキュメントの<body>要素とJavaScriptファイル(script.js)の中身をまとめました。また、サンプル001をCodePenに掲げています。

<body>要素

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

図002■選んだフィルタのリンクにスタイルが与えられた

図002

コード001■リストに表示される項目がフィルタで切り替わる

<body>要素

<section class="todoapp">
	<header class="header">
		<h1>todos</h1>
		<input class="new-todo"
			autofocus
			autocomplete="off"
			placeholder="What needs to be done?"
			v-model="newTodo"
			@keypress.enter="addTodo">
	</header>
	<section class="main" v-show="todos.length" v-cloak>
		<ul class="todo-list">
			<li v-for="todo in filteredTodos"
				class="todo"
				:key="todo.id"
				:class="{completed: todo.completed}">
				<div class="view">
					<input class="toggle" type="checkbox" v-model="todo.completed">
					<label>{{todo.title}}</label>
					<button class="destroy" @click="removeTodo(todo)"></button>
				</div>
			</li>
		</ul>
	</section>
	<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>
</section>

script.js

const STORAGE_KEY = 'todos-vuejs-2.0';
const todoStorage = {
	fetch() {
		const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
		todos.forEach((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
		);
	}
};
const app = new Vue({
	data: {
		todos: todoStorage.fetch(),
		newTodo: '',
		visibility: 'all'
	},
	watch: {
		todos: {
			handler(todos) {
				todoStorage.save(todos);
			},
			deep: true
		}
	},
	computed: {
		filteredTodos() {
			return filters[this.visibility](this.todos);
		},
		remaining() {
			const todos = filters.active(this.todos);
			return todos.length;
		}
	},
	filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	},
	methods: {
		addTodo() {
			const value = this.newTodo && this.newTodo.trim();
			if (!value) {
				return;
			}
			this.todos.push({
				id: todoStorage.uid++,
				title: value,
				completed: false
			});
			this.newTodo = '';
		},
		removeTodo(todo) {
			this.todos.splice(this.todos.indexOf(todo), 1);
		}
	}
});
function onHashChange () {
	const visibility = window.location.hash.replace(/#\/?/, '');
	if (filters[visibility]) {
		app.visibility = visibility;
	} else {
		window.location.hash = '';
		app.visibility = 'all';
	}
}
window.addEventListener('hashchange', onHashChange);
onHashChange();
app.$mount('.todoapp');

サンプル001■Vue.js + ES6: Vue TodoMVC 03

See the Pen Vue.js + ES6: Vue TodoMVC 03 by Fumio Nonaka (@FumioNonaka) on CodePen.


作成者: 野中文雄
更新日: 2019年11月14日 構文をECMAScript 2015に改め、本文も修正。サンプルはCodePenに差し替えた。
作成日: 2017年07月03日


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