HTML5テクニカルノート
Vue.js + ES6: アプリケーションにコンポーネントを使う
- ID: FN1709001
- Technique: HTML5 / JavaScript
- Library: Vue.js 2.4.4
コンポーネントは、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要素が先に読み込まれていないとアプリケーションは動かない
<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リストのアプリケーションとしての動きは変わらない
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-bind
とv-on
を加えてデータのやり取りをします。子は、コンポーネントオプションのprops
とvm.$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>
// 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に書き替えましょう。変数の宣言には、let
とconst
が使えるようになりました。ともにブロックレベルのスコープをもちます。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>
// 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.