HTML5テクニカルノート
Vue.js + Vuex入門 03: データの変化に応じた処理とローカルへの保存
- ID: FN1909004
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
「Vue.js + Vuex入門 02: クラスバインディングと項目の削除」では、チェック済み項目のスタイルを変え、項目ごとに削除ができるようにしました。今回加えるのは、データをローカルから読み込んで、追加や変更されたら保存する機能です。また、未処理の項目数をフッタに示すようにします。
01 未処理の項目数を示す
未処理の項目数を示すフッタから先に加えましょう。新たにフッタとしてつくるコンポーネントsrc/components/TodoController.vue
に定める中身はつぎのとおりです。テンプレートの要素(<span>
)にバインディングして表示するために加えた算出プロパティ(remaining()
)は、未処理の項目数をモジュールsrc/store.js
の同名getters
から得ます。なお、項目リストのコンポーネント(TodoList
)と同じく、項目(todos
)がないときはv-show
で表示しないようにします。
src/components/TodoController.vue<template> <footer class="footer" v-show="todos.length" v-cloak> <span class="todo-count"> <strong>{{remaining}}</strong> items left </span> </footer> </template> <script> export default { name: 'TodoController', computed: { todos() { return this.$store.state.todos; }, remaining() { return this.$store.getters.remaining; } } }; </script>
src/store.jsexport default new Vuex.Store({ getters: { remaining: (state) => { const todos = state.todos.filter( (todo) => !todo.completed ); return todos.length; } }, });
アプリケーションモジュールsrc/App.vue
は、新しいコンポーネントTodoController
を組み込むだけです。
src/App.vue<template> <section id="app" class="todoapp"> <todo-controller /> </section> </template> <script> import TodoController from './components/TodoController.vue'; export default { components: { TodoController } }; </script>
これで未処理、つまりチェックされていないリスト項目の数が、フッタに示されるようになります(図001)。チェックをつけたり外したりすると、Storeのgetters
のデータが変わり、コンポーネントの算出プロパティに渡されますので、バインディングされたフッタの値も改められるはずです。
図001■チェックされていない項目数がフッタに示される
02 フィルタを使う
未処理の項目数は英語で示しました。ということは、名詞は単数と複数でかたちが変わります。ところが、今は複数形(items)の決め打ちです(前掲図001)。そこで、コンポーネントsrc/components/TodoController.vue
のインスタンスオプションにfilters
を加えましょう。テンプレートの二重波かっこ{{{}}
に加えたパイプ|
の左辺がフィルタ対象で、右辺のフィルタメソッドに引数として渡され、テンプレートに示されるのは戻り値です(「フィルター」参照)。
{{ フィルタ対象 | フィルタメソッド }}
src/components/TodoController.vue<template> <footer class="footer" v-show="todos.length" v-cloak> <span class="todo-count"> <!-- <strong>{{remaining}}</strong> items left --> <strong>{{remaining}}</strong> {{remaining | pluralize}} left </span> </footer> </template> <script> export default { filters: { pluralize(n) { return n === 1 ? 'item' : 'items'; } }, }; </script>
これで、項目がひとつのときは単数形の単語(item)に変わります(図002)。なお、項目がないときは、v-show
によりコンポーネントは表示されませんので、考えなくて構いません[*01]。フッタコンポーネントの中身全体は、以下のコード001のとおりです。
図002■項目がひとつのときは単数形の単語が示される
コード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>
</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;
}
}
};
</script>
[*01] もっとも、英語で0の名詞には複数形が用いられます(「英語では『0個の』もの(名詞)は複数形で表現する」参照)。
03 データをローカルから読み込んで保存する
つぎは、リスト項目のデータをローカルにもつことにしましょう。用いるのはWeb Storage APIです。Window.localStorage
に保存すれば、ブラウザを閉じてもデータが残り、つぎに開いたときに表示されます。データの書き込みと読み込に使うメソッドは、それぞれStorage.setItem()
およびStorage.getItem()
です。データの形式はJSONにします。リスト項目のデータを読み書きするメソッドは、モジュールsrc/store.js
の中のStoreインスタンスとは別のオブジェクト(todoStorage
)に納めました。
src/store.jsconst 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)); } }; export default new Vuex.Store({ state: { todos: todoStorage.fetch() // [], // uid: 0 }, mutations: { addTodo(state, todoTitle) { state.todos.push({ id: todoStorage.uid++, // state.uid++, }); todoStorage.save(state.todos); }, removeTodo(state, todo) { todoStorage.save(state.todos); }, done(state, {todo, completed}) { todoStorage.save(state.todos); } } });
これで項目を追加したり、削除したり、さらに処理済みのチェックが切り替えられたときにも、データはローカルに保存されるでしょう。ただ、漏れていることがひとつ残っています。処理済みでつけたチェックが、データを読み込み直したとき項目から消えてしまうことです。チェックボックス(<input type="checkbox">
)のチェックの有無は属性checked
により決まります。そのバインディングを加えなければならないのです。
src/components/TodoItem.vue<template> <div class="view"> <input type="checkbox" :checked="todo.completed" > </div> </template>
04 watch()メソッドでデータを監視する
データのローカルへの保存は、意図どおりにできました。けれど、このあとさらにデータへの操作が加わったとき、いちいち忘れずに保存するというのは手間です。そういうとき、watch()
メソッドを使えば、状態が変わったとき必ず決まった処理が行えます。ふたつの引数は、いずれも関数です。ひとつめの関数は、監視するデータを返します。引数にstate
とgetters
を受け取ります。ふたつめの関数が、状態の変更により呼び出されるコールバックです。監視しているデータの新しい値ともとの値が引数になります。
store.watch( (state, getters) => 監視するデータ, (新しい値, もとの値) => 行う処理 );
すると、データ保存のためのメソッド(save()
)はモジュールsrc/store.js
に新たに加えたうえで、アプリケーションモジュールsrc/App.vue
からwatch()
のコールバックに呼び出しを加えればよさそうです。
src/App.vueexport default { mounted() { store.watch( (state, getters) => state.todos, (newValue, oldValue) => store.commit('save') ); } }
src/store.jsexport default new Vuex.Store({ mutations: { addTodo(state, todoTitle) { // todoStorage.save(state.todos); }, removeTodo(state, todo) { // todoStorage.save(state.todos); }, done(state, {todo, completed}) {
// todoStorage.save(state.todos); }, save(state) { todoStorage.save(state.todos); } } });
けれど、チェックボックスを切り替えたときだけは、watch()
メソッドのコールバックが呼び出されません。これはStoreのmutations
のメソッド(done()
)がstate
を介さずに、配列データ(todos
)の要素(todo
)を直に書き替えていたからです[*02]。state
のデータを変更するように改めれば、監視が働くようになります。書き替えたモジュール3つの中身は、以下のコード002のとおりです。他のモジュールのコードやアプリケーションの動きを確かめたい方は、CodeSandboxに公開したサンプル001をご覧ください。
src/store.jsexport default new Vuex.Store({ mutations: { done(state, {todo, completed}) { // todo.completed = completed; state.todos = state.todos.map((item) => { if(item === todo) { item.completed = completed } return item; }); }, } });
コード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));
}
};
export default new Vuex.Store({
state: {
todos: todoStorage.fetch()
},
getters: {
filteredTodos: (state) => state.todos,
remaining: (state) => {
const todos = state.todos.filter((todo) => !todo.completed);
return todos.length;
}
},
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);
}
}
});
<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')
);
}
};
</script>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>
<template>
<div class="view">
<input
type="checkbox" class="toggle"
:value="todo.completed"
:checked="todo.completed"
@input="onInput"
>
<label>{{todo.title}}</label>
<button
class="destroy"
@click="removeTodo">
</button>
</div>
</template>
<script>
export default {
name: 'TodoItem',
props: {
todo: Object
},
methods: {
removeTodo() {
this.$store.commit('removeTodo', this.todo);
},
onInput() {
this.$store.commit('done', {
todo: this.todo,
completed: !this.todo.completed
});
}
}
};
</script>
サンプル001■vue-vuex-todo-mvc-03
[*02] Vueインスタンスのオプションでデータを監視するwatch
では、deep: true
を加えると、データの中のネストされた深い階層の値の変更も検出されます(「Vue.js + CLI入門 03」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年09月26日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.