HTML5テクニカルノート
Vue.js + Vuex入門 04: リスト表示する項目を選び出して切り替える
- ID: FN1910003
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
単一ファイルコンポーネントに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.jsconst 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つのフィルタボタン
コンポーネント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.vue
のmounted
のオプションで、イベントリスナーを加えます。ハッシュが切り替わったことを捉えるのはhashchange
イベントです。ハッシュの文字列(DOMString
)は、プロパティwindow.location
からLocation.hash
で得られます。要らない記号(#
と/
)は正規表現によりつぎのようにString.replace()
メソッドで除いて、取り出されるのがフィルタのキーワードです。これをフィルタ選択のプロパティ(visibility
)に与えれば、クリックしたボタンのスタイルが変わります(図002)。
src/App.vueexport default { mounted() { window.addEventListener('hashchange', () => store.commit('hashChange') ); } }
src/store.jsexport default new Vuex.Store({ mutations: { hashChange(state) { const visibility = window.location.hash.replace(/#\/?/, ''); state.visibility = visibility; } } });
図002■クリックしたフィルタボタンが選択されたスタイルに変わる
03 フィルタで表示項目を切り替える
それでは、リスト表示する項目をフィルタで切り替えましょう。もっとも、モジュールsrc/store.js
にすでに仕込みはしてあります。表示項目を返すgetters
のfilteredTodos()
です。今までは戻り値が、すべての項目リスト(todos
)そのままでした。そうでなく、フィルタオブジェクト(filters
)から選択したメソッドに渡し、フィルタリングされた項目リストを返せばよいのです。これで、フィルタボタンにより、リスト表示する項目が切り替えられます。
src/store.jsconst 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.vue
のmounted
オプションで、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.vueexport default { mounted() { store.commit('hashChange'); } }
src/store.jsexport default new Vuex.Store({ hashChange(state) { if (filters[visibility]) { state.visibility = visibility; } } } });
モジュールsrc/store.js
とsrc/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;
}
}
}
});
<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入門
- Vue.js + Vuex入門 01: Storeを使う
- Vue.js + Vuex入門 02: クラスバインディングと項目の削除
- Vue.js + Vuex入門 03: データの変化に応じた処理とローカルへの保存
- Vue.js + Vuex入門 04: リスト表示する項目を選び出して切り替える
- Vue.js + Vuex入門 05: チェックをまとめてオン/オフする
- Vue.js + Vuex入門 06: チェックした項目をまとめて削除する
- Vue.js + Vuex入門 07: 項目のテキストをダブルクリックで再編集する
- Vue.js + Vuex入門 08: フォーカスをコントロールする
作成者: 野中文雄
作成日: 2019年10月13日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.