サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6: アプリケーションにコンポーネントを使う


コンポーネントは、Vue.jsのコードとHTML要素を使い回しができる部品に切り分けたものです。「Vue.js入門 07: データを項目ごとに削除する」でつくったTodoリストのアプリケーション(サンプル001)から、ふたつの部分をコンポーネント化してみましょう。

サンプル001■Vue.js: Todo List with Adding or Removing Items

01 <script>要素を<head>の中に移す

「Vue.js入門 07」のコード001では、<script>要素は以下のように<body>の終わりに加えてありました。VueアプリケーションはHTML要素の参照を得て定められるため、<body>要素が先に読み込まれていなければならないからです(図001)。

図001■HTML要素が先に読み込まれていないとアプリケーションは動かない

図001
<body>要素

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

</div>
<script>
var app = new Vue({
	el: '#app',

});
</script>

アプリケーションは<head>要素にまとめた方が管理しやすいでしょう。vm.$mount()メソッドを使うと、アプリケーションにelプロパティを加えることなく、あとからHTML要素に定められます。引数に渡すのは要素の参照またはセレクタです。つぎのように、DOMContentLoadedイベントのリスナー関数で呼び出せば、HTMLドキュメントを読み込み終わってから要素にアプリケーションが定められます。これで、JavaScriptコードは<head>要素に移しても大丈夫です。


var app = new Vue({
	// el: '#app',

)};
document.addEventListener('DOMContentLoaded', function(event) {
	app.$mount('#app');
});

02 ヘッダをコンポーネントにする

コンポーネントはVue.extend()メソッドでつくります。引数に渡すのは、コンポーネントオプションをプロパティに定めたオブジェクトです。コンポーネントのHTML記述はtemplateプロパティに文字列で与えます。バックティック``テンプレート文字列にすれば、HTMLと同じように半角スペースやタブ、改行などのホワイトスペースが含められます。アプリケーションのVue()コンストラクタには、引数のオプションオブジェクトにcomponentsプロパティでコンポーネントのセレクタと参照を定めます。


var コンポーネント = Vue.extend({
	コンポーネントオプション
});
new Vue({
	components: {
		コンポーネントセレクタ: コンポーネント
	},
});

コンポーネントをつくったら、考えなければならないのはデータのやり取りです。コンポーネントが使い回せるためには、できるだけ他と切り離されていなければなりません。データの受け渡しは、決められたやり方にしたがう必要があるのです。親から子のコンポーネントへはデータをプロパティで渡し、子から親へはイベントにより伝えることになります。

親から受け取るプロパティは、子コンポーネントのVue.extend()メソッドの引数オブジェクトに、propsプロパティで配列に文字列要素として加えます。 親は子コンポーネントのテンプレートにv-bindディレクティブでプロパティをバインドして、データを渡せばよいのです(「コンポーネント」の「プロパティ」参照)。


// 子コンポーネント
Vue.extend({
	props: [プロパティ],
});

// 親アプリケーション
<コンポーネントセレクタ v-bind:プロパティ=親のデータ></コンポーネントセレクタ>

子コンポーネントから親へは、vm.$emit()メソッドで引数の文字列のイベントを送ります。親は子コンポーネントのテンプレートに加えたv-on:ディレクティブでイベントを受け、定めたメソッドが呼び出されるというかたちです(「カスタムイベントとの v-on の使用」)。


// 子コンポーネント
this.$emit(イベント);

// 親アプリケーション
<コンポーネントセレクタ v-on:イベント="メソッド"></コンポーネントセレクタ>

ヘッダをコンポーネントに切り分けるコードは、つぎに抜き書きしたとおりです。Todoリストアプリケーションとしての動きは、もとの「Vue.js入門 07」のコード001と変わりません(図002)。


// component: listHeader
var listHeader = Vue.extend({
	props: ['todos', 'remaining'],
	template: `
		<p>
			全{{todos.length}}件中残り{{remaining}}件
			<button  v-on:click="archive" class="btn btn-danger btn-xs">断捨離</button>
		</p>
		`,
	methods: {
		archive: function() {
  			this.$emit('archive');
		}
	}
});
// main application
var app = new Vue({
	components: {
		'list-header': listHeader
	},
	data: {

		todos: [

		]
	},
	methods: {

		archive: function() {

		}
	},
	computed: {
		remaining: function() {

		}
	}
});

<body>要素

<div id="app" class="container">
	<h2>Todo</h2>
	<!--<p>
		全{{todos.length}}件中残り{{remaining}}件
		<button  v-on:click="archive()" class="btn btn-danger btn-xs">断捨離</button>
	</p>-->
	<list-header v-bind:todos="todos" v-bind:remaining="remaining" v-on:archive="archive"></list-header>

