サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6: グリッドコンポーネントをつくる


Vue.jsサイトの「グリッドコンポーネントの例」は、コンポーネントの使い方を知るのによい作例です。 これをECMAScript 2015(ECMAScript 6)の構文でつくってみます(サンプル001)。とくに、データのコレクション(リスト)は、Arrayクラスの新しいメソッドとアロー関数式で扱うようにしました。Vue.jsについては、すでに入門ノートを何本か書いています。これらノートやVue公式サイトの情報は必要に応じて本文に引用しましたので、詳しく知りたい場合にはそちらをお読みください。

サンプル001■Vue.js + ES6: Grid component with sort and search

See the Pen Vue.js + ES6: Grid component with sort and search by Fumio Nonaka (@FumioNonaka) on CodePen.

01 グリッドコンポーネントで静的な表組みをつくる

Vue.jsはCDNでつぎのように<script>要素に読み込みます。

<head>要素

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

アプリケーション(demo)は、Vue()コンストラクタに以下のようにdataを定めます。ふたつの配列が含まれていて、それぞれキーになる列タイトルになるフィールド(gridColumns)と行単位のレコード(gridData)です(「フィールド/レコード」参照)。<body>要素では、あとでつくるコンポーネントのカスタムタグ(demo-grid)に、ふたつの配列データをコンポーネントのプロパティ(dataとcolumns)としてv-bindディレクティブ(省略記法:)により関連づけ(バインディング)しました。アプリケーションは、$mount()メソッドで、id属性(demo')を指定して要素(<div>)に組み込んでいます(「Vue.js + ES6: TodoMVCをつくる 01 ー 項目の追加と削除および残り項目数表示」02「アプリケーションに定めた項目のデータをTodoリストに表示する」参照)。なお、メソッドはDOMContentLoadedイベントで、アロー関数式のリスナーから呼び出しました。

<script>要素

// アプリケーションの起動
const demo = new Vue({
	data: {
		gridColumns: ['name', 'power'],
		gridData: [
			{name: 'Chuck Norris', power: Infinity},
			{name: 'Bruce Lee', power: 9000},
			{name: 'Jackie Chan', power: 7000},
			{name: 'Jet Li', power: 8000}
		]
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

<body>要素

<div id="demo">
	<demo-grid
		:data="gridData"
		:columns="gridColumns">
	</demo-grid>
</div>

Vue.component()メソッドに渡すのは、つぎのように第1引数が前述カスタムタグ(demo-grid)、第2引数はテンプレート(template)およびアプリケーションからバインディングされたプロパティ(props)が納められたオプションオブジェクトです(「Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける」参照)。テンプレートは後掲のようにtype属性にtext/x-templateを与えた<script>要素に定めました。propsプロパティはオブジェクトベースの構文にして、型チェックを加えてあります。(「Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック」04「プロパティの型を定める」)。

<script>要素

// グリッドコンポーネントの登録
Vue.component('demo-grid', {
	template: '#grid-template',
	props: {
		data: Array,
		columns: Array
	}
});

v-forディレクティブは、コンポーネントプロパティ(columnsdata)の配列からデータを順に取り出し、テンプレートにしたがって要素(<th>および<tr>td>)として差し込みます(「Vue.js + ES6入門 03: データから動的にリストをつくる」01「項目のデータを複数にする」)。これでグリッドの静的な表組みができあがります(図001)。なお、CSSは前出「グリッドコンポーネントの例」からそっくりコピーして<style>要素に定めました(後掲コード001)。

<head>要素

<!-- コンポーネントのテンプレート -->
<script type="text/x-template" id="grid-template">
<table>
	<thead>
		<tr>
			<th v-for="key in columns">
				{{key}}
				<span class="arrow">
				</span>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr v-for="entry in data">
			<td v-for="key in columns">
				{{entry[key]}}
			</td>
		</tr>
	</tbody>
</table>
</script>

図001■グリッドの静的な表組みのレイアウト

図001

フィールドとなるデータ(コンポーネントプロパティcolumns)の値は、レコードの並べ替えや検索のキーに使うため、小文字の識別子にしました。けれども、グリッドの列タイトルとして用いるときは、頭が大文字の方がよさそうです。この変換はフィルタで加えましょう(「Vue.js + ES6: TodoMVCをつくる 02 ー データをローカルに保存する」03「フィルタを使う」)。フィルタのメソッド(capitalize())は、つぎのようにVue.component()メソッドに渡すオプションオブジェクトのfiltersに定めます。用いた構文はキーワードfunctionを省いた「ECMAScript 2015 での新しい表記法」です。

<script>要素

Vue.component('demo-grid', {

	filters: {
		capitalize(str) {
			return str.charAt(0).toUpperCase() + str.slice(1);
		}
	}
});

テンプレートでフィルタは、バインディングした変数(key)のあとにパイプ|を添えて加えます。すると、変数値がフィルタのメソッド(capitalize())に渡されて、戻り値が用いられるのです。これで、グリッドの列タイトルは頭が大文字になります(図002)。データからグリッドの静的な表組みをつくるここまでのコードは、以下にまとめました(コード001)。

<script>要素のテンプレート

<th v-for="key in columns">
	{{key | capitalize}}
	<span class="arrow">
	</span>
</th>

図002■列タイトルの頭が大文字になった

図002

コード001■データからグリッドの静的な表組みをつくる

<head>要素

<script>
Vue.component('demo-grid', {
	template: '#grid-template',
	props: {
		data: Array,
		columns: Array
	},
	filters: {
		capitalize(str) {
			return str.charAt(0).toUpperCase() + str.slice(1);
		}
	}
});
const demo = new Vue({
	data: {
		gridColumns: ['name', 'power'],
		gridData: [
			{name: 'Chuck Norris', power: Infinity},
			{name: 'Bruce Lee', power: 9000},
			{name: 'Jackie Chan', power: 7000},
			{name: 'Jet Li', power: 8000}
		]
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);
</script>
<script type="text/x-template" id="grid-template">
<table>
	<thead>
		<tr>
			<th v-for="key in columns">
				{{key | capitalize}}
				<span class="arrow">
				</span>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr v-for="entry in data">
			<td v-for="key in columns">
				{{entry[key]}}
			</td>
		</tr>
	</tbody>
</table>
</script>

<body>要素

<div id="demo">
	<demo-grid
		:data="gridData"
		:columns="gridColumns">
	</demo-grid>
</div>

<style>要素

body {
	font-family: Helvetica Neue, Arial, sans-serif;
	font-size: 14px;
	color: #444;
}
table {
	border: 2px solid #42b983;
	border-radius: 3px;
	background-color: #fff;
}
th {
	background-color: #42b983;
	color: rgba(255,255,255,0.66);
	cursor: pointer;
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}
td {
	background-color: #f9f9f9;
}
th, td {
	min-width: 120px;
	padding: 10px 20px;
}
th.active {
	color: #fff;
}
th.active .arrow {
	opacity: 1;
}
.arrow {
	display: inline-block;
	vertical-align: middle;
	width: 0;
	height: 0;
	margin-left: 5px;
	opacity: 0.66;
}
.arrow.asc {
	border-left: 4px solid transparent;
	border-right: 4px solid transparent;
	border-bottom: 4px solid #fff;
}
.arrow.dsc {
	border-left: 4px solid transparent;
	border-right: 4px solid transparent;
	border-top: 4px solid #fff;
}

02 レコードを並べ替える

グリッドの列タイトルがクリックされたら、レコードをそのフィールドの順序(アルファベットあるいは数値順)で並べ替えるようにしましょう。Vue.component()メソッドに渡すオプションオブジェクトのdataにはキーのフィールド識別子を納めるプロパティ(sortKey)、そしてmethodsにクリックで呼び出されるメソッド(sortBy())を以下のように定めます。メソッドが受け取るのは、ソートのキーの識別子です。なお、コンポーネントに加えるdataは関数でなければならないことにお気をつけください。

並べ替えのメソッド(filteredData())は算出プロパティcomputedとして加えます(「Vue.js + ES6入門 05: 項目を数えて表示する」03「算出プロパティを使う」)。ソートするキーのフィールド(sortKey)とレコード(data)の値を取り出したら、Array.sort()メソッドで並べ替えます。引数に渡すのは、キーに応じた値から順序を決める比較関数です。ソートする前に引数なしで呼び出したArray.slice()メソッドは配列を複製するので、もとのレコードには手が加わりません。

<script>要素

Vue.component('demo-grid', {

	data() {
		return {
			sortKey: ''
		};
	},
	computed: {
		filteredData() {
			const sortKey = this.sortKey;
			let data = this.data;
			if(sortKey) {
				data = data.slice().sort((a, b) => {
					a = a[sortKey];
					b = b[sortKey];
					return (a === b ? 0 : a > b ? 1 : -1);
				});
			}
			return data;
		}
	},

	methods: {
		sortBy(key) {
			this.sortKey = key;
		}
	}
});

コンポーネントのテンプレートには、列タイトルの要素(<th>)に以下のようにディレクティブv-on(省略記法:)でclickイベントにメソッド(sortBy())を定めます(「Vue.js + ES6入門 04: フィールドに入力したテキストを動的に項目として加える」02「入力フィールドの項目をデータに加える」)。引数に渡すのがクリックしたフィールドの識別子(key)です。そして、レコードを取り出す要素(<tr>)のv-forディレクティブは、もとにするデータを算出プロパティ(filteredData())に差し替えました。これでレコードがソートされた順に差し込まれるのです。

また、ふたつの要素にCSSのクラスを加えました。ひとつは列タイトルの要素(<th>)で、ディレクティブv-bind(省略記法:)にclassを添えた構文によるクラスバインディングです(「Vue.js + ES6入門 02: 要素のclass属性を動的に変える」03「バインディングでクラス属性を動的に変える」)。クリックで選ばれたキー(sortKey)の要素に、スタイル(active)が与えられます。もうひとつは、列タイトルに含めた要素(<span>)で、右横に△印を表示するためのクラス(asc)です。これで、グリッドの表組みは、クリックしたフィールドでレコードがソートされるようになりました(図003)。

<script>要素のテンプレート

<table>
	<thead>
		<tr>
			<th v-for="key in columns"
				@click="sortBy(key)"
				:class="{active: sortKey === key}">

				<span class="arrow asc">
				</span>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr v-for="entry in filteredData">

		</tr>
	</tbody>
</table>

図003■クリックしたフィールドでレコードがソートされる

図003

さらに、クリックするたびに、並びを昇順/降順切り替えましょう。整数で昇順が1、降順は-1と決め、つぎのように各キーに初期値1をオブジェクト(sortOrders)に定めたうえで、Vue.component()メソッドのオプションオブジェクトにdataとして加えます。そして、グリッドの列タイトルがクリックされたときのリスナーメソッド(sortBy())でキーの整数値を切り替え、ソートの算出プロパティ(filteredData())が用いる比較関数の戻り値に乗じればよいのです。

<script>要素

Vue.component('demo-grid', {

	data() {
		const sortOrders = {};
		this.columns.forEach((key) => sortOrders[key] = 1);
		return {

			sortOrders: sortOrders
		};
	},
	computed: {
		filteredData() {

			const order = this.sortOrders[sortKey] || 1;

			if(sortKey) {
				data = data.slice().sort((a, b) => {

					return (a === b ? 0 : a > b ? 1 : -1) * order;
				});
			}
			return data;
		}
	},

	methods: {
		sortBy(key) {

			this.sortOrders[key] *= -1;
		}
	}
});

列タイトルに添えた印の要素(<span>)も、昇順/降順で△/▽と上下が変わるように、つぎのテンプレートのとおりクラスをバインディングで差し替えます。これで、フィールドをクリックするたびにソートされるレコードの昇順/降順が変わります(図004)。

<script>要素のテンプレート

<th v-for="key in columns"
	@click="sortBy(key)"
	:class="{active: sortKey === key}">

	<span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'">
	</span>
</th>

図004■クリックするたびにソートの昇順/降順が変わる

図004

コード002■グリッドのフィールドをクリックするとレコードが昇順/降順に並び替わる

<script>要素

Vue.component('demo-grid', {
	template: '#grid-template',
	props: {
		data: Array,
		columns: Array
	},
	data() {
		const sortOrders = {};
		this.columns.forEach((key) => sortOrders[key] = 1);
		return {
			sortKey: '',
			sortOrders: sortOrders
		};
	},
	computed: {
		filteredData() {
			const sortKey = this.sortKey;
			const order = this.sortOrders[sortKey] || 1;
			let data = this.data;
			if(sortKey) {
				data = data.slice().sort((a, b) => {
					a = a[sortKey];
					b = b[sortKey];
					return (a === b ? 0 : a > b ? 1 : -1) * order;
				});
			}
			return data;
		}
	},
	filters: {
		capitalize(str) {
			return str.charAt(0).toUpperCase() + str.slice(1);
		}
	},
	methods: {
		sortBy(key) {
			this.sortKey = key;
			this.sortOrders[key] *= -1;
		}
	}
});
const demo = new Vue({
	data: {
		gridColumns: ['name', 'power'],
		gridData: [
			{name: 'Chuck Norris', power: Infinity},
			{name: 'Bruce Lee', power: 9000},
			{name: 'Jackie Chan', power: 7000},
			{name: 'Jet Li', power: 8000}
		]
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

<script>要素のテンプレート

<table>
	<thead>
		<tr>
			<th v-for="key in columns"
				@click="sortBy(key)"
				:class="{active: sortKey === key}">
				{{key | capitalize}}
				<span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'">
				</span>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr v-for="entry in filteredData">
			<td v-for="key in columns">
				{{entry[key]}}
			</td>
		</tr>
	</tbody>
</table>

03 レコードを検索する

つぎは、テキスト入力フィールドに打ち込む文字列で、レコードを検索できるようにします。<body>要素にはつぎのように<input>要素を加えて、v-modelディレクティブでデータバインディングします(「フォーム入力バインディング」)。バインディングした変数(searchQuery)は、コンポーネントのカスタムタグ(demo-grid)にディレクティブv-bind(省略記法:)で属性(filter-key)としてバインドしました。この値はコンポーネントプロパティから参照します。

<body>要素

<form id="search">
	Search <input name="query" v-model="searchQuery">
</form>
<demo-grid

	:filter-key="searchQuery">
</demo-grid>

v-modelディレクティブでバインドした変数(searchQuery)は、以下のようにアプリケーションのdataに定めます。そして、コンポーネントのカスタムタグにv-bindディレクティブでバインドしたプロパティは、Vue.component()メソッドに渡すオプションオブジェクトのpropsに加えます。このとき、タグには属性のかたちでハイフン(-)つなぎで定めたプロパティ名は、ハイフン(-)は除いて頭を大文字でつなぐ(filterKey)ことにお気をつけください。HTMLコードでは大文字小文字が区別されず、JavaScriptコードの識別子にはハイフン(-)が使えないための仕様です(「プロパティの形式 (キャメルケース vs ケバブケース)」)。

<input>要素に打ち込まれたテキストを検索するのは、算出プロパティのメソッド(filteredData())です。とりあえず、console.log()メソッドでバインディングを確かめておきましょう。また、検索するテキストはString.toLowerCase()メソッドで小文字に統一します。テキストフィールドに入力したアルファベットは、すべて小文字でコンソールに出力されるはずです。

<script>要素

Vue.component('demo-grid', {

	props: {

		filterKey: String
	},

	computed: {
		filteredData() {

			const filterKey = this.filterKey && this.filterKey.toLowerCase();
			console.log(filterKey);  // 確認用
	},

});

const demo = new Vue({
	data: {
		searchQuery: '', 

	}
});

算出プロパティのメソッド(filteredData())は、ECMAScript 5.1で備わった3つのメソッドにより検索語が含まれるレコードを選び出します。まず、Array.filter()メソッドは、引数に渡した関数の戻り値が、ブール値でtrueと評価される要素からなる新たな配列を返します。この戻り値の配列(data)が、検索語を含むレコードです。つぎに、Object.keys()が、レコードごとの要素(row)からキーを配列として取り出します。その配列に対してArray.some()メソッドを呼び出せば、その中に少なくともひとつ条件に当てはまる要素が含まれるかどうかブール値が返ります。引数に渡した関数は、レコードの要素からキーごとの値を取り出して、検索語が含まれるかどうか調べるという手順です。なお、String.includes()はECMAScript 2015の新しいメソッドで、引数の文字列が含まれるかどうかを返します。

<script>要素

Vue.component('demo-grid', {

	computed: {
		filteredData() {

			const filterKey = this.filterKey && this.filterKey.toLowerCase();

			let data = this.data;
			if (filterKey) {
				data = data.filter((row) =>
					Object.keys(row).some((key) =>
						String(row[key]).toLowerCase().includes(filterKey)
					)
				);
			}

			return data;
		}
	},

});

これで、入力フィールドに打ち込んだテキストで検索されたレコードが選ばれてグリッドに表示されます(図005)。ソースのJavaScriptと<body>要素のHTMLの記述を以下のコード003にまとめました。<script>要素に定めたコンポーネントのテンプレートは、前掲コード002のまま変わりません。実際の動きは、CodePenに掲げた冒頭のサンプル001でお確かめください。なお、「Vue.jsの『グリッドコンポーネントの例』をECMAScript 6の構文に書き替える」で、新しい構文にするときのポイントをご紹介しています。

図005■クリックするたびにソートの昇順/降順が変わる

図005

コード003■グリッドコンポーネントで表組みのレコードをソート/検索する

<script>要素

Vue.component('demo-grid', {
	template: '#grid-template',
	props: {
		data: Array,
		columns: Array,
		filterKey: String
	},
	data() {
		const sortOrders = {};
		this.columns.forEach((key) => sortOrders[key] = 1);
		return {
			sortKey: '',
			sortOrders: sortOrders
		};
	},
	computed: {
		filteredData() {
			const sortKey = this.sortKey;
			const filterKey = this.filterKey && this.filterKey.toLowerCase();
			const order = this.sortOrders[sortKey] || 1;
			let data = this.data;
			if (filterKey) {
				data = data.filter((row) =>
					Object.keys(row).some((key) =>
						String(row[key]).toLowerCase().includes(filterKey)
					)
				);
			}
			if(sortKey) {
				data = data.slice().sort((a, b) => {
					a = a[sortKey];
					b = b[sortKey];
					return (a === b ? 0 : a > b ? 1 : -1) * order;
				});
			}
			return data;
		}
	},
	filters: {
		capitalize(str) {
			return str.charAt(0).toUpperCase() + str.slice(1);
		}
	},
	methods: {
		sortBy(key) {
			this.sortKey = key;
			this.sortOrders[key] *= -1;
		}
	}
});
const demo = new Vue({
	data: {
		searchQuery: '',
		gridColumns: ['name', 'power'],
		gridData: [
			{name: 'Chuck Norris', power: Infinity},
			{name: 'Bruce Lee', power: 9000},
			{name: 'Jackie Chan', power: 7000},
			{name: 'Jet Li', power: 8000}
		]
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

<body>要素

<div id="demo">
	<form id="search">
		Search <input name="query" v-model="searchQuery">
	</form>
	<demo-grid
		:data="gridData"
		:columns="gridColumns"
		:filter-key="searchQuery">
	</demo-grid>
</div>


作成者: 野中文雄
更新日: 2019年11月29日 サンプルをCodePenに改め、本文とコードにも少し修正を加えた。
更新日: 2017年11月06日「Vue.jsの『グリッドコンポーネントの例』をECMAScript 6の構文に書き替える」を引用。
作成日: 2017年10月30日


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