サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6入門 11: データチェックの応用とkey属性


Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック」でつくったTodoリストのアプリケーション(サンプル001)でデータチェックの基本についてご説明しました。今回は、もう少し厳密なチェックのオプションをご紹介します。また、ディレクティブv-forとともに用いるべき、key属性の定め方とその役割を解説しましょう。

01 propsをvalidatorオプションでチェックする

前回は、propsのデータチェックがオブジェクトで定められることをご説明しました。そのオプションとして与えたのはtyperequiredでした。今回もうひとつご紹介するのが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属性はなぜ加えなければならないのでしょう。それは、データが変わったとき、どの要素に影響を及ぼすべきか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 todoText = this.todoText.trim();
			this.todoText = '';
			if (todoText) {
				this.$emit('add-todo', todoText);
			}
		}
	},
	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


作成者: 野中文雄
作成日: 2019年7月28日


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