</div>

図002■Todoリストのアプリケーションとしての動きは変わらない

図002

03 リスト項目をコンポーネントにする

つぎに、リスト表示されるTodo項目をコンポーネントにします。考え方は前項のヘッダと同じです。vm.$emit()メソッドから親にイベントとともに送るデータがあるときは、つぎのコードの抜き書きのように第2引数(todo)として渡せます。すると、親から呼び出すメソッド(removeTodo())が引数に受け取れるのです。


// component: todoItem
var todoItem = Vue.extend({
	props: ['todo'],
	template: `
		<div>
			<label>
				<input type="checkbox" v-model="todo.done" />
				<span v-bind:class="{'done': todo.done}" >{{todo.text}}</span>
			</label>
			<button v-on:click="removeTodo(todo)" class="btn btn-warning btn-xs">削除</button>
		</div>
		`,
	methods: {
		removeTodo: function(todo) {
  			this.$emit('remove-todo', todo);
		}
	}
});

// main application
var app = new Vue({
	components: {
		'todo-item': todoItem,

	},
	data: {

		todos: [

		]
	},
	methods: {

		removeTodo: function(todo) {

		},

	},

});

<body>要素

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

	<ul class="list-unstyled">
		<li v-for="todo in todos">
			<!--<label>
				<input type="checkbox" v-model="todo.done" />
				<span v-bind:class="{'done': todo.done}">{{todo.text}}</span>
			</label>
			<button v-on:click="removeTodo(todo)" class="btn btn-warning btn-xs">削除</button>-->
			<todo-item v-bind:todo="todo" v-on:remove-todo="removeTodo">
			</todo-item>
		</li>
	</ul>

</div>

アプリケーションからヘッダ(listHeader)とリスト項目(todoItem)をコンポーネントに切り出す書き替えは、つぎのコード001にまとめました。アプリケーションの<body>要素は、コンポーネントを分けた分すっきりしました。子コンポーネントのセレクタにディレクティブv-bindv-onを加えてデータのやり取りをします。子は、コンポーネントオプションのpropsvm.$emit()メソッドにより受け渡しをするわけです。以下のサンプル002をjsdo.itに掲げました。

コード001■ヘッダとリスト項目をコンポーネントに切り分ける

<body>要素

<div id="app" class="container">
	<h2>Todo</h2>
	<list-header v-bind:todos="todos" v-bind:remaining="remaining" v-on:archive="archive"></list-header>
	<ul class="list-unstyled">
		<li v-for="todo in todos">
			<todo-item v-bind:todo="todo" v-on:remove-todo="removeTodo">
			</todo-item>
		</li>
	</ul>
	<p>
		<input type="text" v-model="todoText" placeholder="add new todo here" />
		<button v-on:click="addTodo()" class="btn btn-primary btn-sm">追加</button>
	</p>
</div>

<script>要素

// component: todoItem
var todoItem = Vue.extend({
	props: ['todo'],
	template: `
		<div>
			<label>
				<input type="checkbox" v-model="todo.done" />
				<span v-bind:class="{'done': todo.done}" >{{todo.text}}</span>
			</label>
			<button v-on:click="removeTodo(todo)" class="btn btn-warning btn-xs">削除</button>
		</div>
		`,
	methods: {
		removeTodo: function(todo) {
  			this.$emit('remove-todo', todo);
		}
	}
});
// component: listHeader
var listHeader = Vue.extend({
	props: ['todos', 'remaining'],
	template: `
		<p>
			全{{todos.length}}件中残り{{remaining}}件
			<button  v-on:click="archive" class="btn btn-danger btn-xs">断捨離</button>
		</p>
		`,
	methods: {
		archive: function() {
  			this.$emit('archive');
		}
	}
});
// main application
var app = new Vue({
	components: {
		'todo-item': todoItem,
		'list-header': listHeader
	},
	data: {
		todoText: '',
		todos: [
			{text: 'Vue.jsを学ぶ', done: true},
			{text: 'Vue.jsでアプリケーションをつくる', done: false},
		]
	},
	methods: {
		addTodo: function() {
			var newTodo = this.todoText.trim();
			if (!newTodo) {return;}
			this.todos.push(
				{text: newTodo, done: false}
			);
			this.todoText = '';
		},
		removeTodo: function(todo) {
			var todos = this.todos;
			var index = todos.indexOf(todo);
			todos.splice(index, 1);
		},
		archive: function() {
			var remains = [];
			var todos = this.todos;
			var length = todos.length;
			for(var i = 0; i < length; i++) {
				var todo = todos[i];
				if(!todo.done) {
					remains.push(todo);
				}
			}
			this.todos = remains;
		}
	},
	computed: {
		remaining: function() {
			var count = 0;
			var todos = this.todos;
			var length = todos.length;
			for(var i = 0; i < length; i++) {
				if(!todos[i].done) {
					count++;
				}
			}
			return count;
		}
	}
});
document.addEventListener('DOMContentLoaded', function(event) {
	app.$mount('#app');
});

