SortableJS / Vue.Draggable

Vue drag-and-drop component based on Sortable.js
https://sortablejs.github.io/Vue.Draggable/
MIT License
20.11k stars 2.9k forks source link

Moving components around & updating vuex just once. #172

Closed patrickdavey closed 7 years ago

patrickdavey commented 7 years ago

Hi there,

Thanks so much for this component, massively useful!

I'm trying to use Vue.Draggable to manipulate a nested tree structure. I have used normalizr to normalize my tree, and I can update the components in the store as I drag them around. I made a fiddle here.

My issue is that when moving a component between children, the set method is called twice. It totally makes sense that it's called twice, but, as I want to update an API I want to do it as one atomic move. Hopefully some code will make my question clear:

My main component has computed methods like this which do the updating:

  computed: {
    children: {
      get() {
        if( this.container.nodes) {
          return this.container.nodes.map((node_id) => this.$store.state.nodes[node_id]);
        } else {
          return [];
        }
      },
      set(value) {
       // this gets called twice when I move a node between containers.
       // which is fair enough, but I want to only post once to my API endpoint, so that the complete
       // move is atomic.
        let newOrder = value.filter(function(n) { return n != undefined }).map(function(v) { return v.id } )
        this.$store.commit('SET_NODES_FOR_CONTAINER', {
          containerID: this.container.id,
          nodes: newOrder
        });
      }
    }
  },

Now, my plan (possibly a bad plan) is that any time there's a move event I want to persist the move back to an API endpoint. The thing is, I don't really want to send the entire structure back and forth, just a call with the containerID and and nodes list. The issue is I get two of the set(value) calls, one for the container which had the child moved out of it, and one for where the child got moved into. I want to capture these two events and either do the complete move, or abort.

Is there a way to do this with Vue.Draggable ? Is this a bad approach ?

David-Desmaisons commented 7 years ago

You should listen to the end event and send data to the server only when it fires

patrickdavey commented 7 years ago

Thanks for the quick response! ok I'll do that, probably store up the changes which would otherwise get saved and then persist the lot (or send the entire tree). Thanks very much.

patrickdavey commented 7 years ago

I tried that, and the end event is only fired if I move components internally within their own list.. if I move it from one list to another it's not getting fired (possibly something broken in the way I'm doing things). The save is getting called correctly though.

new js fiddle

<template>
  <draggable element="ul" v-model="children" :options="{group:'people'}" @end="end" :move="move">
    <li class="list-group-item" v-for="child in children">
      <template v-if="child.nodes">
        <part-container :container="child"/>
      </template>
      <template v-else>
        <document-problem :problem="child"/>
      </template>
    </li>
  </draggable>
</template>

<script>

import draggable from 'vuedraggable'
import DocumentProblem from './Document-Problem.vue'

export default {
  name: 'part-container',
  props: ['container'],
  components: { draggable, DocumentProblem },
  beforeCreate: function () {
    this.$options.components.PartContainer = require('./Part-Container.vue')
  },
  computed: {
    hasChildren: function () {
      return this.children.length > 0;
    },

    children: {
      get() {
        if( this.container.nodes) {
          return this.container.nodes.map((node_id) => this.$store.state.nodes[node_id]);
        } else {
          return [];
        }
      },
      set(value) {
        // value is the new list, but we just want to persist updated ids.
        let newOrder = value.filter(function(n) { return n != undefined }).map(function(v) { return v.id } )
        console.log("save called")
        console.log(newOrder);
        console.log(this.container.id);
        this.$store.commit('SET_NODES_FOR_CONTAINER', {
          containerID: this.container.id,
          nodes: newOrder
        });
      }
    }
  },

  methods: {
    move: function(evt) {
      console.log(`move: ${this.container.id}`)
    },
    end: function(evt) {
      console.log(`end: ${this.container.id}`)
    }
  }
}

</script>

<style>
.flip-list-move {
  transition: transform 0.5s;
}

.no-move {
  transition: transform 0s;
}

.ghost {
  opacity: .5;
  background: #C8EBFB;
}

.list-group {
  min-height: 20px;
}

.list-group-item {
  cursor: move;
}

.list-group-item i{
  cursor: pointer;
}
</style>
David-Desmaisons commented 7 years ago

End event is called always. please use the vue debugtools to check that this event is trigerred,

patrickdavey commented 7 years ago

Very interesting. I did use the debug tools, and I can see that the end event is emitted. What's interesting is that I do not see my little console.log above being triggered.

I pushed up my toy repo and I'll continue looking into it. The issue I'm having is easy to reproduce, just drag one of the nested elements to an outside it's box, you'll see the end event is emitted, but that the console.log isn't triggered.

I'm sure it's something silly I'm doing (I'm very new to vue, and this is frankly the most complex client side functionality I've tried to build!) but still, I can't quite see the issue.

I'm going to try work out how to listen for emitted events, and see if I can hook in that way.

patrickdavey commented 7 years ago

I think the issue I'm having is that I'm dragging the nested draggables around, the components aren't actually wired up to "know" about the parent-child relationships, so the end event is being emitted but there's no parent around to actually listen to the event.

Is there a way to wire in an event bus or similar so that I can catch the events? Anyway, I'll keep on playing!

patrickdavey commented 7 years ago

Ah, my mistake, I had a component wrapping the entire tree structure, once I listened on that for the end event it all works perfectly.

Thanks again @David-Desmaisons for a fantastic library!!