Vue.Draggableの子要素内におけるドラッグ＆ドロップで気をつけたいこと

Vue.Draggable大変便利です。いつもお世話になっております。
https://github.com/SortableJS/Vue.Draggable

今日は子要素内におけるドラッグ＆ドロップ(以下D&D)時に自分が嵌まってしまった点についての備忘録です。

## 前置き

- プロジェクト作成
- 親子コンポーネントの作成
- App.vueの修正
- 実行

### プロジェクト作成

さくっとvuecliでプロジェクトを作成します。設定内容はdefaultです。

```shell

$ vue create my-vue-draggable
```

続いて、vuedraggableのインストール

```shell

$ npm i -S vuedraggable
```

### 親子コンポーネントの作成

componentsディレクトリ下にParent.vueとChild.vueを作成します。

```src/components/Parent.vue
<template>
    <div>
        <h3>list1を表示</h3>
        <draggable v-model="list1" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list1" :key="item">{{ item }}</div>
        </draggable>

        <h3>list2を表示</h3>
        <draggable v-model="list2" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list2" :key="item">{{ item }}</div>
        </draggable>

        <h3>listForChildを表示</h3>
        <Child :list-from-parent="listForChild" v-on:update="listForChild = $event;"></Child>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

import Child from '@/components/Child.vue'

export default {
    name: "Parent",
    components: {
        draggable,
        Child
    },
    data: ()=> {
        return {
            list1: ['task1-1', 'task1-2', 'task1-3', 'task1-4'],
            list2: ['task2-1', 'task2-2', 'task2-3', 'task2-4'],
            listForChild: ['task3-1']
        }
    },
}
</script>

<style>
.dev_flex {
    display: flex;
}

.dev_setting-area {
    color: #fff;
    border: 6px double #fff;
    background: #464646;
    border-radius: 10px;
    margin: .5rem;
    padding: .5rem;
    height: 16mm;
}

.dev_back {
    margin: .1em;
    width: 15mm;
    height: 15mm;
    text-decoration: none;
    color: #67c5ff;
    border: solid 1px #67c5ff;
    border-radius: 3px;
    transition: .4s;
}
</style>
```

```src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    computed: {
        list3: {
            get: function () {
                return this.listFromParent.concat();
            },
            set: function (list) {
                this.$emit('update', list);
            }
        },
    },
}
</script>
```

### App.vueの修正

Parent.vueを表示するように少し修正します。

```src/App.vue
<template>
  <div id="app">
    <Parent></Parent>
  </div>
</template>

<script>
import Parent from '@/components/Parent.vue'

export default {
  name: 'App',
  components: {
    Parent
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

### 実行

ここまでで一旦起動する準備ができたので、実行します。

```shell
npm run serve
```

ローカルホスト( http://localhost:8080/ )へアクセスして下記が表示されればOKです。

<img width="463" alt="スクリーンショット 2020-03-19 19.44.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58725/00a6ff00-3011-2f89-f57b-7ebfaf0ebb18.png">

下記は適当にD&Dを行った結果です。

<img width="465" alt="スクリーンショット 2020-03-19 19.46.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58725/fe5277db-0c97-f99b-10f5-69cb3390cb4a.png">

親同士や親子間でのD&Dが問題なく成功します。

## 本題

続いて、子要素内にもう1つリストを設けます。
このときリストには下記の制限を設けます。

- 既存のlist3にはtaskの末尾が1or2のものを表示する
- 新規のlist4にはtaskの末尾が3or4のものを表示する
- 親が持つlistForChildは増やさない

こちらを踏まえて、Child.vueを変更します。

```src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>

        <h4>list4を表示</h4>
        <draggable v-model="list4" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list4" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    computed: {
        list3: {
            get: function () {
                return this.listFromParent.concat().filter((task)=>{
                    const lastword = task.slice(-1);
                    return lastword === '1' || lastword === '2';
                });
            },
            set: function (list) {
                this.$emit('update', this.list4.concat(list));
            }
        },
        list4: {
            get: function () {
                return this.listFromParent.concat().filter((task)=>{
                    const lastword = task.slice(-1);
                    return lastword === '3' || lastword === '4';
                });
            },
            set: function (list) {
                this.$emit('update', this.list3.concat(list));
            }
        },
    },
}
</script>
```

再度実行し、ブラウザで確認します。

<img width="461" alt="スクリーンショット 2020-03-19 19.54.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58725/3c6fabbf-3fdc-229e-ac88-549da95928d5.png">

親から子へ適当にD&Dを行います。このときlist3へ末尾が3,4のものを送っても、list4に表示され、list4へ末尾が1,2のものを送ってもlist3に表示されます。

<img width="465" alt="スクリーンショット 2020-03-19 19.56.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58725/b825b6af-5117-cbd8-3e52-283de4824124.png">

ただ、子要素内にてD&Dを行うと要素が削除されます。
本来であればD&Dしても格納されているlistに変化がないことが期待動作です。

<img width="458" alt="スクリーンショット 2020-03-19 19.58.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58725/fbee07dc-e946-4023-6c61-f031e4314c5a.png">

## 原因(憶測)と解決策

親要素へのemitが別々のcomputedから同時に発火されるとこのような現象が発生するようです。根拠となるissueなどが見つからなかったので、憶測ではありますが。。。

Child.vueを修正します。具体的にはcomputedではなくwatchを使います。

```src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>

        <h4>list4を表示</h4>
        <draggable v-model="list4" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list4" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    watch: {
        listFromParent: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            this.list3 = this.listFromParent.filter((task) => {
                const lastword = task.slice(-1);
                return lastword === '1' || lastword === '2';
            });
            this.list4 = this.listFromParent.filter((task) => {
                const lastword = task.slice(-1);
                return lastword === '3' || lastword === '4';
            });
        },
        list3: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            if (newlist.length === oldlist.length) {
                return;
            }
            this.$emit('update', newlist.concat(this.list4));
        },
        list4: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            if (newlist.length === oldlist.length) {
                return;
            }
            this.$emit('update', newlist.concat(this.list3));
        },
    },
    data: ()=> {
        return {
            list3: [],
            list4: [],
        }
    },
}
</script>
```

これで子要素内におけるD&Dが実現できました。

なお、watchを使う際の注意点として、watchのlist3,list4にて次の記述があります。

```js
            if (newlist.length === oldlist.length) {
                return;
            }
```

list内に変更がない場合はその後の処理を行わない制御となっております。
こちらがないと無限ループに陥るため、お気をつけください。。。（←実際やった人）