サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック


Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける」でつくったTodoリストのアプリケーション(サンプル001)から、さらにコンポーネントを分けて入れ子にします。そして、データのチェックも加えて、つくりをしっかりしましょう。

01 項目入力部分をコンポーネントに切り分ける

先に、入力した項目の追加部分をコンポーネント(add-todo)に切り分けます(図001)。

図001■項目入力の部分をコンポーネントに切り分ける

図001

ここでひとつ考えることは、v-modelディレクティブで双方向バインディングしているデータ(todoText)の扱いです。コンポーネント(add-todo)内でフィールドに入力しているテキストそのものは、アプリケーションが気にしなくて構いません。追加ボタンが押されたとき(clickイベント)、そのテキストを受け取れば済むからです。アプリケーションのハンドラメソッド(addTodo())には、そのために引数(newTodo)を新たに加えます。

<body>要素

<div id="app" class="container">

	<!-- <p>
		<input type="text" v-model="todoText" placeholder="add new todo here">
		<button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
	</p> -->
	<add-todo @add-todo="addTodo"></add-todo>

</div>

<script>要素

const app = new Vue({
	data: {
		// todoText: '',

	},

	methods: {
		// addTodo() {
		addTodo(newTodo) {
			// const newTodo = this.todoText.trim();
			// this.todoText = '';
			// if (!newTodo) {return;}
			this.todos = [
				...this.todos,
				{text: newTodo, done: false}
			];
		},

	}
});

そうすると、データ(dataオプション)は、子コンポーネントの側に持たせます。そのとき「dataは関数でなければなりません」。オブジェクトにしてしまうと、コンポーネントを同時に使いまわしたとき、参照が同じになってしまうからです。関数(メソッド)にすることで、コンポーネントごとに異なる戻り値のオブジェクトが与えられます。項目入力のコンポーネントのJavaScriptコードはつぎのとおりです。

コード001■項目入力のコンポーネント

<script>要素

Vue.component('add-todo', {
	data() {
		return {
			todoText: ''
		};
	},
	methods: {
		addTodo() {
			const newTodo = this.todoText.trim();
			this.todoText = '';
			if (!newTodo) {return;}
			this.$emit('add-todo', newTodo);
		}
	},
	template: `
		<p>
			<input type="text" v-model="todoText" placeholder="add new todo here">
			<button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
		</p>
	`
});

02 リスト表示のコンポーネントを切り分ける

つぎに切り分けるのが、リスト表示のコンポーネントです(図003)。項目のコンポーネントが含まれるので、入れ子ということになります。

図002■リスト表示部分をコンポーネントに切り分ける

図002

親アプリケーションのテンプレートを、つぎのようにリスト表示のコンポーネント(todo-list)に差し替えます。子が参照する算出プロパティ(filteredTodos)はv-bind(:)でバインドしてください。

<body>要素

<div id="app" class="container">

	<!-- <ul class="list-unstyled">
		<li v-for="todo in filteredTodos">
			<todo-item :todo="todo" @remove-todo="removeTodo">
			</todo-item>
		</li>
	</ul> -->
	<todo-list :filtered-todos="filteredTodos" @remove-todo="removeTodo">
	</todo-list>

</div>

リスト表示コンポーネントのJavaScriptコードはつぎのとおりです(コード002)。項目データのオブジェクト(todo)は入れ子コンポーネントにv-bind(:)で渡し、v-on(@)で項目削除のイベント(remove-todo)を受け取ったら、ハンドラメソッド(removeTodo)から親アプリケーションにバケツリレーのように送ります。vm.$emit()メソッドに渡す第2引数(todo)は削除する項目のオブジェクトです。

コード002■リスト表示のコンポーネント

<script>要素

Vue.component('todo-list', {
	props: ['filteredTodos'],
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		}
	},
	template: `
		<ul class="list-unstyled">
			<li v-for="todo in filteredTodos">
				<todo-item :todo="todo" @remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	`
});

