とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

Vue.js で Drag & Drop 可能なソート機能な UI を作るには?

概要

Vue.js で drag & drop できる UI が作りたいときどのライブラリを入れてどう作ると良いか調べたのでまとめておく。

TL/DR

Vue.Draggable が一強。迷ったらこれを使っておけばよさそうではある。 ただ、だいたいのライブラリは SortableJS を使って作られており、Vue.Draggable も SortableJSの人たちが作ったものになっている。 無駄な機能を落として柔軟に作りたいなら SortableJS をそのまま使ったコンポーネントを自分で作った方がいい。

Vue.Draggable

Vue.js で言うといまはこれが一強だと思われる。 内部で Sortable.js を使っている。(まあ SortableJS の人たちが作っているし)

draggable コンポーネントで挟み込めば簡単に使うことができ、配下に v-for で繰り返されるコンポーネントを draggable にできる。

      <draggable
        :list="list"
        :disabled="!enabled"
        class="list-group"
        ghost-class="ghost"
        :move="checkMove"
        @start="dragging = true"
        @end="dragging = false"
      >
        <div
          class="list-group-item"
          v-for="element in list"
          :key="element.name"
        >
          {{ element.name }}
        </div>
      </draggable>

少し検索すれば使ってみた記事やサンプルをいくつかみることができるので、参考になるものがゴロゴロ転がっている。

ライブラリになってしまっていたり、配下で繰り返しにならないコンポーネントなどは、適用させるのがちょっと面倒そう。

github.com

その他のライブラリ

vue-sortable

v-sortable をコンポーネントにつければ配下のコンポーネントを draggable なコンポーネントにする、というアイデアは悪くないと思うが、現時点ですでに3年間メンテされていない。ちょっとこれは論外...。

github.com

vue-drag

2年メンテされてなさそうだし、こちらも個人で作ったものと思われる。考えるまでもなく論外。

github.com

element-ui-el-table-draggable

Element-UI の Table を draggable にする特化型のライブラリ。内部で Sortable に自由に option を与えられないのが残念。

github.com

Sortable.js をそのまま使う

ちなみに僕は SortableJS をそのまま利用しています。

github.com

以下のような sortable-wrapper.vue を用意する。

<template>
  <div class="wrapper">
    <slot></slot>
  </div>
</template>

<script>
import Sortable from 'sortablejs';

export default {
  props: {
    selector: {
      type: String,
    },
    option: {
      type: Object,
    },
  },
  data() {
    return {
      sortable: null,
    };
  },
  mounted() {
    // DOM がレンダリングされてから呼ぶために nextTick を使う
    this.$nextTick(this.refresh);
  },
  methods: {
    refresh() {
      if (this.sortable) {
        return;
      }
      if (this.$children.length === 0) {
        return;
      }
      const component = this.$children[0].$el.querySelector(this.selector);
      if (!component) {
        return;
      }
      const option = this.option || {};
      this.sortable = Sortable.create(component, option);
    },
  },
  watch: {
    option() {
      // 変更された option.disabled の内容を反映させる
      this.sortable.option('disabled', this.option.disabled);
    },
  },
};
</script>

以下のように wrapper で挟み込み、selector で class を指定すれば draggable にできるし、option で sortable.js に渡すオプションをそのまま指定できるので柔軟にできる。 wrapper 側の watch で option の更新もできる。

          <sortable-wrapper
            :selector="selector"
            :option="option"
          >
            <div 
              v-for="item in list" 
              :key="item.id"
              class="list" >
              {{ item.name }}
            </div>
          </sortable-wrapper>

気をつけること

ただし、sortableJS はあくまで 実DOM レベルで sortable にするライブラリなので、 vue の仮想DOM レベルでの要素との互換性を保つ必要がある。

sort されるイベントで Vue のデータを書き換えて、データの書き換えに応じて再レンダリングすれば基本的には問題ないはず。

しかしながら、場合によっては例えば以下のように onUpdate で DOM を書き換えるコードを記述するとともに、

...
        onUpdate: event => {
          // sortable.js は vue に対応してないので実DOMを更新する
          const fatherNode = event.from;
          const node = event.item;
          const position = event.newIndex;
          const refNode =
            position === 0
              ? fatherNode.children[0]
              : fatherNode.children[position - 1].nextSibling;
          fatherNode.insertBefore(node, refNode);
        },

vue で持っている data を更新も必要になってしまうことがありうるかもしれない。

          onEnd: ({ newIndex, oldIndex }) => {
           const rearranged = Array.from(targetArray);
            const target = rearranged.splice(oldIndex, 1)[0];
            rearranged.splice(newIndex, 0, target);
            this.targetArray = rearranged;
          },

まとめ

Vue.Draggable は便利なのでちゃっと使うには良さそう。 ただもう少し柔軟に作りたいなら sortable.js で自前の wrapper を用意する手もある、と思う。


Vue.js を勉強するなら 後悔しないためのVueコンポーネント設計