HTML5テクニカルノート
Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック
- ID: FN1907002
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
「Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける」でつくったTodoリストのアプリケーション(サンプル001)から、さらにコンポーネントを分けて入れ子にします。そして、データのチェックも加えて、つくりをしっかりしましょう。
01 項目入力部分をコンポーネントに切り分ける
先に、入力した項目の追加部分をコンポーネント(add-todo
)に切り分けます(図001)。
図001■項目入力の部分をコンポーネントに切り分ける
ここでひとつ考えることは、v-model
ディレクティブで双方向バインディングしているデータ(todoText
)の扱いです。コンポーネント(add-todo
)内でフィールドに入力しているテキストそのものは、アプリケーションが気にしなくて構いません。追加ボタンが押されたとき(click
イベント)、そのテキストを受け取れば済むからです。アプリケーションのハンドラメソッド(addTodo()
)には、そのために引数(newTodo
)を新たに加えます。
<body>要素<script>要素<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>
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■リスト表示部分をコンポーネントに切り分ける
親アプリケーションのテンプレートを、つぎのようにリスト表示のコンポーネント(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
)を、コンポーネントが直に参照しているからです。
<script>要素[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.
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>要素<script>要素<div id="app" class="container"> <list-footer :filters="filters" > </list-footer> </div>
Vue.component('list-footer', { props: ['visibility', 'filters'], });
04 プロパティの型を定める
コンポーネントのprops
オプションは、配列にプロパティ名を加える以外に、オブジェクトでもっと細かく定められます。このオブジェクトによる定義が公式スタイルガイドの推奨です(「プロパティの定義」参照)。以下のように、オブジェクトにプロパティ名とその型を与えます。すると、開発版のライブラリであれば、型が違うとコンソールに警告を示してくれるのです。
<script>要素[Vue warn]: Invalid prop: type check failed for prop "todo". Expected String with value "[object Object]", got Object found in ---> <TodoItem> <TodoList> <Root>
Vue.component('todo-item', { // props: ['todo'], props: { todo: String }, });
もっとも、デフォルトではプロパティ(notExist
)がなくても放っておかれます。
<script>要素Vue.component('todo-item', { props: { todo: Object, notExist: String // 存在しない }, });
プロパティの有無まで確かめたいときは、さらに値をオブジェクトにして、required
オプションに定められます(デフォルト値false
)。データ型はtype
オプションです。
<script>要素[Vue warn]: Missing required prop: "notExist"
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>
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
- Vue.js + ES6入門 01: Vue.jsを始める
- Vue.js + ES6入門 02: 要素のclass属性を動的に変える
- Vue.js + ES6入門 03: データから動的にリストをつくる
- Vue.js + ES6入門 04: フィールドに入力したテキストを動的に項目として加える
- Vue.js + ES6入門 05: 項目を数えて表示する
- Vue.js + ES6入門 06: 項目を調べてデータから削除する
- Vue.js + ES6入門 07: 表示する項目をフィルタで切り替える
- Vue.js + ES6入門 08: フィルタをボタンで切り替える
- Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける
- Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック
- Vue.js + ES6入門 11: データチェックの応用とkey属性
- Vue.js + ES6入門 12: ローカルコンポーネントを定める
作成者: 野中文雄
更新日: 2019年8月26日 本文説明とスクリプトの加筆・補正。
作成日: 2019年7月19日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.