CSS transition ignored when array order in v-for changes

2

I'm making a component where I can drag other components vertically. When one component passes the other, they must switch places (on the X axis).

What happens is that the element I'm dragging respects the CSS transition, slides to the next position, but the other does not. It jumps directly to the new position.

What is the reason for this and how to correct it?

Vue.component('dragable', {
    props:  ['config', 'leftPosition'],
    template: '#dragable',
    data() {
        return {
            elementStartCoords: null,
            pointerTouchDown: null,
            pointerUpHandler: this.onPointerUp.bind(this),
            pointerMoveHandler: this.onPointerMove.bind(this)
        }
    },
    computed: {
        style() {
            return {
                top: this.config.top + 'px',
                left: this.leftPosition * 150 + 'px'
            }
        }
    },
    methods: {
        getPointerCoordinates(e) {
                return {
                    y: e.pageY || e.originalEvent.touches[0].pageY
                };
            },
            getElementPosition(el) {
                const coords = el.getBoundingClientRect();
                return {
                    y: coords.top
                };
            },

            onPointerDown(e) {
                this.elementStartCoords = this.getElementPosition(this.$el);
                this.pointerTouchDown = this.getPointerCoordinates(e);

                window.addEventListener('mouseup', this.pointerUpHandler);
                window.addEventListener('mousemove', this.pointerMoveHandler);
            },
            onPointerMove(e) {
                e.stopPropagation();
                if (!this.pointerTouchDown) return;

                const pointerPosition = this.getPointerCoordinates(e);
                const yDiff = pointerPosition.y - this.pointerTouchDown.y;

                this.$emit('dragevent', {
                    name: this.config.name,
                    top: this.elementStartCoords.y + yDiff
                });
            },
            onPointerUp() {
                window.removeEventListener('mouseup', this.pointerUpHandler);
                window.removeEventListener('mousemove', this.pointerMoveHandler);

                // reset used variables
                this.pointerTouchDown = null;
                this.elementStartCoords = null;
            },
    }
})

new Vue({
    el: '#app',
    data() {
        return {
            draggables: [{
                name: 'A',
                top: 70
            }, {
                name: 'B',
                top: 0
            }]
        }
    },
    methods: {
        handleDrag(config) {
            const draggable = this.draggables.find(drg => drg.name == config.name);
            draggable.top = config.top;
            this.draggables = this.draggables.sort((a, b) => b.top - a.top);
        }
    }
})
.dragable {
	user-select: none;
    position: absolute;
    background-color: #dde;
    border: 2px solid #88A;
    border-radius: 5px;
    padding: 30px;
    cursor: pointer;
	transition: left 1s;
}
<script src="https://vuejs.org/js/vue.min.js"></script><divid="app">
    <p>Dragable test</p>
    <template v-for="(draggable, i) in draggables">
        <dragable :key="draggable.name" :config="draggable" :left-position="i" @dragevent="handleDrag"></dragable>
    </template>
</div>

<template lang="html" id="dragable">
    <div class="dragable" :style="style" @mousedown="onPointerDown" draggable="false">{{'Draggable ' + config.name}}</div>
</template>
    
asked by anonymous 16.09.2017 / 21:46

1 answer

1

The problem was that .sort() was changing the position of elements in v-for and this made Vue think that it was new elements. Setting the position to the left otherwise solves the problem:

Vue.component('dragable', {
  props: ['config', 'leftPosition'],
  template: '#dragable',
  data() {
    return {
      elementStartCoords: null,
      pointerTouchDown: null,
      pointerUpHandler: this.onPointerUp.bind(this),
      pointerMoveHandler: this.onPointerMove.bind(this)
    }
  },
  computed: {
    style() {
      return {
        top: this.config.top + 'px',
        left: this.leftPosition * 150 + 'px'
      }
    }
  },
  methods: {
    getPointerCoordinates(e) {
      return {
        y: e.pageY || e.originalEvent.touches[0].pageY
      };
    },
    getElementPosition(el) {
      const coords = el.getBoundingClientRect();
      return {
        y: coords.top
      };
    },

    onPointerDown(e) {
      this.elementStartCoords = this.getElementPosition(this.$el);
      this.pointerTouchDown = this.getPointerCoordinates(e);

      window.addEventListener('mouseup', this.pointerUpHandler);
      window.addEventListener('mousemove', this.pointerMoveHandler);
    },
    onPointerMove(e) {
      e.stopPropagation();
      if (!this.pointerTouchDown) return;

      const pointerPosition = this.getPointerCoordinates(e);
      const yDiff = pointerPosition.y - this.pointerTouchDown.y;

      this.$emit('dragevent', {
        name: this.config.name,
        top: this.elementStartCoords.y + yDiff
      });
    },
    onPointerUp() {
      window.removeEventListener('mouseup', this.pointerUpHandler);
      window.removeEventListener('mousemove', this.pointerMoveHandler);

      // reset used variables
      this.pointerTouchDown = null;
      this.elementStartCoords = null;
    },
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      draggables: [{
        name: 'A',
        top: 70,
        left: 0
      }, {
        name: 'B',
        top: 0,
        left: 1
      }]
    }
  },
  methods: {
    handleDrag(config) {
      const draggable = this.draggables.find(drg => drg.name == config.name);
      draggable.top = config.top;
      const positions = this.draggables.slice().sort((a, b) => b.top - a.top).reduce((obj, drg, i) => (obj[drg.name] = i, obj), {});
      this.draggables = this.draggables.map(drg => {
        return {
          ...drg,
          left: positions[drg.name]
        }
      });
    }
  }
})
.dragable {
  user-select: none;
  position: absolute;
  background-color: #dde;
  border: 2px solid #88A;
  border-radius: 5px;
  padding: 30px;
  cursor: pointer;
  transition: left 1s;
}
<script src="https://vuejs.org/js/vue.min.js"></script><divid="app">
  <p>Dragable test</p>
  <template v-for="draggable in draggables">
        <dragable :key="draggable.name" :config="draggable" :left-position="draggable.left" @dragevent="handleDrag"></dragable>
    </template>
</div>

<template lang="html" id="dragable">
    <div class="dragable" :style="style" @mousedown="onPointerDown" draggable="false">{{'Draggable ' + config.name}}</div>
</template>
    
10.11.2017 / 22:15