Vue3教程:列表过渡

目前为止,关于过渡我们已经讲到:

  • 单个节点
  • 多个节点,每次只渲染一个

那么怎么同时渲染整个列表,比如使用 v-for?在这种场景下,我们会使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag attribute 指定渲染一个元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素总是需要提供唯一的 key attribute 值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

列表的进入/离开过渡

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。

<div id="list-demo">
  <button @click="add">Add</button>
  <button @click="remove">Remove</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" :key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
</div>

JS代码:

const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
      nextNum: 10
    }
  },
  methods: {
    randomIndex() {
      return Math.floor(Math.random() * this.items.length)
    },
    add() {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove() {
      this.items.splice(this.randomIndex(), 1)
    }
  }
}

Vue.createApp(Demo).mount('#list-demo')

CSS代码:

.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
  transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

完整DEMO如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表过渡 | Web176教程(web176.com)</title>
    <style>
        .list-item {
        display: inline-block;
        margin-right: 10px;
        }
        .list-enter-active,
        .list-leave-active {
        transition: all 1s ease;
        }
        .list-enter-from,
        .list-leave-to {
        opacity: 0;
        transform: translateY(30px);
        }
    </style>
</head>
<body>
<div id="list-demo">
    <button @click="add">Add</button>
    <button @click="remove">Remove</button>
    <transition-group name="list" tag="p">
        <span v-for="item in items" :key="item" class="list-item">
        {{ item }}
        </span>
    </transition-group>
</div>
      

<script src='https://unpkg.com/vue@next'></script>
<script>
        const Demo = {
        data() {
            return {
            items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
            nextNum: 10
            }
        },
        methods: {
            randomIndex() {
            return Math.floor(Math.random() * this.items.length)
            },
            add() {
            this.items.splice(this.randomIndex(), 0, this.nextNum++)
            },
            remove() {
            this.items.splice(this.randomIndex(), 1)
            }
        }
        }

        Vue.createApp(Demo).mount('#list-demo')

</script>
</body>
</html>

这个例子有一个问题,当添加和移除元素的时候,周围的元素会瞬间移动到它们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

列表的移动过渡

<transition-group> 组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的 v-move就可以使用这个新功能,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name attribute 来自定义,也可以通过 move-class attribute 手动设置。

这个类主要用于指定过渡时长和缓动效果曲线,如下所示:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

<div id="flip-list-demo">
  <button @click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </transition-group>
</div>
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }
  },
  methods: {
    shuffle() {
      this.items = _.shuffle(this.items)
    }
  }
}

Vue.createApp(Demo).mount('#flip-list-demo')
.flip-list-move {
  transition: transform 0.8s ease;
}

完整DEMO如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表过渡 | Web176教程(web176.com)</title>
    <style>
        .flip-list-move {
        transition: transform 0.8s ease;
        }

    </style>
</head>
<body>
<div id="flip-list-demo">
    <button @click="shuffle">Shuffle</button>
    <transition-group name="flip-list" tag="ul">
        <li v-for="item in items" :key="item">
        {{ item }}
        </li>
    </transition-group>
</div>
      

<script src='https://unpkg.com/vue@next'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script>
        const Demo = {
        data() {
            return {
            items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
            }
        },
        methods: {
            shuffle() {
            this.items = _.shuffle(this.items)
            }
        }
        }

        Vue.createApp(Demo).mount('#flip-list-demo')


</script>
</body>
</html>

这个看起来很神奇,其实 Vue 内部使用了一个叫 FLIP 的动画技术,它使用 transform 将元素从之前的位置平滑过渡到新的位置。

我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="list-complete-demo" class="demo">
  <button @click="shuffle">Shuffle</button>
  <button @click="add">Add</button>
  <button @click="remove">Remove</button>
  <transition-group name="list-complete" tag="p">
    <span v-for="item in items" :key="item" class="list-complete-item">
      {{ item }}
    </span>
  </transition-group>
</div>
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
      nextNum: 10
    }
  },
  methods: {
    randomIndex() {
      return Math.floor(Math.random() * this.items.length)
    },
    add() {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove() {
      this.items.splice(this.randomIndex(), 1)
    },
    shuffle() {
      this.items = _.shuffle(this.items)
    }
  }
}

Vue.createApp(Demo).mount('#list-complete-demo')
.list-complete-item {
  transition: all 0.8s ease;
  display: inline-block;
  margin-right: 10px;
}

.list-complete-enter-from,
.list-complete-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.list-complete-leave-active {
  position: absolute;
}

完整DEMO如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表过渡 | Web176教程(web176.com)</title>
    <style>
        .list-complete-item {
  transition: all 0.8s ease;
  display: inline-block;
  margin-right: 10px;
}

