CSS transition ignored when the array order in v-for changes

Asked

Viewed 55 times

3

I’m making a component where I can drag other components vertically. When one component passes the other they must change 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 one doesn’t. He jumps directly into the new position.

What is the reason for this and how to correct?

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>

<div id="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>

1 answer

3


The problem was that the .sort() was changing the position of the elements in the v-for and this made Vue think that these were new elements. Configuring the left position 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>

<div id="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>

Browser other questions tagged

You are not signed in. Login or sign up in order to post.