menu
書いてる野郎
orebike@gmail.com
各要素に対してリネーム機能を入れてみようと思う 全部の要素に対してまず rename ボタンを設置しよう
var File = Vue.extend({ template: ` <li class="file"> {{ item.name }} <button type="button">rename</button> </li>`, store: store, props: ['item'] }); var Folder = File.extend({ template: ` <li v-bind:class="['folder', {close: !item.isOpen}]"> <h2 v-on:click="toggleOpenClose">{{ item.name }}</h2> <button type="button">rename</button> <ul> <li v-for="(aitem, i) in item.items"> <component v-bind:is="aitem.type" v-bind:item="aitem"></component> </li> </ul> </li>`, methods: { toggleOpenClose: function(){ this.$store.dispatch("toggleOpenClose", this.item.id); } } });
単にボタンを設置してみた。 コンポーネント化されているので非常に簡単に設置できた。
次にクリックすると名前の部分が入力フォームになり、確定とキャンセルボタンが出るようにしてみよう。 このような状態もデータとしてコントロールすると考える。
1度に1つしか編集できないと考えるなら、データは全体で1個となる ここにデータを追加する。
var data = { id: 1, name: "", type: "folder", isOpen: true, // 中略 editId: null };
根っこのケツに editId というモノを追加。ここに id が入っている対象が編集中ということにする。
それでは自分自身が編集対象かを調べるメソッドを作っておく
このようになる。
var File = Vue.extend({ // 中略 computed: { isEdit: function(){ return this.$store.state.editId === this.item.id } } }); var RootFolder = File.extend({ // 中略 computed: { isEdit: function(){ return false; } } }); var Folder = File.extend({ // 中略 });
store の情報と自分自身が一致しているのかを確かめる。 RootFolder は編集が無いので上書きして常に false へ。 Folder は File を継承しているので実装しなくていい。
ではこれを画面に表示してみよう
<li class="file"> {{ item.name }} <button type="button">rename</button> {{ isEdit }} </li>`,
Folder にも同様に追加する。そうするとキレイに全部 false で表示される。
手で editId を書き換えてみると、うまく該当のモノだけ true と表示されることがわかる。
ではこいつの mutation を書く
mutations: { // 中略 updateEditId: function(state, id){ state.editId = id; } }
今回は単純に値を書き換えるだけなのでそのまんまである。
ではこいつをキックする method を File に作る。
var File = Vue.extend({ // 中略 methods: { startEdit: function(){ this.$store.commit('updateEditId', this.item.id); } } });
ではこいつを追加したボタンに紐付ける
<button type="button" v-on:click="startEdit">rename</button>
これまた便利なのは Folder は File を継承しているので v-on をつけるだけでよいということである。
そして動かしてみると、見事にクリックした対象だけが true に切り替わる。 ツリーの中全体で true が1個しか無いようにうまく制御できている。
自分は Vue と Vuex の流儀にしたがって必要なモノを淡々と順番に実装しているだけなのに、うまく全体統合されている。 設計がうまくなったと錯覚するようだ。これがフレームワークのちからである。
それではこの isEdit の結果に従って input 要素と OK Cancel のボタンを実装してみる。
こうする。
<li class="file"> <template v-if="isEdit"> <input type="text" v-bind:value="item.name" /> <button type="button">OK</button> <button type="button">Cancel</button> </template> <template v-else> {{ item.name }} <button type="button" v-on:click="startEdit">rename</button> {{ isEdit }} </template> </li>
動作させるとうまく切り替わると思う。input に既存の名前を入れるために value に値をデータバインディングしている。
それでは OK ボタンを機能させてみる まず名前を update するメソッドを作る。
当然作る場所は mutations である。
mutations: { // 中略 updateName: function(state, {getters, id, name}){ var item = getters.findById(id); if(item == null){ return; } item.name = name; } },
今回はデータ特定しないとイケナイ系なので getters をもらってきている。 当然これは action 経由での呼び出しとなるので action も実装する。
actions: { // 中略 updateName: function(context, {id, name}){ context.commit('updateName', {getters: context.getters, id: id, name: name}); } }
こいつをコールする method を File に実装する。 しかし、新しい名前を得たいが新しい名前を得る手段が無い。
新しい名前は、一時的な使い捨ての値で全体で管理すべき値でもないので data に保持することにする。
var File = Vue.extend({ // 中略 data: function(){ return { newName: "" }; }, });
File はコンストラクタであるので、 data の定義にはその内容を返す関数を設定する。こうすることでコンポーネントはそれぞれの data を持てることになる。
値もとりあえず初期化してしまおう
var File = Vue.extend({ // 中略 methods: { startEdit: function(){ this.$store.commit('updateEditId', this.item.id); this.newName = this.item.name; } } });
それでは入力の値をデータ側に反映させるように記述する。 現状では、 name の値が value にバインディングされているのだが、これは一方通行で、value の値が変わったからといって、name が変わるわけではない
change のイベントはフォーカスアウトしたとき(入力が完了して他のボタンを押した時)に発火する。
<input type="text" v-bind:value="item.name" v-on:change="setNewName($event.target.value)"/>
これでよい。
そしたら OK ボタンに更新メソッドをくっつけよう
更新メソッドは data から取れる名前を渡せるようになった
methods: { // 中略 finishEdit: function(){ this.$store.commit('updateEditId', null); }, updateName: function(){ this.$store.dispatch('updateName', { id: this.item.id, name: this.newName }); this.finishEdit(); } }
更新と同時に、更新は終了するわけだから、そのメソッドも取り付けておく。
最後にボタンにフックすればOK。
<button type="button" v-on:click="updateName">OK</button>
キャンセルは簡単で単に finish すればいい
<button type="button" v-on:click="finishEdit">Cancel</button>
同様にフォルダのテンプレートにも実装すればよい
<!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" } ], editId: 5 }; 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; }, updateEditId: function(state, id){ state.editId = id; }, updateName: function(state, {getters, id, name}){ var item = getters.findById(id); if(item == null){ return; } item.name = name; } }, 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}); }, updateName: function(context, {id, name}){ context.commit('updateName', {getters: context.getters, id: id, name: name}); } } }); var File = Vue.extend({ template: ` <li class="file"> <template v-if="isEdit"> <input type="text" v-bind:value="item.name" v-on:change="setNewName($event.target.value)"/> <button type="button" v-on:click="updateName">OK</button> <button type="button" v-on:click="finishEdit">Cancel</button> </template> <template v-else> {{ item.name }} <button type="button" v-on:click="startEdit">rename</button> {{ isEdit }} </template> </li>`, store: store, props: ['item'], data: function(){ return { newName: "" }; }, computed: { isEdit: function(){ return this.$store.state.editId === this.item.id } }, methods: { startEdit: function(){ this.$store.commit('updateEditId', this.item.id) }, finishEdit: function(){ this.$store.commit('updateEditId', null); }, updateName: function(){ this.$store.dispatch('updateName', { id: this.item.id, name: this.newName }); this.finishEdit(); }, setNewName: function(v){ console.log(v); this.newName = v; } } }); var RootFolder = File.extend({ template: ` <ul> <li v-for="(aitem, i) in items"> <component v-bind:is="aitem.type" v-bind:item="aitem"></component> </li> </ul>`, props: ['items'], computed: { isEdit: function(){ return false; } } }); var Folder = File.extend({ template: ` <li v-bind:class="['folder', {close: !item.isOpen}]"> <template v-if="isEdit"> <input type="text" v-bind:value="item.name" v-on:change="setNewName($event.target.value)"/> <button type="button" v-on:click="updateName">OK</button> <button type="button" v-on:click="finishEdit">Cancel</button> </template> <template v-else> <h2 v-on:click="toggleOpenClose">{{ item.name }}</h2> <button type="button" v-on:click="startEdit">rename</button> {{ isEdit }} </template> <ul> <li v-for="(aitem, i) in item.items"> <component v-bind:is="aitem.type" v-bind:item="aitem"></component> </li> </ul> </li>`, methods: { toggleOpenClose: function(){ this.$store.dispatch("toggleOpenClose", this.item.id); } } }); Vue.component("file", File); Vue.component("root-folder", RootFolder); Vue.component("folder", Folder); var vm = new Vue({ el: "#app", store: store, computed: { rootItems: function(){ return this.$store.state.items; } } }); </script> </body> </html>