サイトトップ

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

HTML5テクニカルノート

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


単一ファイルコンポーネントにVuexのStoreを加えてつくるTodoMVCアプリケーションのチュートリアルシリーズ「Vue.js + Vuex入門」の第4回、加える機能はデータのフィルタリングです。チェックをつけた処理済みと、まだついていない未処理の項目を示できるようにします。

01 フィルタボタンをつくって設定する

まず、モジュールsrc/store.jsに定めるのは、フィルタリングのメソッド3つを収めたオブジェクト(filters)です。もっとも、まだ空っぽで実装はこのあと行います。というのは、この3つのメソッド名が、選択するフィルタのキーワードにもなるからです。選ばれたフィルタのキーワードは、stateのプロパティ(visibility)にもたせます(初期値all)。

全表示(all)と未処理(active)、処理済み(completed)、3つのフィルタは、リンクの<a>要素で切り替えることにしましょう。フッタのコンポーネント(src/components/TodoController.vue)に、つぎのようにリスト項目(<li>要素)として加えます(図001)。それぞれのhref属性に、キーワード(key)がハッシュ#で与えられていることにご注目ください。

src/store.js

const filters = {
	all(todos) {},
	active(todos) {},
	completed(todos) {}
};
export default new Vuex.Store({
	state: {

		visibility: 'all'
	},
	getters: {

		filters: (state) => filters
	},

});

src/components/TodoController.vue

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

		<ul class="filters">
			<li v-for="(value, key) in filters" :key="key">
				<a
					:href="'#/' + key"
					:class="{selected: visibility === key}"
				>
					{{ key[0].toUpperCase() + key.substr(1) }}
				</a>
			</li>
		</ul>
	</footer>
</template>

<script>
export default {

	computed: {

		filters() {
			return this.$store.getters.filters;
		},
		visibility() {
			return this.$store.state.visibility;
		}
	}
};
</script>

図001■フッタに加えられた3つのフィルタボタン

図001

コンポーネントsrc/components/TodoController.vueは、これででき上がりです。つぎのコード001に全体をまとめておきましょう。

コード001■フィルタボタンが定められたフッタコンポーネント

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 v-for="(value, key) in filters" :key="key">
				<a
					:href="'#/' + key"
					:class="{selected: visibility === key}"
				>
					{{ key[0].toUpperCase() + key.substr(1) }}
				</a>
			</li>
		</ul>
	</footer>
</template>

<script>
export default {
	name: 'TodoController',
	filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	},
	computed: {
		todos() {
			return this.$store.state.todos;
		},
		remaining() {
			return this.$store.getters.remaining;
		},
		filters() {
			return this.$store.getters.filters;
		},
		visibility() {
			return this.$store.state.visibility;
		}
	}
};
</script>

02 選択したフィルタボタンのスタイルを変える

前掲コード001のフッタコンポーネントで、フィルタボタン(<a>要素)のスタイルはクラスバインディングしてありました。Storeのstateでフィルタ選択のプロパティ(visibility)の値に応じて、選ばれたボタンのクラス(selected)がスタイルに割り当てられるのです。

そこで、アプリケーションモジュールsrc/App.vuemountedのオプションで、イベントリスナーを加えます。ハッシュが切り替わったことを捉えるのはhashchangeイベントです。ハッシュの文字列(DOMString)は、プロパティwindow.locationからLocation.hashで得られます。要らない記号(#/)は正規表現によりつぎのようにString.replace()メソッドで除いて、取り出されるのがフィルタのキーワードです。これをフィルタ選択のプロパティ(visibility)に与えれば、クリックしたボタンのスタイルが変わります(図002)。

src/App.vue

export default {

	mounted() {

		window.addEventListener('hashchange', () =>
			store.commit('hashChange')
		);
	}
}

src/store.js

export default new Vuex.Store({

	mutations: {

		hashChange(state) {
			const visibility = window.location.hash.replace(/#\/?/, '');
			state.visibility = visibility;
		}
	}
});

図002■クリックしたフィルタボタンが選択されたスタイルに変わる

図002

03 フィルタで表示項目を切り替える

それでは、リスト表示する項目をフィルタで切り替えましょう。もっとも、モジュールsrc/store.jsにすでに仕込みはしてあります。表示項目を返すgettersfilteredTodos()です。今までは戻り値が、すべての項目リスト(todos)そのままでした。そうでなく、フィルタオブジェクト(filters)から選択したメソッドに渡し、フィルタリングされた項目リストを返せばよいのです。これで、フィルタボタンにより、リスト表示する項目が切り替えられます。

src/store.js

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

	getters: {
		filteredTodos: (state) =>  // state.todos,
			filters[state.visibility](state.todos),

		},

	}
});