サンプル002■Vue.js: Todo List with Components

04 ECMAScript 6の構文を使う

この機会に、JavaScriptコードの構文もECMAScript 6に書き替えましょう。変数の宣言には、letconstが使えるようになりました。ともにブロックレベルのスコープをもちます。constは定数を定め、初期値は代入で書き替えられません(「JavaScript『再』入門」の「変数」参照)。また、オブジェクトにメソッドを備えるとき、コロン(:)とfunctionキーワードが省けます(「メソッドの定義」)。


let todoItem = Vue.extend({

	methods: {
		removeTodo(todo) {  // : function(todo) {
  			this.$emit('remove-todo', todo);
		}
	}
});

Array.filter()メソッドは、引数に渡すコールバック関数から論理(ブール)値を返すように定めます。すると、配列要素は順に関数の引数に渡され、trueが返された要素からなる新たな配列が返されるのです。さらに、名前のない(匿名)関数には、アロー関数式が使えます。functionキーワードが要らず、関数本体がreturn文だけのときはreturnと波かっこ{}も省いてしまって構いません。


let app = new Vue({

	methods: {

		archive() {
			/* let remains = [];
			let todos = this.todos;
			const length = todos.length;
			for(let i = 0; i < length; i++) {
				let todo = todos[i];
				if(!todo.done) {
					remains.push(todo);
				}
			} */
			this.todos =  // remains;
				this.todos.filter((todo) => !todo.done);
		}
	},

});

ここで、Vue.jsのテンプレートにも「省略記法」を用いることにしましょう。ディレクティブv-bindはコロン(:)ひとつ、v-on@記号に替えることができます。以上の書き替えを加えたのが、つぎのコード002です。jsdo.itのコードは、以下のサンプル003に掲げました。

コード002■ECMAScript 6とVue.jsの省略記法を使う

<body>要素

<div id="app" class="container">
	<h2>Todo</h2>
	<list-header :todos="todos" :remaining="remaining" @archive="archive"></list-header>
	<ul class="list-unstyled">
		<li v-for="todo in todos">
			<todo-item :todo="todo" @remove-todo="removeTodo">
			</todo-item>
		</li>
	</ul>
	<p>
		<input type="text" v-model="todoText" placeholder="add new todo here" />
		<button @click="addTodo()" class="btn btn-primary btn-sm">追加</button>
	</p>
</div>

<script>要素

// component: todoItem
let todoItem = Vue.extend({
	props: ['todo'],
	template: `
		<div>
			<label>
				<input type="checkbox" v-model="todo.done" />
				<span :class="{'done': todo.done}">{{todo.text}}</span>
			</label>
			<button @click="removeTodo(todo)" class="btn btn-warning btn-xs">削除</button>
		</div>
		`,
	methods: {
		removeTodo(todo) {
  			this.$emit('remove-todo', todo);
		}
	}
});
// component: listHeader
let listHeader = Vue.extend({
	props: ['todos', 'remaining'],
	template: `
		<p>
			全{{todos.length}}件中残り{{remaining}}件
			<button  @click="archive" class="btn btn-danger btn-xs">断捨離</button>
		</p>
		`,
	methods: {
		archive() {
  			this.$emit('archive');
		}
	}
});
// main application
let app = new Vue({
	components: {
		'todo-item': todoItem,
		'list-header': listHeader
	},
	data: {
		todoText: '',
		todos: [
			{text: 'Vue.jsを学ぶ', done: true},
			{text: 'Vue.jsでアプリケーションをつくる', done: false},
		]
	},
	methods: {
		addTodo() {
			const newTodo = this.todoText.trim();
			if (!newTodo) {return;}
			this.todos.push(
				{text: newTodo, done: false}
			);
			this.todoText = '';
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((_todo) => _todo !== todo);
		},
		archive() {
			this.todos = this.todos.filter((todo) => !todo.done);
		}
	},
	computed: {
		remaining() {
			return this.todos.filter((todo) => !todo.done).length;
		}
	}
});
document.addEventListener('DOMContentLoaded', function(event) {
	app.$mount('#app');
});

サンプル003■Vue.js + ES6: Todo List with Components


作成者: 野中文雄
作成日: 2017年9月18日


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