サイトトップ

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

HTML5テクニカルノート

Vue.js + Vuex入門 03: データの変化に応じた処理とローカルへの保存


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

export 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■チェックされていない項目数がフッタに示される

図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■項目がひとつのときは単数形の単語が示される

図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.js

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()  // [],
		// 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()メソッドを使えば、状態が変わったとき必ず決まった処理が行えます。ふたつの引数は、いずれも関数です。ひとつめの関数は、監視するデータを返します。引数にstategettersを受け取ります。ふたつめの関数が、状態の変更により呼び出されるコールバックです。監視しているデータの新しい値ともとの値が引数になります。

store.watch(
	(state, getters) => 監視するデータ,
	(新しい値, もとの値) => 行う処理
);

すると、データ保存のためのメソッド(save())はモジュールsrc/store.jsに新たに加えたうえで、アプリケーションモジュールsrc/App.vueからwatch()のコールバックに呼び出しを加えればよさそうです。

src/App.vue

export default {

	mounted() {
		store.watch(
			(state, getters) => state.todos,
			(newValue, oldValue) => store.commit('save')
		);
	}
}

src/store.js

export 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.js

export 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);
		}
	}
});

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')
		);
	}
};
</script>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

src/components/TodoItem.vue

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


作成者: 野中文雄
作成日: 2019年09月26日


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