04 予定外の操作に対応する

少し試してみると、いくつか不具合が見つかるでしょう。まず、初期値(all)以外のフィルタを選んだうえで、ブラウザを再読み込みしたときです。フィルタの選択と表示項目は初期化されるものの、URLはそのまま変わりません。そのため、直前に選んでいたボタンをクリックしてもhashchangeイベントは起こらず、表示リストが書き替わらないのです。

対応するには、アプリケーションコンポーネントsrc/App.vuemountedオプションで、hashChangeイベントのリスナーを定めたすぐあとに、呼び出してしまえばよいでしょう。そうすると今度は、URLにハッシュのないルートを指定したり、キーワードにないハッシュを入力したとき、つぎのようなエラーが出てしまいます。そこで、以下のsrc/store.jsモジュールのメソッド(hashChange())に加えたのが、対応するフィルタメソッドがあるかどうかの判定です。


[Vue warn]: Error in render: "TypeError: _filters[state.visibility] is not a function"

found in

---> <TodoList> at src/components/TodoList.vue
       <App> at src/App.vue
         <Root>

src/App.vue

export default {

	mounted() {

		store.commit('hashChange');
	}
}

src/store.js

export default new Vuex.Store({

		hashChange(state) {

			if (filters[visibility]) {
				state.visibility = visibility;
			}
		}
	}
});

モジュールsrc/store.jssrc/App.vueの書き替えも済みました。ふたつのコード全体をつぎのコード002に掲げます。また、各モジュールのコードや動きは、CodeSandboxに公開した以下のサンプル001をご覧ください。

コード002■表示するリスト項目をフィルタで切り替える

src/store.js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
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 new Vuex.Store({
	state: {
		todos: todoStorage.fetch(),
		visibility: 'all'
	},
	getters: {
		filteredTodos: (state) =>
			filters[state.visibility](state.todos),
		remaining: (state) => {
			const todos = state.todos.filter((todo) => !todo.completed);
			return todos.length;
		},
		filters: (state) => filters
	},
	mutations: {
		addTodo(state, todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			state.todos.push({
				id: todoStorage.uid++,
				title: newTodo,
				completed: false
			});
		},
		removeTodo(state, todo) {
			state.todos = state.todos.filter((item) => item !== todo);
		},
		done(state, {todo, completed}) {
			state.todos = state.todos.map((item) => {
				if(item === todo) {
					item.completed = completed
				}
				return item;
			});
		},
		save(state) {
			todoStorage.save(state.todos);
		},
		hashChange(state) {
			const visibility = window.location.hash.replace(/#\/?/, '');
			if (filters[visibility]) {
				state.visibility = visibility;
			}
		}
	}
});

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input />
		</header>
		<todo-list />
		<todo-controller />
	</section>
</template>

<script>
import store from './store';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';
export default {
	name: 'app',
	store,
	components: {
		TodoInput,
		TodoList,
		TodoController
	},
	mounted() {
		store.watch(
			(state, getters) => state.todos,
			(newValue, oldValue) => store.commit('save')
		);
		window.addEventListener('hashchange', () =>
			store.commit('hashChange')
		);
		store.commit('hashChange');
	}
};
</script>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

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

Vue.js + Vuex入門


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


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