サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6: TodoMVCをつくる 02 ー データをローカルに保存する


Vue.js + ES6: TodoMVCをつくる 01 ー 項目の追加と削除および残り項目数表示」(以下「Vue.js + ES6: TodoMVCをつくる 01」)では、基本的な項目の追加と削除ができるようにし、チェックされていない項目数を表示しました。項目のデータはアプリケーションのプロパティに納めたので、ページを読み込み直せばすべて失われてしまいます。今回のお題は、項目のデータをローカルに保存することです。また、簡単なフィルタも使ってみます。

01 localStorageとJSONでデータをローカルから読み込み・保存する

データをローカルに保存するために使うのは、Window.localStorageプロパティです。Storageオブジェクトが返されますので、データはメソッドStorage.getItem()を使って読み込み、Storage.setItem()で保存できます。どちらも、第1引数にはデータを識別するためのキーの名前を文字列で渡します。Storage.setItem()メソッドの第2引数は、保存する文字列(DOMString)のデータです。

アプリケーションでは、データはJSONオブジェクトとして扱います。そのため、読み込んだ文字列のデータはメソッドJSON.parse()によりJSONとして解析し、JSON.stringify()でJSON文字列にしてから保存します。

Window.localStorageでデータを読み書きするためのオブジェクト(todoStorage)は、アプリケーションにつぎのように定めます。読み込みのメソッド(fetch())が、Storage.getItem()JSON.parse()で得たJSONオブジェクトにArray.forEach()メソッドを用いているのは、項目の追加・削除で抜けが出たidを連番にするためです。そのうえで、つぎの追加項目のid(uid)を決めてJSONオブジェクトが返されます。保存のメソッド(save())は、前述のとおりメソッドJSON.stringify()でデータを文字列にして、Storage.setItem()により保存しています。

script.js

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 app = new Vue({
	data: {
		todos: // [],
			todoStorage.fetch(),

	},

	methods: {
		addTodo() {

			this.todos.push({
				id: // this.todos.length,
					todoStorage.uid++,

			});

			todoStorage.save(this.todos);
		},
		removeTodo(todo) {

			todoStorage.save(this.todos);
		},

	}
});

アプリケーションの項目追加(addTodo())と削除(removeTodo())のメソッドは、処理の終わりに項目リストの配列(todos)をローカルデータのオブジェクト(todoStorage)のメソッド(save())で保存するようにしました。また、追加のメソッドはオブジェクトからつぎの項目のid(uid)を受け取ってカウントアップします。アプリケーションの動きは変わらないものの、項目リストのデータはローカルから読み書きされることになりました(図001)。なお、データはブラウザごとに扱われます。そのため、アプリケーションを違うブラウザで開くと、データは異なるのです。

図001■動きは変わらないもののデータがローカルに保存された

図001

02 データの更新を検知して処理する

データはローカルに保たれるようになりましたので、ページを読み込み直しても、あるいはブラウザを閉じてもまた同じブラウザで開くかぎり、項目データは失われません。ただし、ひとつ抜け落ちているデータがあります。チェックボックスのチェックです(図002)。つぎのように、<input>要素はv-modelディレクティブで項目のプロパティ(completed)とバインドしています。この値が変わったときに、ローカルデータのオブジェクト(todoStorage)で保存(save())しなければならないのです。

<body>要素

<ul class="todo-list">
	<li v-for="todo in filteredTodos"

		:class="{completed: todo.completed}">
		<div class="view">
			<input class="toggle" type="checkbox" v-model="todo.completed">

		</div>

	</li>
</ul>

図002■読み込み直すと項目は残ってもチェックが消える

図002

データ変更の操作ひとつひとつに保存の処理を書き加えるのでなく、データを監視してどこからの操作であれ変更が加えられたら処理できると便利です。ウォッチャを使えば、データが変わるのに応じて非同期やコストの高い処理が実行できます。Vue()コンストラクタに渡すオブジェクトのwatchプロパティに監視するデータを与え、handlerに処理するメソッドを定めます。引数に受け取るのは変更が加えられたデータです。

アプリケーションは、つぎのように項目データ(todos)をwatchに加え、ローカルデータの保存のメソッドを呼び出すようにしましょう。deepオプションをtrueにすると、入れ子のオブジェクトまで含めて監視します(「vm.$watch()」参照)。項目追加(addTodo())と削除(removeTodo())のメソッドから保存はしなくてよくなりましたので呼び出しのコードを除きます。

script.js

const app = new Vue({

	watch: {
		todos: {
			handler(todos) {
				todoStorage.save(todos);
			},
			deep: true
		}
	},

	methods: {
		addTodo() {

			//  todoStorage.save(this.todos);
		},
		removeTodo(todo) {

			//  todoStorage.save(this.todos);
		},

	}
});

03 フィルタを使う

気になる細かいところを、もうひとつ直しましょう。フッターにはチェックのついていない残り項目数を左端に示しました(「Vue.js + ES6: TodoMVCをつくる 01」05「残り項目を数える」参照)。算出プロパティの数は動的に変わるものの、テキストは決め打ちです。そのため、項目が残りひとつでも「items」と複数形のままになってしまいます(図003)。単数か複数かによってテキストを変えるために、フィルタを使いましょう。

図003■残り項目がひとつでも複数形

図003

フィルタで動的に変えるテキストは二重波かっこ{{}}でくくり、パイプ|の左右にフィルタのキーとフィルタメソッドを並べます。今回、単数と複数を分けるフィルタのキーは、算出プロパティ(remaining)の値です。フィルタの結果を返すメソッド(pluralize())は、アプリケーションに定めます。

<body>要素

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

フィルタ関数は、つぎのようにVue()コンストラクタの引数オブジェクトにfiltersとして加えます。関数はフィルタのキーの値を引数に受け取り、戻り値は表示したい結果です。算出プロパティから引数として渡された値が1なら単数形、そうでなければ複数形の単語を返します。

script.js

const app = new Vue({

	filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	},

	}
});

これでデータの扱いはローカルになり、テキストの表示がフィルタで変わるようになりました。書き上げた<body>要素とJavaScriptファイルの中身を、つぎのコード001にまとめます。併せて、以下のサンプル001をCodePenに掲げました。

コード001■データはローカルで扱いフィルタを加えたTodoリスト

<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>
	</footer>
</section>

script.js

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 app = new Vue({
	data: {
		todos: todoStorage.fetch(),
		newTodo: ''
	},
	watch: {
		todos: {
			handler(todos) {
				todoStorage.save(todos);
			},
			deep: true
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		},
		remaining() {
			const todos = this.getActive(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);
		},
		getActive(todos) {
			return todos.filter((todo) =>
				!todo.completed
			);
		}
	}
});
app.$mount('.todoapp');

サンプル001■Vue.js + ES6: Vue TodoMVC 02

See the Pen Vue.js + ES6: Vue TodoMVC 02 by Fumio Nonaka (@FumioNonaka) on CodePen.


作成者: 野中文雄
更新日: 2019年11月14日 構文をECMAScript 2015に改め、本文も修正。サンプルはCodePenに差し替えた。
作成日: 2017年06月28日


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