# Анимация списков

До сих пор управляли переходами для:

  • Одиночных элементов
  • Множества элементов, когда единовременно отображается только 1 элемент

Но как насчёт ситуации, когда есть целый список элементов, который нужно отображать одновременно, например с помощью v-for? В этом случае, надо использовать компонент <transition-group>. Прежде чем перейдём к примеру, нужно отметить несколько важных моментов о нём:

  • По умолчанию он не отрисовывает элемент обёртку, но можно указать какой элемент должен быть отрисован с помощью атрибута tag.
  • Режимы переходов недоступны, так как больше не переключаются туда-сюда взаимоисключающие элементы.
  • У каждого элемента внутри <transition-group> всегда должно быть уникальное значение атрибута key.
  • 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>
1
2
3
4
5
6
7
8
9
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

See the Pen Transition List by Vue (@Vue) on CodePen.

В примере есть одна проблема. При добавлении или удалении элемента, окружающие его элементы будут резко прыгать на новое место, вместо плавного перемещения. Исправим эту недоработку чуть позднее.

# Анимация перемещения элементов списка

У компонента <transition-group> есть ещё один козырь в рукаве. Он может анимировать не только появление и удаление элементов, но и их перемещение. Происходит это путём добавления класса v-move, который применяется при изменении позиции элементов. Как и с другими классами, его префикс устанавливается в соответствии с значением атрибута name, но его можно и определить вручную с помощью атрибута move-class.

Им удобно устанавливать тайминги перехода и функцию плавности, как показано ниже:

<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>
1
2
3
4
5
6
7
8
9
10
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.flip-list-move {
  transition: transform 0.8s ease;
}
1
2
3

See the Pen Transition-group example by Vue (@Vue) on CodePen.

Хоть это и выглядит как магия, но «под капотом» Vue использует анимационную технику под названием FLIP (opens new window), которая позволяет плавно перемещать элементы с их старой позиции на новую применяя CSS-трансформации.

Теперь можно совместить эту технику с предыдущим примером, чтобы добавить анимации на любые изменения списка!

<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>
1
2
3
4
5
6
7
8
9
10
11
12
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

See the Pen Transition-group example by Vue (@Vue) on CodePen.

Совет

Запомните, что FLIP-анимации не работают с элементами display: inline. В таких случаях можно воспользоваться display: inline-block или расположить элементы внутри flex-контейнера.

FLIP-анимации не ограничены одной осью. Элементы в многомерных массивах также можно анимировать (opens new window):

# Упругая анимация элементов списка

Управляя JavaScript-переходами через data-атрибуты, можно также организовать упругую анимацию списка:

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

See the Pen Staggered Lists by Vue (@Vue) on CodePen.

# Переиспользование анимаций переходов

Анимации переходов можно переиспользовать благодаря компонентной системе Vue. Всё что нужно сделать — поместить компонент <transition> или <transition-group> в корне компонента, а затем передавать ему любые дочерние компоненты для отображения.

Пример такого шаблонного компонента для переиспользования:

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) {
      // ...
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Особенно хорошо для этой цели подходят функциональные компоненты:

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)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Динамические переходы

Да, даже переходы во Vue могут зависеть от данных! Самый простой пример — привязка атрибута name к свойству.

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

Это удобно, когда заранее определены CSS-переходы или анимации, используя принятые во Vue соглашения об именовании классов, и хочется переключаться между ними.

На самом деле, динамическую привязку можно использовать для любого атрибута перехода. И речь не только об атрибутах. Ведь хуки это просто методы и у них есть доступ ко всем данных в текущем контексте. А значит, в зависимости от состояния компонента, 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

Наконец, ещё больше возможностей для создания динамических переходов будет, если создать компоненты, которые по входным параметрам будут определять каким образом должны работать анимации переходов. Звучит избито, но всё ограничивается лишь полётом воображения.

Deployed on Netlify.
Последнее обновление: 2021-01-09, 17:24:12 UTC