サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6入門 12: ローカルコンポーネントを定める


Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける」では、Vue.component()メソッドでコンポーネントをつくって、アプリケーションに組み込みましました。今回は、親コンポーネントを特定して加えるローカルコンポーネントの定め方についてご説明します。

01 グローバルコンポーネントとローカルコンポーネント

Vue.js + ES6入門 11」ででき上がったコード001に手を加えることにしましょう。アプリケーションのコンポーネントは、Vue.component()メソッドでつくられています。第1引数がコンポーネント名の文字列、第2引数はコンポーネントのオプションオブジェクトです。Vue()コンストラクタでインスタンスをつくれば、アプリケーションにコンポーネントが組み込まれます。親からのデータはv-bind(省略記法@)で渡し、子が受け取るのはpropsオプションです。子が$emit()メソッドで送るイベントは、親はv-on(省略記法@)によりリスナーメソッドを呼び出します。

<script>要素

Vue.component('list-header', {
	props: {
		todos: {
			type: Array,
			required: true
		},

	},
	methods: {
		archive() {
			this.$emit('archive');
		}
	},

});

const app = new Vue({
	data: {
		todos: [

		]

	},

	methods: {

		archive() {

		},

	}
});

<body>要素

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

	<list-header
		:todos="todos"

		@archive="archive"
	>
	</list-header>

</div>

アプリケーションのVueインスタンスにコンポーネントをグローバルに組み込むのが、Vue.component()メソッドです。アプリケーションからただちに使えるものの、コンポーネントが増えて、とくに階層化されてくると、問題が起こっても原因を探しにくくなります。それに対して、親を決めてスコープを絞れるのが、ローカルコンポーネントです。

ローカルコンポーネントをつくるには、Vue.extend()メソッドを用います。引数はコンポーネントに定めるオプションオブジェクトで、戻り値がVueを継承したインスタンス(listHeader)です。まだ、誰の子でもありません。親にするコンポーネントのcomponentsオプションに加えてはじめて子コンポーネントになります。オプションには、オブジェクトでコンポーネント名('list-header')とインスタンスを与えてください。componentsに定めのない他のコンポーネントからは、この子コンポーネントは参照できません。親子間のデータの受け渡しはVue.component()メソッドでつくったコンポーネントと同じです(書き替えは要りません)。

<script>要素

// Vue.component('list-header', {
const listHeader = Vue.extend({

});

const app = new Vue({
	components: {
		'list-header': listHeader,
	},

}

02 コンポーネントを入れ子にする

リスト表示のコンポーネントtodo-listは、リスト項目の子コンポーネントtodo-itemをもちます。まず、リスト表示のコンポーネントのアプリケーションへの加え方は、前項01と変わりません。

<script>要素

// Vue.component('todo-list', {
const todoList = Vue.extend({

});

const app = new Vue({
	components: {

		'todo-list': todoList,
	},

});

つぎに、リスト項目のコンポーネント(todoItem)は、やはりVue.extend()メソッドでつくります。ただし、組み込む先はリスト表示コンポーネントtodoListです。したがって、親のcomponentsオプションに加えなければなりません。ローカルコンポーネントにするとどうしても増えてしまうのが、親を決めるというひと手間です。そのかわり、子コンポーネントの働く範囲が絞れます。問題が起きたとき調べる範囲が狭まり、修正や管理もしやすくなるのです。親子の組み合わせを細かく決めることは、単一ファイルコンポーネントによる開発の考え方にも通じます。単一ファイルコンポーネントを使ったアプリケーションについては、「Vue.js + CLI入門」01-08をお読みください。

<script>要素

// Vue.component('todo-item', {
const todoItem = Vue.extend({

});
const todoList = Vue.extend({
	components: {
		'todo-item': todoItem
	},

	template: `
		<ul class="list-unstyled">
			<li v-for="todo in filteredTodos" :key="todo.id">
				<todo-item :todo="todo" @remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	`
});

03 すべてローカルコンポーネントに差し替える

コンポーネントはあとふたつ残っています。けれど、入れ子でもありませんし、つぎのコード001にすべてローカルコンポーネントに書き替えたJavaScriptの記述全体をまとめて示しましょう。

コード001■ローカルコンポーネントに書き替えたアプリケーション

<script>要素

const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.done
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.done
		);
	}
};
const listHeader = Vue.extend({
	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>
	`
});
const todoItem = Vue.extend({
	props: {
		todo: {
			type: Object,
			required: true,
			validator(value) {
				return (
					value.hasOwnProperty('text') &&
					value.hasOwnProperty('done') &&
					value.hasOwnProperty('id')
				);
			}
		}
	},
	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>
	`
});
const todoList = Vue.extend({
	components: {
		'todo-item': todoItem
	},
	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" :key="todo.id">
				<todo-item :todo="todo" @remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	`
});
const addTodo = Vue.extend({
	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>
	`
});
const listFooter = Vue.extend({
	props: {
		visibility: {
			type: String,
			required: true,
			validator(value) {
				return (
					['all', 'active', 'completed'].includes(value)
				);
			}
		},
		filters: {
			type: Object,
			required: true,
			validator(value) {
				return (
					value.hasOwnProperty('all') &&
					value.hasOwnProperty('active') &&
					value.hasOwnProperty('completed')
				);
			}
		}
	},
	methods: {
		changeFilter(key) {
			this.$emit('change-filter', key);
		}
	},
	template: `
	<div>
		<button type="button"
			v-for="(value, key) in filters"
			:key="key"
			: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({
	components: {
		'list-header': listHeader,
		'todo-list': todoList,
		'add-todo': addTodo,
		'list-footer': listFooter
	},
	data: {
		todos: [
			{text: 'Vue.jsを学ぶ', done: true},
			{text: 'Vue.jsでアプリケーションをつくる', done: false},
		]
		.map((todo, index) => ({...todo, id: index})),
		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);
		},
		nextId() {
			if (!this.todos.length) {return 0;}
			const ids = this.todos.map((todo) => todo.id);
			return Math.max(...ids) + 1;
		}
	},
	methods: {
		addTodo(newTodo) {
			this.todos = [
				...this.todos,
				{text: newTodo, done: false, id: this.nextId}
			];
			console.log(this.todos);
		},
		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: Using local components

See the Pen Vue.js + ES6: Using local components by Fumio Nonaka (@FumioNonaka) on CodePen.

Vue.js + ES6


作成者: 野中文雄
作成日: 2019年11月17日


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