サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 03: データの計算処理とローカルへの保存


Vue.js公式サイトの「TodoMVC の例」を単一ファイルコンポーネント(.vue)でつくるシリーズの第3回は、データをローカルから読み込んで、追加や変更されたら保存します。また、未処理の項目数をフッタに示すようにします。

01 未処理の項目数を示す

未処理の項目数を示すフッタから先に加えましょう。新たにフッタとしてつくるコンポーネントのファイルsrc/components/TodoController.vueに定める中身はつぎのとおりです。テンプレートの要素(<span>)にバインディングして表示する未処理の項目数(remaining)は、親のアプリケーションからもらう予定です。そのため、propsに定めておかなければなりません。なお、項目リストのコンポーネントと同じく、項目がないときは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',
	props: {
		todos: Array,
		remaining: Number
	}
}
</script>

親のアプリケーション(src/App.vue)は、未処理の項目数を算出プロパティ(remaining())で調べます。実際に未処理の項目を取り出しているのは、そこから呼び出すメソッド(getActive())です。Array.prototype.filter()メソッドで処理済み(completed)でない項目を新たな配列にして返しています。その長さを調べて、算出プロパティの戻り値とすればよいでしょう。そして、算出プロパティをフッタの要素(<todo-controller>)にv-on(省略記法:)でバインディングします。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<todo-controller
			:todos="todos"
			:remaining="remaining">
		</todo-controller>
	</section>
</template>

<script>

import TodoController from './components/TodoController.vue';

export default {

	components: {

		TodoController
	},

	computed: {

		remaining() {
			const todos = this.getActive(this.todos);
			return todos.length;
		}
	},
	methods: {

		getActive(todos) {
			return todos.filter((todo) =>
				!todo.completed
			);
		}
	}
}
</script>

これで未処理、つまりチェックされていないリスト項目の数が、フッタに示されるようになります(図001)。チェックをつけたり外したりすると、算出プロパティが項目を数え直すので、バインディングされたフッタの値も変わるはずです。

図001■チェックされていない項目数がフッタに示される

図001

02 フィルタを使う

未処理の項目数は英語で示しました。ということは、名詞は単数と複数でかたちが変わります。ところが、今は複数形(items)の決め打ちです(前掲図001)。標準JavaScriptコードで、条件に応じて文字を切り替えるという手はあります。でも、ここではVueのフィルタを試してみましょう。

フィルタは、まずテンプレートの二重波かっこ{{{}}に、パイプを添えてつぎのように定めます(「フィルター」参照)。フィルタ対象がVueインスタンスに定めるフィルタメソッドの引数に渡され、戻り値が表示される仕組みです。

{{ フィルタ対象 | フィルタメソッド }}

フィルタメソッド(pluralize())を加えるのは、Vueインスタンスのfiltersです。メソッドをつぎのように定めれば、項目がひとつのときは単数形の単語(item)が示されます(図002)。なお、項目がないときは、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 -->
			<strong>{{remaining}}</strong> {{remaining | pluralize}} left
		</span>
	</footer>
</template>

<script>
export default {

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

}
</script>

図002■項目がひとつのときは単数形の単語が示される

図002

ここまでのアプリケーション(src/App.vue)とフッタ(src/components/TodoController.vue)のコンポーネント(VUE)ファイルの中身を、つぎのコード001にまとめました。

コード001■未処理の項目数を示す

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input
				@add-todo="addTodo" />
		</header>
		<todo-list
			:todos="todos"
			:filtered-todos="filteredTodos"
			@remove-todo="removeTodo"
			@done="done">
		</todo-list>
		<todo-controller
			:todos="todos"
			:remaining="remaining">
		</todo-controller>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';

export default {
	name: 'app',
	components: {
		TodoInput,
		TodoList,
		TodoController
	},
	data() {
		return {
			todos: [],
			uid: 0
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		},
		remaining() {
			const todos = this.getActive(this.todos);
			return todos.length;
		}
	},
	methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todos.push({
				id: this.uid++,
				title: newTodo,
				completed: false
			});
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		},
		done(todo, completed) {
			todo.completed = completed;
		},
		getActive(todos) {
			return todos.filter((todo) =>
				!todo.completed
			);
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

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';
		}
	},
	props: {
		todos: Array,
		remaining: Number
	}
}
</script>

03 データをローカルから読み込んで保存する

つぎに、リスト項目のデータはローカルにもつことにしましょう。用いるのはWeb Storage APIです。Window.localStorageに保存すれば、ブラウザを閉じてもデータが残り、つぎに開いたときに表示されます。

データの書き込みにはメソッドStorage.setItem()、読み込むときはStorage.getItem()を使います。データの形式はJSONにしますので、保存の前にJSON.stringify()で文字列にし、取り出したらJSON.parse()によりJSONデータに戻さなければなりません。リスト項目のデータを読み書きするメソッドは、アプリケーション(src/App.vue)につぎのようにVueインスタンスとは別のオブジェクト(todoStorage)に定めます。

