LoginSignup
3
3

More than 3 years have passed since last update.

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

Posted at

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

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

前置き

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

プロジェクト作成

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


$ vue create my-vue-draggable

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


$ 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>

実行

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

npm run serve

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

スクリーンショット 2020-03-19 19.44.03.png

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

スクリーンショット 2020-03-19 19.46.05.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>

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

スクリーンショット 2020-03-19 19.54.16.png

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

スクリーンショット 2020-03-19 19.56.28.png

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

スクリーンショット 2020-03-19 19.58.04.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にて次の記述があります。

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

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3