# Анимация списков
На данный момент разобрались как управлять переходами для:
- Одиночных элементов
- Множества элементов, когда единовременно отображается только 1 элемент
Но что если есть целый список элементов, которые нужно отображать одновременно, например через v-for
? В этом случае, надо использовать компонент <transition-group>
. Прежде чем перейти к примеру, перечислим несколько важных моментов о нём:
- По умолчанию он не отрисовывает элемент-обёртку, но с помощью атрибута
tag
можно указать, какой элемент должен быть отрисован. - Режимы переходов недоступны, потому что больше не переключаются взаимоисключающие элементы.
- Каждый элемент внутри
<transition-group>
всегда должен быть с уникальным значением атрибутаkey
. - CSS-классы переходов будут применяться к внутренним элементам, а не к самой группе/контейнеру.
# Анимации добавления и удаления элементов списка
Рассмотрим небольшой пример с анимацией добавления и удаления элементов списка, используя те же CSS-классы, что и ранее:
<div id="list-demo">
<button @click="add">Добавить</button>
<button @click="remove">Удалить</button>
<transition-group name="list" tag="p">
<span v-for="item in items" :key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
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')
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
See the Pen Анимация списков 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">Перемешать</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</transition-group>
</div>
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')
2
3
4
5
6
7
8
9
10
11
12
13
14
.flip-list-move {
transition: transform 0.8s ease;
}
2
3
See the Pen Пример использования Transition-group 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">Перемешать</button>
<button @click="add">Добавить</button>
<button @click="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>
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')
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
See the Pen Пример с Transition-group 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>
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')
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 Упругая анимация элементов списка 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) {
// ...
}
}
})
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)
}
})
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>
2
3
Это удобно, когда заранее определены CSS-переходы или анимации, используя принятые во Vue соглашения об именовании классов, и хочется между ними переключаться.
Динамическую привязку можно использовать для любого атрибута <transition>
. И речь не только об атрибутах. Ведь хуки это просто методы и у них есть доступ ко всем данных в текущем контексте. А значит можно реализовать, чтобы JavaScript-переходы вели себя по-разному, в зависимости от состояния компонента.
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="dynamic-fade-demo" class="demo">
Появление:
<input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
Исчезновение:
<input
type="range"
v-model="fadeOutDuration"
min="0"
:max="maxFadeDuration"
/>
<transition
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<p v-if="show">привет</p>
</transition>
<button v-if="stop" @click="stop = false; show = false">
Начать анимацию
</button>
<button v-else @click="stop = true">Остановить!</button>
</div>
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')
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
Будет ещё больше возможностей для создания динамических переходов, если создать компоненты, которые по входным параметрам будут определять каким образом должна работать анимация перехода. Звучит избито, но всё ограничивается лишь воображением.