03 開発バージョンのVue.jsライブラリを使う

ここで、開発バージョンのVue.jsライブラリの機能を使います。Vue.jsサイトからダウンロードして、<script>要素に読み込んでください。

<head>要素

<!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
<script src="./lib/vue.js"></script>

ここまで書き上げたアプリケーションを試すと、早速つぎのような警告が出されるでしょう。Vueインスタンス(app)の外の以下のオブジェクト(filters)を、コンポーネントが直に参照しているからです。


[Vue warn]: Property or method "filters" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.

<script>要素

const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.done
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.done
		);
	}
};

Vue.component('list-footer', {

	template: `
	<div>
		<button type="button"
			v-for="(value, key) in filters"

			>

		</button>
	</div>
	`
});
const app = new Vue({

	computed: {

		filteredTodos() {
			return filters[this.visibility](this.todos);
		}
	},

});

データを管理するために、参照するオブジェクトやメソッドはVueインスタンス(app)のdataオプションに定めておかなければならないのです(「リアクティブプロパティの宣言」参照)。

<script>要素

const app = new Vue({
	data: {

		filters: filters
	},

}

もちろん、子コンポーネント(list-footer)のプロパティ(filters)にバインドして渡すことも忘れないでください。

<body>要素

<div id="app" class="container">

	<list-footer

		:filters="filters"

	>
	</list-footer>
</div>

<script>要素

Vue.component('list-footer', {
	props: ['visibility', 'filters'],

});

04 プロパティの型を定める

コンポーネントのpropsオプションは、配列にプロパティ名を加える以外に、オブジェクトでもっと細かく定められます。このオブジェクトによる定義が公式スタイルガイドの推奨です(「プロパティの定義」参照)。以下のように、オブジェクトにプロパティ名とその型を与えます。すると、開発版のライブラリであれば、型が違うとコンソールに警告を示してくれるのです。


[Vue warn]: Invalid prop: type check failed for prop "todo". Expected String with value "[object Object]", got Object

found in

---> <TodoItem>
	 <TodoList>
	   <Root>
<script>要素

Vue.component('todo-item', {
	// props: ['todo'],
	props: {
		todo: String
	},

});

もっとも、デフォルトではプロパティ(notExist)がなくても放っておかれます。

<script>要素

Vue.component('todo-item', {
	props: {
		todo: Object,
		notExist: String  // 存在しない
	},

});

プロパティの有無まで確かめたいときは、さらに値をオブジェクトにして、requiredオプションに定められます(デフォルト値false)。データ型はtypeオプションです。


[Vue warn]: Missing required prop: "notExist"

<script>要素

Vue.component('todo-item', {
	props: {

		notExist: {
			type: String,
			required: true
		}
	},
	`
});

他のコンポーネントも含めて、propsオプションはオブジェクトでつぎのように定めればよいでしょう。

<script>要素

Vue.component('list-header', {
	// props: ['todos', 'remaining'],
	props: {
		todos: {
			type: Array,
			required: true
		},
		remaining: {
			type: Number,
			required: true
		}
	},

});
Vue.component('todo-item', {
	props: {
		todo: {
			type: Object,
			required: true
		}
	},

});
Vue.component('todo-list', {
	// props: ['filteredTodos'],
	props: {
		filteredTodos: {
			type: Array,
			required: true
		}
	},

});
Vue.component('list-footer', {
	// props: ['visibility', 'filters'],
	props: {
		visibility: {
			type: String,
			required: true
		},
		filters: {
			type: Object,
			required: true
		}
	},

});

これで、ふたつのコンポーネントが切り分けられ、プロパティのデータチェックも加わりました。HTMLの記述とJavaScriptコードは、それぞれつぎにまとめたコード003のとおりです。併せて、サンプル001をCodePenに掲げます。

コード003■コンポーネントの切り分けとデータチェックを加えたHTMLとJavaScriptの記述全体

<body>要素

<div id="app" class="container">
	<h2>Todo</h2>
	<list-header
		:todos="todos"
		:remaining="remaining"
		@archive="archive"
	>
	</list-header>
	<todo-list :filtered-todos="filteredTodos" @remove-todo="removeTodo">
	</todo-list>
	<add-todo @add-todo="addTodo"></add-todo>
	<list-footer
		:visibility="visibility"
		:filters="filters"
		@change-filter="changeFilter"
	>
	</list-footer>
</div>

<script>要素

const filters = {const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.done
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.done
		);
	}
};
Vue.component('list-header', {
	props: {
		todos: {
			type: Array,
			 required : true
		},
		remaining: {
			type: Number,
			 required : true
		}
	},
	methods: {
		archive() {
			this.$emit('archive');
		}
	},
	template: `
		<p>
			全{{todos.length}}件中残り{{remaining}}件
			<button class="btn btn-danger btn-sm"  @click="archive">断捨離</button>
		</p>
	`
});
Vue.component('todo-item', {
	props: {
		todo: {
			type: Object,
			 required : true
		}
	},
	methods: {
		removeTodo(todo) {
  			this.$emit('remove-todo', todo);
		}
	},
	template: `
		<div>
			<label>
				<input type="checkbox" v-model="todo.done">
				<span :class="{'done': todo.done}">{{todo.text}}</span>
			</label>
			<button class="btn btn-warning btn-sm" @click="removeTodo(todo)">削除</button>
		</div>
	`
});
Vue.component('todo-list', {
	props: {
		filteredTodos: {
			type: Array,
			 required : true
		}
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		}
	},
	template: `
		<ul class="list-unstyled">
			<li v-for="todo in filteredTodos">
				<todo-item :todo="todo" @remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	`
});
Vue.component('add-todo', {
	data() {
		return {
			todoText: ''
		};
	},
	methods: {
		addTodo() {
			const newTodo = this.todoText.trim();
			this.todoText = '';
			if (!newTodo) {return;}
			this.$emit('add-todo', newTodo);
		}
	},
	template: `
		<p>
			<input type="text" v-model="todoText" placeholder="add new todo here">
			<button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
		</p>
	`
});
Vue.component('list-footer', {
	props: {
		visibility: {
			type: String,
			 required : true
		},
		filters: {
			type: Object,
			 required : true
		}
	},
	methods: {
		changeFilter(key) {
			this.$emit('change-filter', key);
		}
	},
	template: `
	<div>
		<button type="button"
			v-for="(value, key) in filters"
			:class="['btn btn-outline-info btn-sm mr-1', {active: visibility === key}]"
			@click="changeFilter(key)">
			{{ key[0].toUpperCase() + key.substr(1) }}
		</button>
	</div>
	`
});
const app = new Vue({
	data: {
		todos: [
			{text: 'Vue.jsを学ぶ', done: true},
			{text: 'Vue.jsでアプリケーションをつくる', done: false},
		],
		visibility: 'all',
		filters: filters
	},
	computed: {
		remaining() {
			const count =
				this.todos.reduce((count, todo) =>
					count = (todo.done) ? count : ++count
				, 0);
			return count;
		},
		filteredTodos() {
			return filters[this.visibility](this.todos);
		}
	},
	methods: {
		addTodo(newTodo) {
			this.todos = [
				...this.todos,
				{text: newTodo, done: false}
			];
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((_todo) => _todo !== todo);
		},
		archive() {
			this.todos = this.todos.filter((todo) => !todo.done);
		},
		changeFilter(visibility) {
			this.visibility = visibility;
		}
	}
});
document.addEventListener('DOMContentLoaded', () =>
	app.$mount('#app')
);

サンプル001■Vue.js + ES6: Separating components and validating props

See the Pen Vue.js + ES6: Separating components and validating props by Fumio Nonaka (@FumioNonaka) on CodePen.

Vue.js + ES6


作成者: 野中文雄
更新日: 2019年8月26日 本文説明とスクリプトの加筆・補正。
作成日: 2019年7月19日


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