HTML5テクニカルノート
Vue.js + ES6入門 11: データチェックの応用とkey属性
- ID: FN1907003
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
「Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック」でつくったTodoリストのアプリケーション(サンプル001)でデータチェックの基本についてご説明しました。今回は、もう少し厳密なチェックのオプションをご紹介します。また、ディレクティブv-for
とともに用いるべき、key
属性の定め方とその役割を解説しましょう。
01 propsをvalidatorオプションでチェックする
前回は、props
のデータチェックがオブジェクトで定められることをご説明しました。そのオプションとして与えたのはtype
とrequired
でした。今回もうひとつご紹介するのがvalidator
オプションです(「プロパティのバリデーション」参照)。メソッドのかたちで、確かめるプロパティを引数に受け取ります。そして、メソッド本体にチェックのコードを書き、戻り値がtrue
ならテスト合格です。
list-footer
コンポーネントのprops
には、プロパティvisibility
が備わっていました。値は文字列で、'all'
か'active'
あるいは'completed'
のいずれかです。そこで、試しにvalidator
オプションをつぎのように定めてみましょう。Array.prototype.includes()
メソッドで、値の文字列が'all'
であることを確かめています。
<script>要素Vue.component('list-footer', { props: { visibility: { type: String, required: true, validator(value) { return ( ['all'].includes(value) ); } }, }, }
初期値は'all'
ですので、はじめはブラウザコンソールに警告はでません。けれど、たとえば別のフィルタボタンを選ぶとつぎのような警告が示されるでしょう。
[Vue warn]: Invalid prop: custom validator check failed for prop "visibility". found in ---> <ListFooter> <Root>
つぎのように、残りのふたつの値も配列要素に加えれば、警告が出なくなります。visibility
プロパティの値がチェックできたということです。
<script>要素Vue.component('list-footer', { props: { visibility: { validator(value) { return ( ['all', 'active', 'completed'].includes(value) ); } }, }, }
同じlist-footer
コンポーネントはもうひとつ、3つのメソッドが与えられたオブジェクトfilters
をプロパティにもちます。この場合は、メソッド(プロパティ)名を確かめるのがよさそうです。つぎのようにObject.prototype.hasOwnProperty()
メソッドでプロパティの有無が調べられます。
<script>要素Vue.component('list-footer', { props: { filters: { type: Object, required: true, validator(value) { return ( value.hasOwnProperty('all') && value.hasOwnProperty('active') && value.hasOwnProperty('completed') ); } } }, }
02 v-forディレクティブでつくる要素に一意のkey属性を与える
データにもとづいて複数の項目を要素として差し込むのがv-for
ディレクティブでした。公式「スタイルガイド」は、v-for
に特別属性key
を与えるのが「必須」のルールだとしています(「キー付きv-for」参照)。
list-footer
コンポーネントには、フィルタボタンをv-for
ディレクティブで加えました。ボタンの名前にも用いたフィルタのキーは、もちろん一意です。この値をつぎのようにkey
属性にバインドしましょう。
<script>要素Vue.component('list-footer', { template: ` <div> <button type="button" v-for="(value, key) in filters" :key="key" > </button> </div> ` });
もうひとつ、Todo項目をリスト表示するときも、v-for
で要素を差し込みました。こちらは、データにIDを加えておくのがよいでしょう。まずは、アプリケーション(app
)に定めた初期値の配列(todos
)です。もちろん、手で書き込んで構いません。でも、ここではArray.prototype.map()
メソッドを使ってみることにします。引数のコールバック関数は、配列要素(todo
)とインデックス(index
)が受け取れます。オブジェクトのプロパティはスプレッド演算子...
で展開できますので、配列インデックスをプロパティid
に与えればよいでしょう(「スプレッド構文を使う」参照)。
<script>要素const app = new Vue({ data: { todos: [ {text: 'Vue.jsを学ぶ', done: true}, {text: 'Vue.jsでアプリケーションをつくる', done: false}, ] // , .map((todo, index) => ({...todo, id: index})), }, });
リスト表示のコンポーネント(todo-list
)は、v-for
ディレクティブで差し込む要素にkey
属性を加えます。与えるのは項目データ(todo
)から取り出したid
プロパティの値です。
<script>要素Vue.component('todo-list', { template: ` <ul class="list-unstyled"> <li v-for="todo in filteredTodos" :key="todo.id"> </li> </ul> ` });
項目を加えるときにも、id
プロパティを与えなければなりません。メソッド(addTodo()
)はアプリケーション(app
)に定めてありました。もっとも簡単なのは、はじめに整数を決めて(todos.length
でよいでしょう)、あとは項目を加えるたびにひたすらカウントアップした値を用いることです。
今回はそれで差し支えありません。ただ、id
の値が表示されるようなとき、項目の追加・削除を繰り返すと数値が大きくなって、はじめの方の項目と開きが大きくなります。そのような見栄えが気になる場合の処理をご紹介します。
つぎのID番号を返す算出プロパティ(nextId
)は、すでに使われている最大値に1加えた数値を返します。ここで用いた、スプレッド演算子と同じ構文の分割代入...
は、配列を展開して関数の引数として渡します。
<script>要素const app = new Vue({ computed: { 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); // 確認用 }, } });
これで、項目データ(todo
)が備えるべきプロパティが決まりました。そこで、Todo項目のコンポーネント(todo-item
)のprops
にデータチェックのvalidator
オプションをつぎのように加えましょう。
<script>要素Vue.component('todo-item', { props: { todo: { type: Object, required: true, validator(value) { return ( value.hasOwnProperty('text') && value.hasOwnProperty('done') && value.hasOwnProperty('id') ); } } }, });
key
属性はなぜ加えなければならないのでしょう。それは、データが変わったとき、どのDOM要素に反映すべきかVueが追いかける目印にするためです。key
が付されないとどのような問題が起こるのかについては「Vue.js: v-forで項目インデックスをkey属性にしていいのか」をお読みください。
ここまで書き上げたJavaScrptコードは、つぎにまとめたとおりです(コード001)。また、サンプル001をCodePenに掲げました。
コード001■ヘッダコンポーネントと親のテンプレート
<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-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,
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>
`
});
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" :key="todo.id">
<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,
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({
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: props validation and key attribute
See the Pen Vue.js + ES6: props validation and key attribute 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年7月28日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.