src/App.vue

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

アプリケーション(src/App.vue)のVueインスタンスの定めも、新たなオブジェクトに加えた読み書きのメソッドに合わせて書き替えなければなりません。dataにもたせるリスト項目のデータ(todos)は、ローカルから読み込みます。methodsについては、項目の追加(addTodo())と削除(removeTodo())のメソッドです。いずれも、データを書き替えたら、最後に必ずローカルに保存します。

src/App.vue

export default {

	data() {
		return {
			todos: todoStorage.fetch()  // [],
			// uid: 0
		}
	},

	methods: {
		addTodo(todoTitle) {

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

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

			todoStorage.save(this.todos);
		},

	}
}

これで、追加したり削除した項目のリストはローカルに保存されます。ブラウザを一旦閉じても、改めて開くと、保存された項目が表示されるはずです。

04 データが変更されたらローカルに保存する

ローカルへの保存処理には、まだ足りないところがあります。ローカルに保存されるのは、項目を追加したときと削除したときだけです。チェックをつけ替えて、そのままブラウザを閉じた場合、再び開くと閉じる直前の項目は残っていても、チェックは直近に追加か削除したときに戻ってしまいます。

ひとつのやり方は、チェックをつけ替えたときに呼ばれるアプリケーションのメソッド(done())で保存することです。メソッドごとにローカルに保存すべきかどうか考えた方がよい場合には適しています。今回のTodoリストでは、項目データが変更されたら、メソッドを問わず保存してしまって差し支えないでしょう。Vueインスタンスのオプションでwatchを使えば、データの変更が監視できます(「ウォッチャ」参照)。

watchには、つぎのように監視するデータを与え、値のオブジェクトにhandler()メソッドで処理を定めます。引数は変更されたデータです。なお、deep: trueを加えると、データの中のネストされた深い階層の値の変更も検出されます。これで、項目データが変更されたらローカルに保存されるようになりました。したがって、追加と削除のメソッドは、ローカルに保存しなくて構いません。

src/App.vue

export default {

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

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

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

	}
}

リスト項目の追加と削除だけでなく、チェックも含めて、データの変更があればローカルに保存されるようになりました。けれど、まだもうひとつ問題が残っています。チェックした項目のチェックが、ページをリロードすると消えてしまうことです。というのは、チェックボックスの値を、データバインディングしていませんでした。つまり、項目のデータにかかわらず、リロードすればオフに戻ってしまうのです。

図003■リロードするとチェックが消える

図003

チェックボックスのオン/オフをブール(論理)値でもつのは、<input type="checkbox">要素のcheckedプロパティです。これをつぎのようにデータ(todo)のプロパティ(completed)とv-on(省略記法:)でバインディングします。これで、リロードしてもデータに応じてチェックがつくはずです。

src/components/TodoItem.vue

<template>
	<div class="view">
		<input
			type="checkbox" class="toggle"

			:checked="todo.completed"

			>

</template>

書き改めたアプリケーションのsrc/App.vueの中身は、つぎのコード002のとおりです。コンポーネントsrc/components/TodoItem.vueは、1行の追加だけですので再掲は省きます。それぞれのコンポーネントのコードについては、CodeSandboxに公開した以下のサンプル001をご参照ください。

コード002■項目データに変更が加わるとローカルに保存される

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input
				@add-todo="addTodo" />
		</header>
		<todo-list
			:todos="todos"
			:filtered-todos="filteredTodos"
			@remove-todo="removeTodo"
			@done="done">
		</todo-list>
		<todo-controller
			:todos="todos"
			:remaining="remaining">
		</todo-controller>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';

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 {
	name: 'app',
	components: {
		TodoInput,
		TodoList,
		TodoController
	},
	data() {
		return {
			todos: todoStorage.fetch()
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		},
		remaining() {
			const todos = this.getActive(this.todos);
			return todos.length;
		}
	},
	watch: {
		todos: {
			handler(todos) {
				todoStorage.save(todos);
			},
			deep: true
		}
	},
	methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todos.push({
				id: todoStorage.uid++,
				title: newTodo,
				completed: false
			});
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		},
		done(todo, completed) {
			todo.completed = completed;
		},
		getActive(todos) {
			return todos.filter((todo) =>
				!todo.completed
			);
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

サンプル001■vue-todo-mvc-03

Vue.js + CLI入門


作成者: 野中文雄
更新日: 2019年10月14日 src/App.vueのコードを一部修正。
更新日; 2019年09月05日 サンプル001を追加。
更新日: 2019年9月5日 タイトルを「データをローカルに保存する」から原題に変更。
更新日: 2019年3月23日 ブラウザの違いによる問題に対応。
作成日: 2018年12月23日


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