menu
書いてる野郎
orebike@gmail.com
今まで全体を1個の Vueオブジェクトで管理してきたが、それを分割管理してみる。 コンポーネントと呼ばれるモノである。
分割管理しても store は1個は変わらない、そのロジックもほぼすべて store に存在するので、 つまり template の分割と言ってヨイ。
分割したいのは li からの一連の流れである。
コンポーネントというのはこのように作成する。 作成用のメソッドにコンポーネント名に続き、設定値を渡している。
Vue.component("folder", { template: `<li class="folder"><h2>folder</h2></li>`, store: store });
作成するというか登録するという感じである。
そこに Vueオブジェクトのコンストラクタとほぼ同等のモノを渡すことになる。 Vueオブジェクトを再利用したいという目的なので、パラメータが似ているのはそのままである。
違うのは el が無いこと。 これはコンポーネントというのが再利用を目的としていて、そもそも対象が決めれないからである。
単なるテンプレート化時と同様に一段目から揃えてみよう コンポーネントを2つ登録する。store を登録しているということは当然ながら store を作成した後に登録している。
Vue.component("folder", { template: `<li class="folder"><h2>folder</h2></li>`, store: store }); Vue.component("file", { template: '<li class="file">file</li>', store: store });
これを元のテンプレートに組み込んでみる
<div id="app"> <ul> <li v-for="(item, i) in rootItems"> <template v-if="item.type === 'folder'"> <folder></folder> </template> <template v-else> <file></file> </template> </li> </ul> </div>
そして動作させてみると、何かコンポーネントで登録したテンプレートらしきものが4つハマっていることが確認できる。 埋め込んでいる HTMLでもなんでもないタグは登録したコンポーネント名と対応している。
これでは名前がわからないので、名前を表示したいのだが、コンポーネントは store を持っているが、自分がどのデータの担当かは知ることができない。
コンポーネントはタグからそのパラメータを内部に埋め込むことができるので、こいつを利用する。
props というパラメータに、受け取りたい名前を列挙する
では受け取ってそのままタグ経由でパラメータを渡してしまう。渡したパラメータはその名前でそのまま内部のテンプレートで使えるようになる。
なのでこのように書く
Vue.component("folder", { template: `<li class="folder"><h2>{{ item.name }}</h2></li>`, store: store, props: ['item'] }); Vue.component("file", { template: '<li class="file">{{ item.name }}</li>', store: store, props: ['item'] });
そしたら対象のコンポーネントのタグから値を送り込んでやる。
<div id="app"> <ul> <li v-for="(item, i) in rootItems"> <template v-if="item.type === 'folder'"> <folder v-bind:item="item"></folder> </template> <template v-else> <file v-bind:item="item"></file> </template> </li> </ul> </div>
HTML ではないが属性に対して値を引き込むので v-bind で指定する。
動作させると、ちゃんと反映されていることがわかる。
では、これを配下にも適用する。
コンポーネントからコンポーネント呼び出し、しかも自分自身。
Vue.component("folder", { template: ` <li class="folder"> <h2>{{ item.name }}</h2> <ul> <li v-for="(aitem, i) in item.items"> <template v-if="aitem.type === 'folder'"> <folder v-bind:item="aitem"></folder> </template> <template v-else> <file v-bind:item="aitem"></file> </template> </li> </ul> </li>`, store: store, props: ['item'] });
そして実行してみると・・・ちゃんと下の階層まで表示されるのだ。
これ、自分から自分を呼ぶ再帰構造になっているということ、 つまり、これで何階層でもできるという構造になったというわけだ。
では根っこの要素もコンポーネント化して
Vue.component("root-folder", { template: ` <ul> <li v-for="(aitem, i) in items"> <template v-if="aitem.type === 'folder'"> <folder v-bind:item="aitem"></folder> </template> <template v-else> <file v-bind:item="aitem"></file> </template> </li> </ul>`, store: store, props: ['items'] });
最終的元のテンプレートが、こうなる
<div id="app"> <root-folder v-bind:items="rootItems"></root-folder> </div>
シンプル。
基本構造がコンポーネント分割できたので、開閉挙動もコンポーネントで実現しよう 開閉挙動はフォルダしかしないのでフォルダにメソッドを実装すればよい。移植する。
もうひとつあるのは props によって値を上からもらっているので、引数で渡す必要がなくなる。
Vue.component("folder", { // 中略 methods: { toggleOpenClose: function(){ this.$store.dispatch("toggleOpenClose", this.item.id); } } });
開閉の見た目はクラスによって制御されていたので、クラスの付与の仕組みも移植する。 フォルダ限定で書けるようになっているので、以前より記述が楽になっているのがわかる。
Vue.component("folder", { template: ` <li v-bind:class="['folder', {close: !item.isOpen}]"> <h2 v-on:click="toggleOpenClose">{{ item.name }}</h2> <ul> <li v-for="(aitem, i) in item.items"> <template v-if="aitem.type === 'folder'"> <folder v-bind:item="aitem"></folder> </template> <template v-else> <file v-bind:item="aitem"></file> </template> </li> </ul> </li>`, // 略 })
さっきからさらりと書いているが新し目のJSではバッククォートで挟むと複数行の String を作ることができる。
コンポーネント分割できた。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Hello</title> <script src="https://unpkg.com/vue@latest"></script> <script src="https://unpkg.com/vuex@latest"></script> <style> #app li.folder.close ul{ display: none; } </style> </head> <body> <div id="app"> <root-folder v-bind:items="rootItems"></root-folder> </div> <script> var data = { id: 1, name: "", type: "folder", isOpen: true, items: [ { id: 2, name: "aaa", type: "folder", isOpen : false, items: [ { id: 3, name: "bbb", type: "file" }, { id: 4, name: "ccc", type: "file" }, { id: 5, name: "ddd", type: "file" } ] }, { id: 6, name: "eee", type: "file" }, { id: 7, name: "fff", type: "folder", isOpen: false, items: [ { id: 8, name: "ggg", type: "file" }, { id: 9, name: "hhh", type: "folder", isOpen: false, items: [ { id: 10, name: "iii", type: "file" }, { id: 11, name: "jjj", type: "file" } ] } ] }, { id: 12, name: "kkk", type: "file" } ] }; var store = new Vuex.Store({ state: data, mutations: { toggleOpenClose: function(state, {getters, id}){ var item = getters.findById(id); if(item == null){ return; } if(item.type !== 'folder'){ return; } item.isOpen = !item.isOpen; }, }, getters: { findById: (state, getters) => (_id, _item) => { if(_item == null){ return getters.findById(_id, state); } if(_item.id === _id){ return _item; } if(_item.type === 'file'){ return null; } for(var i = 0; i < _item.items.length; i++){ var tmp = getters.findById(_id, _item.items[i]); if(tmp != null){ return tmp; } } return null; } }, actions: { toggleOpenClose: function(context, id){ context.commit('toggleOpenClose', {getters: context.getters, id: id}); } } }); Vue.component("root-folder", { template: ` <ul> <li v-for="(aitem, i) in items"> <template v-if="aitem.type === 'folder'"> <folder v-bind:item="aitem"></folder> </template> <template v-else> <file v-bind:item="aitem"></file> </template> </li> </ul>`, store: store, props: ['items'] }); Vue.component("folder", { template: ` <li v-bind:class="['folder', {close: !item.isOpen}]"> <h2 v-on:click="toggleOpenClose">{{ item.name }}</h2> <ul> <li v-for="(aitem, i) in item.items"> <template v-if="aitem.type === 'folder'"> <folder v-bind:item="aitem"></folder> </template> <template v-else> <file v-bind:item="aitem"></file> </template> </li> </ul> </li>`, store: store, props: ['item'], methods: { toggleOpenClose: function(){ this.$store.dispatch("toggleOpenClose", this.item.id); } } }); Vue.component("file", { template: '<li class="file">{{ item.name }}</li>', store: store, props: ['item'] }); var vm = new Vue({ el: "#app", store: store, computed: { rootItems: function(){ return this.$store.state.items; } } }); </script> </body> </html>