HTML5テクニカルノート
Vue.js + ES6: TodoMVCをつくる 03 ー 表示する項目をフィルタで切り替える
- ID: FN1707001
- Technique: HTML5 / JavaScript
- Library: Vue.js 2.6.10
「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■フッターに表示切り替えのリンクが加わった
ハッシュが変わったことを捉えるのはwindow.onhashchange
イベントです。ハッシュの文字列(DOMString
)は、プロパティwindow.location
からLocation.hash
で得られます。要らない記号(#
と/
)は正規表現によりつぎのようにString.replace()
メソッドで除いて、取り出したのがリンクのキーワード(visibility
)です。console.log()
メソッドでその文字列を確かめています。
script.jsfunction 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
)にメソッドとして与えます。メソッド名(all
とactive
およびcompleted
)は、リンクのハッシュのキーワードと揃えました。3つの表示のどれが今選ばれているかは、Vue()
コンストラクタに渡すオブジェクトのdata
にプロパティ(visibility
)として加えます。
window.onhashchange
イベントのリスナー関数(onHashChange()
)は表示のプロパティ(visibility
)値をハッシュのキーワードに変え、算出プロパティ(filteredTodos
)がフィルタのオブジェクト(filters)から選んだメソッドで表示する項目を改めればよいでしょう。メソッドはArray.filter()
により、リストのデータから抜き出した項目を配列にして返しています。戻り値は新たな配列なので、リスト項目のデータ(todos
)はもとのまま変わりません。
script.jsconst 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.jsconst 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.jsfunction 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■選んだフィルタのリンクにスタイルが与えられた
コード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>
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.
- Vue.js + ES6: TodoMVCをつくる 01 ー 項目の追加と削除および残り項目数表示
- Vue.js + ES6: TodoMVCをつくる 02 ー データをローカルに保存する
- Vue.js + ES6: TodoMVCをつくる 03 ー 表示する項目をフィルタで切り替える
- Vue.js + ES6: TodoMVCをつくる 04 ー チェックをまとめてオン/オフしたり削除する
- Vue.js + ES6: TodoMVCをつくる 05 ー 項目のテキストを再編集できるようにする
作成者: 野中文雄
更新日: 2019年11月14日 構文をECMAScript 2015に改め、本文も修正。サンプルはCodePenに差し替えた。
作成日: 2017年07月03日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.