.list-complete-enter-from,
.list-complete-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.list-complete-leave-active {
  position: absolute;
}


    </style>
</head>
<body>
    <div id="list-complete-demo" class="demo">
        <button @click="shuffle">Shuffle</button>
        <button @click="add">Add</button>
        <button @click="remove">Remove</button>
        <transition-group name="list-complete" tag="p">
          <span v-for="item in items" :key="item" class="list-complete-item">
            {{ item }}
          </span>
        </transition-group>
      </div>
      

<script src='https://unpkg.com/vue@next'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script>
        const Demo = {
        data() {
            return {
            items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
            nextNum: 10
            }
        },
        methods: {
            randomIndex() {
            return Math.floor(Math.random() * this.items.length)
            },
            add() {
            this.items.splice(this.randomIndex(), 0, this.nextNum++)
            },
            remove() {
            this.items.splice(this.randomIndex(), 1)
            },
            shuffle() {
            this.items = _.shuffle(this.items)
            }
        }
        }

        Vue.createApp(Demo).mount('#list-complete-demo')



</script>
</body>
</html>

TIPs

需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline。作为替代方案,可以设置为 display: inline-block 或者将元素放置于 flex 布局中。

列表的交错过渡

通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>

<div id="demo">
  <input v-model="query" />
  <transition-group
    name="staggered-fade"
    tag="ul"
    :css="false"
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
  >
    <li
      v-for="(item, index) in computedList"
      :key="item.msg"
      :data-index="index"
    >
      {{ item.msg }}
    </li>
  </transition-group>
</div>
const Demo = {
  data() {
    return {
      query: '',
      list: [
        { msg: 'Bruce Lee' },
        { msg: 'Jackie Chan' },
        { msg: 'Chuck Norris' },
        { msg: 'Jet Li' },
        { msg: 'Kung Fury' }
      ]
    }
  },
  computed: {
    computedList() {
      var vm = this
      return this.list.filter(item => {
        return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
      })
    }
  },
  methods: {
    beforeEnter(el) {
      el.style.opacity = 0
      el.style.height = 0
    },
    enter(el, done) {
      gsap.to(el, {
        opacity: 1,
        height: '1.6em',
        delay: el.dataset.index * 0.15,
        onComplete: done
      })
    },
    leave(el, done) {
      gsap.to(el, {
        opacity: 0,
        height: 0,
        delay: el.dataset.index * 0.15,
        onComplete: done
      })
    }
  }
}

Vue.createApp(Demo).mount('#demo')

完整DEMO如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表过渡 | Web176教程(web176.com)</title>
    <style>
        .list-complete-item {
  transition: all 0.8s ease;
  display: inline-block;
  margin-right: 10px;
}

.list-complete-enter-from,
.list-complete-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.list-complete-leave-active {
  position: absolute;
}


    </style>
</head>
<body>
    <div id="demo">
        <input v-model="query" />
        <transition-group
          name="staggered-fade"
          tag="ul"
          :css="false"
          @before-enter="beforeEnter"
          @enter="enter"
          @leave="leave"
        >
          <li
            v-for="(item, index) in computedList"
            :key="item.msg"
            :data-index="index"
          >
            {{ item.msg }}
          </li>
        </transition-group>
    </div>
      

<script src='https://unpkg.com/vue@next'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
<script>
        const Demo = {
        data() {
            return {
            query: '',
            list: [
                { msg: 'Bruce Lee' },
                { msg: 'Jackie Chan' },
                { msg: 'Chuck Norris' },
                { msg: 'Jet Li' },
                { msg: 'Kung Fury' }
            ]
            }
        },
        computed: {
            computedList() {
            var vm = this
            return this.list.filter(item => {
                return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
            })
            }
        },
        methods: {
            beforeEnter(el) {
            el.style.opacity = 0
            el.style.height = 0
            },
            enter(el, done) {
            gsap.to(el, {
                opacity: 1,
                height: '1.6em',
                delay: el.dataset.index * 0.15,
                onComplete: done
            })
            },
            leave(el, done) {
            gsap.to(el, {
                opacity: 0,
                height: 0,
                delay: el.dataset.index * 0.15,
                onComplete: done
            })
            }
        }
        }

        Vue.createApp(Demo).mount('#demo')




</script>
</body>
</html>

可复用的过渡

过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用的过渡组件,你需要做的就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置其中就可以了。

TODO:使用 Vue3 重构

使用 template 的简单例子:

Vue.component('my-special-transition', {
  template: '\
    <transition\
      name="very-special-transition"\
      mode="out-in"\
      @before-enter="beforeEnter"\
      @after-enter="afterEnter"\
    >\
      <slot></slot>\
    </transition>\
  ',
  methods: {
    beforeEnter(el) {
      // ...
    },
    afterEnter(el) {
      // ...
    }
  }
})

函数式组件更适合完成这个任务:

Vue.component('my-special-transition', {
  functional: true,
  render: function(createElement, context) {
    var data = {
      props: {
        name: 'very-special-transition',
        mode: 'out-in'
      },
      on: {
        beforeEnter(el) {
          // ...
        },
        afterEnter(el) {
          // ...
        }
      }
    }
    return createElement('transition', data, context.children)
  }
})

动态过渡

在 Vue 中即使是过渡也是数据驱动的!动态过渡最基础的例子是通过 name attribute 来绑定动态的值。

<transition :name="transitionName">
  <!-- ... -->
</transition>

当你已经定义了 Vue 的过渡类约定,并希望可以快速切换它们的时候,这会非常有用。

尽管所有过渡 attribute 都可以动态绑定,但我们可用的不只有 attribute。因为事件钩子是方法,它们可以访问任何上下文中的数据,这意味着根据组件状态的不同,你的 JavaScript 过渡可以有不同的表现。

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="dynamic-fade-demo" class="demo">
  Fade In:
  <input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
  Fade Out:
  <input
    type="range"
    v-model="fadeOutDuration"
    min="0"
    :max="maxFadeDuration"
  />
  <transition
    :css="false"
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
  >
    <p v-if="show">hello</p>
  </transition>
  <button v-if="stop" @click="stop = false; show = false">
    Start animating
  </button>
  <button v-else @click="stop = true">Stop it!</button>
</div>
const app = Vue.createApp({
  data() {
    return {
      show: true,
      fadeInDuration: 1000,
      fadeOutDuration: 1000,
      maxFadeDuration: 1500,
      stop: true
    }
  },
  mounted() {
    this.show = false
  },
  methods: {
    beforeEnter(el) {
      el.style.opacity = 0
    },
    enter(el, done) {
      var vm = this
      Velocity(
        el,
        { opacity: 1 },
        {
          duration: this.fadeInDuration,
          complete: function() {
            done()
            if (!vm.stop) vm.show = false
          }
        }
      )
    },
    leave(el, done) {
      var vm = this
      Velocity(
        el,
        { opacity: 0 },
        {
          duration: this.fadeOutDuration,
          complete: function() {
            done()
            vm.show = true
          }
        }
      )
    }
  }
})

app.mount('#dynamic-fade-demo')

看下完整实例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态过渡 | Web176教程(web176.com)</title>
    <style>
        .list-complete-item {
  transition: all 0.8s ease;
  display: inline-block;
  margin-right: 10px;
}

.list-complete-enter-from,
.list-complete-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.list-complete-leave-active {
  position: absolute;
}


    </style>
</head>
<body>

<div id="dynamic-fade-demo" class="demo">
  Fade In:
  <input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
  Fade Out:
  <input
    type="range"
    v-model="fadeOutDuration"
    min="0"
    :max="maxFadeDuration"
  />
  <transition
    :css="false"
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
  >
    <p v-if="show">hello</p>
  </transition>
  <button v-if="stop" @click="stop = false; show = false">
    Start animating
  </button>
  <button v-else @click="stop = true">Stop it!</button>
</div>

      

<script src='https://unpkg.com/vue@next'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script>
        const app = Vue.createApp({
  data() {
    return {
      show: true,
      fadeInDuration: 1000,
      fadeOutDuration: 1000,
      maxFadeDuration: 1500,
      stop: true
    }
  },
  mounted() {
    this.show = false
  },
  methods: {
    beforeEnter(el) {
      el.style.opacity = 0
    },
    enter(el, done) {
      var vm = this
      Velocity(
        el,
        { opacity: 1 },
        {
          duration: this.fadeInDuration,
          complete: function() {
            done()
            if (!vm.stop) vm.show = false
          }
        }
      )
    },
    leave(el, done) {
      var vm = this
      Velocity(
        el,
        { opacity: 0 },
        {
          duration: this.fadeOutDuration,
          complete: function() {
            done()
            vm.show = true
          }
        }
      )
    }
  }
})

app.mount('#dynamic-fade-demo')





</script>
</body>
</html>

最后,创建动态过渡的最终方案是组件通过赋值 prop 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。

作者:terry,如若转载,请注明出处:https://www.web176.com/vue3/6216.html

(0)
打赏 支付宝 支付宝 微信 微信
terryterry
上一篇 2022年7月5日 下午4:03
下一篇 2022年7月6日 上午9:46

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注