サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6: 再帰的なコンポーネントでツリー表示をつくる


再帰的なコンポーネント」は、テンプレートに自分自身を入れ子にして読み込む仕組みです。基本となる構造を階層化して表現できます。公式サイトの「例」に掲げられた 「ツリー表示の例」は、コンポーネントを再帰的に用いたサンプルです。入れ子にしたデータを、ツリー構造でリスト表示してみましょう(図001)。なお、JavaScriptコードの構文は、ECMAScript 2015 (ECMAScript 6)を用います。

図001■ツリー構造で表示したリスト

図001

01 入れ子のデータをツリー表示する

データの基本単位は、ふたつのプロパティを備えるオブジェクトにします。プロパティは、名前のテキスト(name)と、入れ子データの配列(children)です。入れ子をまとめたデータはつぎのように定数(data)に納め、Vueアプリケーション(demo)に渡す引数オブジェクトのdataプロパティに加えます(treeData)。アプリケーションは、HTMLドキュメントが読み込み終わったとき(DOMContentLoadedイベント)、vm.$mount()メソッドでHTMLドキュメントの指定した要素(id属性demo)に関連づけられます。

<script>要素

const data = {
	name: 'My Tree',
	children: [
		{name: 'hello'},
		{name: 'wat'},
		{
			name: 'child folder',
			children: [
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				},
				{name: 'hello'},
				{name: 'wat'},
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				}
			]
		}
	]
};

