Vuetifyの TreeviewをDrag and Drop可能にしたコンポーネントを作った
この記事は suusan2go Advent Calendar 2019 - Adventar の6日目の記事です。
仕事でVuetifyを使っていて、 TreeviewコンポーネントのUIでドラッグアンドドロップ出来るようにする必要があった。Treeviewというのは以下のように、階層構造をもつデータ構造をいい感じにするコンポーネント。
が、以下のIssueにもあるように現状はドラッグアンドドロップをサポートする予定はないとのこと。
There is currently no plan to implement drag & drop functionality
というわけで、作った
作ったもの
こんな感じでコンポーネントを定義してやれば、Treeviewかつドラッグアンドドロップ可能なコンポーネントが使えるようになる。
<template> <v-draggable-treeview group="test" v-model="items" ></v-draggable-treeview> </template> <script> export default { data() { return { items: [{ id: 1, name: "hoge", children: [{ id:11, name: "hoge-child1" }] }] } } } </script>
見ての通り、もともとのTreeviewのUIでドラッグアンドドロップが可能になっている
中身
実装はめちゃくちゃ力技で、オリジナルのTreeviewがコンポーネントが描画するDOM構造をもとに、Vue.Draggable を噛まして子要素をドラッグアンドドロップ可能な作りで組み直している。そのためprops / event / slotも自分が必要だった最低限しか実装していない… もともとはVuetifyのコンポーネントを拡張して作ろうと思ったのだけど、普段使ってないclassコンポーネントを読み解くのが辛かったので、とりあえず仕事上必要になった最低限の仕様をDOM構造だけトレースして実装するにとどめている。
難しかったのは、複数のTreeA、TreeB... のように複数のTreeがあったときに、TreeBのchildrenからTreeAのchildrenに要素を移した場合の挙動で、結論からいうとこれは単純に親にprops / emitしていく形式だとレースコンディションが起きて意図した結果にならない。TreeBのchildrenからTreeAのchildrenに要素を移した場合にprops / emitを上のコンポーネントに伝えていくと以下のようなことが起こる。
root
のコンポーネントで [TreeA, TreeB]
のようなvalueをpropsとして渡されているとすると、全てをprops / emitで処理する場合にはroot
のコンポーネントで以下のような形で emit
を実施したい。
- ①
this.$emit('input', [TreeA, 更新後のTreeB])
- ②
this.$emit('input', [更新後のTreeA, 更新後のTreeB])
しかし実際には①でemitした結果はすぐにpropsとして反映されるわけではないので、 以下のようになりドラッグアンドドロップした要素がどこかに消えてしまう。
- ①
this.$emit('input', [TreeA, 更新後のTreeB])
- ②
this.$emit('input', [更新後のTreeA, TreeB])
というわけで、全て一度localのstateにして受け取って、更新があったらemitするような形式にしている。というか仕事で必要だったのは一つのTree内での並べ替えだけで、ライブラリ化するにあたって全てのTreeで要素を異動できるように変えたんだけども、思いの外これが大変で時間を食ってしまった。
まとめ
コンポーネント化してnpmパッケージ化するにあたっては、クックブックがとても参考になりました。Rollup触るのも初めてだったけど割とシュッとかけてよかった。