Vue.jsインラインコンポーネントでフォルダツリー
前回はバージョン3になったVue.jsでインライン記述の方法を学びました。今回はそこから一歩踏み出して、インラインでコンポーネントを作成してみます。
公式ページにあるVue.js:「ツリー表示 の例」のバージョン2用のフォルダ構造表示のコードをバージョン3用に書き換えてみたいと思います。
バージョン2のコード
公式ページにあるバージョン2のコードは次のようになっています。
フォルダ構造表示(V2)
<!DOCTYPE html>
<html>
<head>
<title>Tree View</title>
<script src="https://unpkg.com/vue@next"></script>
<style> .bold { text-decoration: underline; } </style>
<script type="text/x-template" id="item-template">
<li>
<div
:class="{bold: isFolder}"
@click="toggle"
@dblclick="makeFolder">
{{ item.name }}
<span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
</div>
<ul v-show="isOpen" v-if="isFolder">
<tree-item
class="item"
v-for="(child, index) in item.children"
:key="index"
:item="child"
@make-folder="$emit('make-folder', $event)"
@add-item="$emit('add-item', $event)"
></tree-item>
<li class="add" @click="$emit('add-item', item)">+</li>
</ul>
</li>
</script>
</head>
<body>
<p>(You can double click on an item to turn it into a folder.)</p>
<!-- the demo root element -->
<ul id="demo">
<tree-item
class="item"
:item="treeData"
@make-folder="makeFolder"
@add-item="addItem"
></tree-item>
</ul>
<script>
// demo data
var treeData = {
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" }]
}
]
}
]
};
// define the tree-item component
Vue.component("tree-item", {
template: "#item-template",
props: {
item: Object
},
data: function() {
return {
isOpen: false
};
},
computed: {
isFolder: function() {
return this.item.children && this.item.children.length;
}
},
methods: {
toggle: function() {
if (this.isFolder) {
this.isOpen = !this.isOpen;
}
},
makeFolder: function() {
if (!this.isFolder) {
this.$emit("make-folder", this.item);
this.isOpen = true;
}
}
}
});
// boot up the demo
var demo = new Vue({
el: "#demo",
data: {
treeData: treeData
},
methods: {
makeFolder: function(item) {
Vue.set(item, "children", []);
this.addItem(item);
},
addItem: function(item) {
item.children.push({
name: "new stuff"
});
}
}
});
</script>
</body>
</html>
Vue.jsバージョン2での記法
さしあたりバージョン2のコードでわかりづらい点を解説しておきます。コード内にアンダーラインを引いた箇所がそれにあたります。
- script type="text/x-template"
script type="text/x-template"の部分は、typeでtemplateの記述だと示したうえで、templateの中身をhtmlで記述しています。あとからここで設定してあるidを使って読みだすことができます。
- :class="{ bold: isFolder }"
isFolderが真だったら、boldというクラスが追加されるという記述方法です。別の方法として:class="[isFolder ? 'bold' : '']"という、三項演算子と配列を使った方法でも実現できます。またこちらのを使えばboldの部分は変数にもできます。
- tree-item(head scriptタグ内)
順番が逆転してしまいますが、あとでこの名前でコンポーネントを定義します。ここでは定義するはずの名前を使って再帰に使う記述になっています。
- $emit
子のコンポーネントからは親のデータを直接変更できないので、$emit(イベント名,パラメータ)で親にイベントを伝播するようにしています。親からはonイベント名="..."というかたちでキャッチできます。その際、子側の第二引数として設定したパラメータが親に渡されます。
- tree-item(body ulタグ内)
コンポーネント利用指定です。子の$emitで発生したイベントを受け取るために@make-folderと@add-itemのリスナを設定し、Vueのmethodに結びつけています。
- treeData
サンプル用の初期値です。nameで名前を指定します。フォルダの場合childrenという名前の配列を持ちその中に子を持ちます。
- Vue.component
ここではコンポーネントをグローバルに設定しています。templeteの箇所で、先のscript type="text/x-template"でidとして指定した"#item-template"を用いることで中身を読みだしています。ちなみにtemplateの部分に直接内容を文字列で記述することもできます。その際改行は使えません。改行を使いたい場合は`(バッククォート)で囲みます。
もしローカルにコンポーネントを設定してしまった場合、再帰時には読み出すことができずエラーとなります。
- computed:
computedでディレクトリの構造が変わった場合に備えています。childrenという配列を持ち、その数が0でないときtrueとなります。
このコードの一番の肝は、最初に分かち書きとして<script type="text/x-template">にある
v-for="(child, index) in item.children"
:item="child"
の部分です。親のdataにバインドしながら再帰構造を形成しています。
バージョン3への変換
上のコードをバージョン3用に変換すると次のようになります。
下線部が修正箇所です。
フォルダ構造表示(V3)
<!DOCTYPE html>
<html>
<head>
<title>Tree View</title>
<script src="https://unpkg.com/vue@next"></script>
<style>
.bold { text-decoration: underline; }
</style>
<script type="text/x-template" id="item-template">
<li>
<div
:class="[isFolder ? 'bold' : '']"
@click="toggle"
@dblclick="makeFolder">
{{ item.name }}
<span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
</div>
<ul v-show="isOpen" v-if="isFolder">
<tree-item
class="item"
v-for="(child, index) in item.children"
:key="index"
:item="child"
@make-folder="$emit('make-folder', $event)"
@add-item="$emit('add-item', $event)"
></tree-item>
<li class="add" @click="$emit('add-item', item)">+</li>
</ul>
</li>
</script>
</head>
<body>
<p>(You can double click on an item to turn it into a folder.)</p>
<ul id="demo">
<tree-item
class="item"
:item="treeData"
@make-folder="makeFolder"
@add-item="addItem"
></tree-item>
</ul>
<script>
// demo data
var treeData = {
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" }]
}
]
}
]
};
var params = {
data: function(){
return { treeData: treeData };
},
methods: {
makeFolder: function(item) {
Vue.set(item, "children", []);
this.addItem(item);
},
addItem: function(item) {
item.children.push({
name: "new stuff"
});
}
}
};
const appli = Vue.createApp(params);
appli.component("tree-item", {
template: "#item-template",
props: {
item: Object
},
data: function() {
return {
isOpen: false
};
},
computed: {
isFolder: function() {
return this.item.children && this.item.children.length;
}
},
methods: {
toggle: function() {
if (this.isFolder) {
this.isOpen = !this.isOpen;
}
},
makeFolder: function() {
if (!this.isFolder) {
this.$emit("make-folder", this.item);
this.isOpen = true;
}
}
}
});
appli.mount("#demo");
</script>
</body>
</html>