const demo = new Vue({
	data: {
		treeData: data
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

コンポーネントを登録するVue.component()メソッドには、第1引数のid(item)のほか、第2引数のオプションオブジェクトにつぎのように3つのプロパティを与えます。templateに与えたのは、あとで定めるコンポーネントのテンプレートのid属性(item-template)です。propsにはバインディングされるObject型のプロパティ(model)、computedには算出プロパティ(isFolder())を加えました。算出プロパティは、子のデータ(children)をもつかどうかブール(論理)値で返します。

<script>要素

Vue.component('item', {
	template: '#item-template',
	props: {
		model: Object
	},
	computed: {
		isFolder() {
			return this.model.children &&
				this.model.children.length;
		}
	},
});

<body>要素には、つぎのようにリストの大枠をつくります。親の<ul>要素にアプリケーションが関連づけされるid属性(demo)を与えました。子の要素はコンポーネント(item)です。定数(treeData)に納めたデータが、プロパティ(model)にバインディングされます。なお、ディレクティブ:は、v-bindの省略記法です。

<body>要素

<ul id="demo">
	<item
		class="item"
		:model="treeData">
	</item>
</ul>

コンポーネントのテンプレートは、以下のように<script>要素にtype属性をtext/x-templateとして定めます。id属性は、前掲のVue.component()メソッドを呼び出すときオプションオブジェクトのtemplateに与えた値(item-template)です。バインディングされたプロパティ(model)のテキスト(name)を示し、子のデータ(children)の配列からオブジェクトを順に取り出します。ここでコンポーネント(item)を再帰的に差し込み、v-forディレクティブで取り出したデータ(model)をバインディングすることにより、入れ子のツリー構造がつくられるのです。

v-forディレクティブで配列要素を取り出すとき、ふたつ目の引数(index)にインデックスが得られます(「v-for で配列に要素をマッピングする」参照)。v-forは項目に一意のkeyを与えなければなりません(「キー付きv-for」参照)。そこで、インデックスをその値に用いました。なお、:classv-bind:classの省略記法で、値(算出プロパティisFolder)がtrueのときクラス(bold)を動的に割り当てます。

<head>要素

<script type="text/x-template" id="item-template">
<li>
	<div
		:class="{bold: isFolder}">
		{{model.name}}
	</div>
	<ul>
		<item
			class="item"
			v-for="(model, index) in model.children"
			:key="index"
			:model="model">
		</item>
	</ul>
</li>
</script>

HTMLドキュメントの<body>要素の記述はこれででき上がりです。<style>要素のCSSの定めと併せて、つぎのコード001に掲げます。

コード001■ツリー表示の<body>要素と<style>要素

<body>要素

<ul id="demo">
	<item
		class="item"
		:model="treeData">
	</item>
</ul>

<style>要素

body {
	font-family: Menlo, Consolas, monospace;
	color: #444;
}
.item {
	cursor: pointer;
}
.bold {
	font-weight: bold;
}
ul {
	padding-left: 1em;
	line-height: 1.5em;
	list-style-type: dot;
}

02 ツリーの子を開け閉じする

ツリー表示の子の階層を、マウスクリックで開け閉じできるようにしましょう。先に、子のデータが含まれている項目(フォルダ)に印をつけます。あとで開け閉めしますので、それがわかるように閉じていれば[+]、開いていたら[-]です。開いているどうかのプロパティ(open)は、コンポーネントのdataに以下のように加えます。子のあるなしは算出プロパティ(isFolder)で確かめて、表示・非表示を切り替えるのがv-ifディレクティブです。

コンポーネントテンプレート

<li>
	<div
		:class="{bold: isFolder}">

		<span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
	</div>

</li>

<script>要素

Vue.component('item', {

	data() {
		return {
			open: true
		};
	},

});

これで、子のデータをもつ項目に印がつきます(図002)。開いているどうかのプロパティ(open)はデフォルト値をtrueにしたので印ははじめ[-]です。

図002■子のデータをもつ項目に印がつく

図002

子のデータをもつ項目は、クリックで開け閉じできるようにします。項目の要素(<div>)にclickイベントリスナーを、v-on(省略記法@)ディレクティブで定めます。呼び出すのはコンポーネントに以下のように加えたメソッド(toggle())です。クリックするたびに、子の階層が開いているかどうかのプロパティ(open)のブール値を反転します。すると、テンプレートのリスト(<ul>要素)の表示・非表示が、v-showディレクティブにより切り替わるのです。なお、項目が子のデータをもたなければ(isFolderがfalse)、やはりv-ifディレクティブで表示されません。

コンポーネントテンプレート

<li>
	<div

		@click="toggle">

	</div>
	<ul v-show="open" v-if="isFolder">
		<item

			>
		</item>
	</ul>
</li>

<script>要素

Vue.component('item', {

	methods: {
		toggle() {
			if (this.isFolder) {
				this.open = !this.open;
			}
		}
	}
});

これで、子のデータをもつ項目には[+]/[-]の印がつき、マウスクリックで子の階層の開け閉じができるようになりました。ここまでのコンポーネントテンプレートとJavaScriptコードを、いったんつぎのコード002にまとめます。併せて、以下のサンプル001をjsdo.itに掲げましたので、詳しいコードやその動きはこちらでお確かめください。

コード002■ツリーの子を開け閉じする

コンポーネントテンプレート

<li>
	<div
		:class="{bold: isFolder}"
		@click="toggle">
		{{model.name}}
		<span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
	</div>
	<ul v-show="open" v-if="isFolder">
		<item
			class="item"
			v-for="(model, index) in model.children"
			:key="index"
			:model="model">
		</item>
	</ul>
</li>

<script>要素

const data = {
	name: 'My Tree',
	children: [
		{name: 'hello'},
		{name: 'wat'},
		{
			name: 'child folder',
			children: [
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				},
				{name: 'hello'},
				{name: 'wat'},
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				}
			]
		}
	]
};
Vue.component('item', {
	template: '#item-template',
	props: {
		model: Object
	},
	data() {
		return {
			open: false
		};
	},
	computed: {
		isFolder() {
			return this.model.children &&
				this.model.children.length;
		}
	},
	methods: {
		toggle() {
			if (this.isFolder) {
				this.open = !this.open;
			}
		}
	}
});
const demo = new Vue({
	data: {
		treeData: data
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

サンプル001■Vue.js + ES6: Tree view base

03 項目とフォルダを加える

さらに、項目とフォルダが加えられるようにしましょう。まずは項目です。それぞれの階層の終わりに、つぎのように追加ボタン代わりのテキスト(+)を要素(<li>)に加えます。クリックしたとき(@clickディレクティブ)のイベントリスナー(addChild())は、コンポーネントに以下のように定め、子のデータ(children)の配列に新たな項目をつくって納めます。

コンポーネントテンプレート

<li>

	<ul v-show="open" v-if="isFolder">

		<li class="add" @click="addChild">+</li>
	</ul>
</li>

<script>要素

Vue.component('item', {

	methods: {

		addChild() {
			this.model.children.push({
				name: 'new stuff'
			})
		}
	}
});

フォルダはいきなりつくるのではなく、項目をダブルクリックして変換しましょう。イベントリスナーは、つぎのようにv-onディレクティブ(省略記法@)にネイティブイベントdblclickを添えて定めます。以下のとおりコンポーネントに加えたのがリスナーメソッド(changeType())です。フォルダでないことを確かめたうえで、子のデータのプロパティ(children)に配列と要素の項目を加えています。

コンポーネントテンプレート

<li>
	<div
		:class="{bold: isFolder}"
		@click="toggle"
		@dblclick="changeType">

	</div>

</li>

<script>要素

Vue.component('item', {

	methods: {

		changeType() {
			if (!this.isFolder) {
				Vue.set(this.model, 'children', [])
				this.addChild()
				this.open = true
			}
		},

	}
});

これで、追加記号(+)のクリックで項目が加わり、項目をダブルクリックすればフォルダに変わるようになりました。お題にした公式サイトの「ツリー表示の例」と同じ動きです。でき上がったコンポーネントテンプレートとJavaScriptコードは、つぎのコード003にまとめました。また、以下のサンプル002をjsdo.itに掲げます。

コード003■項目とフォルダが加えられるツリー表示のインタフェース

コンポーネントテンプレート

<li>
	<div
		:class="{bold: isFolder}"
		@click="toggle"
		@dblclick="changeType">
		{{model.name}}
		<span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
	</div>
	<ul v-show="open" v-if="isFolder">
		<item
			class="item"
			v-for="(model, index) in model.children"
			:key="index"
			:model="model">
		</item>
		<li class="add" @click="addChild">+</li>
	</ul>
</li>

<script>要素

const data = {
	name: 'My Tree',
	children: [
		{name: 'hello'},
		{name: 'wat'},
		{
			name: 'child folder',
			children: [
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				},
				{name: 'hello'},
				{name: 'wat'},
				{
					name: 'child folder',
					children: [
						{name: 'hello'},
						{name: 'wat'}
					]
				}
			]
		}
	]
};
Vue.component('item', {
	template: '#item-template',
	props: {
		model: Object
	},
	data() {
		return {
			open: false
		};
	},
	computed: {
		isFolder() {
			return this.model.children &&
				this.model.children.length;
		}
	},
	methods: {
		toggle() {
			if (this.isFolder) {
				this.open = !this.open;
			}
		},
		changeType() {
			if (!this.isFolder) {
				Vue.set(this.model, 'children', [])
				this.addChild()
				this.open = true
			}
		},
		addChild() {
			this.model.children.push({
				name: 'new stuff'
			})
		}
	}
});
const demo = new Vue({
	data: {
		treeData: data
	}
});
document.addEventListener('DOMContentLoaded', (event) =>
	demo.$mount('#demo')
);

サンプル002■Vue.js + ES6: Tree view


作成者: 野中文雄
作成日: 2018年2月28日


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