Vue で高さをアニメーションさせる
Tweetこの記事の Vue の基礎的な仕様に関しては以下の書籍を参照しています。おすすめです。
高さをアニメーションさせるのは難しい
後半に紹介する記事でもこう書かれている。
- 今までの方法 1: max-height: 0 => Xpx に動かす。ただしこの場合 x を幾つにするかを適当に大きな数に決めるしかない。適当に決めると、足りなかったり、多すぎてアニメーションが変になる。
- 今までの方法 2: transform: scaleY(0) => transform: scaleY(1)
- 今までの方法 3: JavaScript を駆使する。
そう、1 と 2 では思ったようにならない。結局 JavaScript を駆使するしかない。
平たくいうと height: auto 時の高さをいかに取得するかが肝心
平たく言えば、見えないように heigth: auto 時の高さを取得して、この高さに値を変更すればいい。そのテクニックが紹介されている記事を参考に実装した。
$el と $refs
まず Vue において、DOM Element の高さを取得するには、そもそも DOM Element そのものを取得できなくてはいけない。そのためには $el と $refs がある。ケースに合わせて使い分けるが、DOM Element を参照するという意味では同じ機能。
$el
コンポーネントが描画された結果出力される DOM Element を this.$el で取得できる。mounted 以降のライフサイクルで実行できる。
$el で DOM Element に直接アクセスする
<template>
<div id="app">
<h2>タイトル</h2>
<p>ボディです</p>
</div>
</template>
<script>
export default {
mounted() {
console.log(this.$el);
}
};
</script>
<style></style>
$refs
コンポーネント全体ではなくて、特定の要素の描画された DOM Element を取得するには ref を使う。同じく mounted 以降でないと参照できない。
- template 内で ref="名前" で特定の要素を this.$refs に紐づける。
- this.$refs で参照可能。その下に全ての ref がオブジェクト的に紐づいている。
$refs で
<template>
<div id="app">
<h2 ref='title'>タイトル</h2>
<p ref='body'>ボディです</p>
</div>
</template>
<script>
export default {
mounted() {
console.log(this.$refs);
console.log(this.$refs.title);
console.log(this.$refs.body);
}
};
</script>
<style></style>
nextTick を使って更新後の DOM にアクセスする
正確にはわからないのだが、何か変更を DOM に加えて、その変更が終わった後に何かを実行するためには nextTick を使うらしい。
Height を animation させる記事
https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/
冒頭要約
- height 0 => auto に animation させたいが、これは普通にやってもできなくて難しい。
- 方法は今まで三つしかないと思っていたが、できる方法を見つけたのでこの記事で紹介する。
- 今までの方法 1: max-height: 0 => Xpx に動かす。ただしこの場合 x を幾つにするかを適当に大きな数に決めるしかない。適当に決めると、足りなかったり、多すぎてアニメーションが変になる。
- 今までの方法 2: transform: scaleY(0) => transform: scaleY(1)
- 今までの方法 3: JavaScript を駆使する。
- 昔は CSS Animation をメインで使うべきでアニメーションさせるのに JS を使うのは何か変だと思っていたが、React, Vue などゴリゴリ JS を書くライブラリにおいては JS を使うのは自然だと趣旨替えした。
実装内容のうち参考になるところ
- mount された瞬間に高さとか幅を取得する
- その際に表示されないように細々テクニックを使っている(visible: hidden, position: absolute 等)
- とにかく mount された瞬間に見えないようにしながら元の高さを取得して、その高さまで height を変更すればいい
- animation は普通に transiton を使う
高さを見えないように取得し、変更する
<template>
<div id="app">
<button @click="open">オープン</button>
<button @click="close">クローズ</button>
<div class="wrapper" ref="wrapper">
<h2>タイトル</h2>
<p>ボディです</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
height: null
};
},
methods: {
open() {
const el = this.$refs.wrapper;
el.style.height = this.height;
},
close() {
const el = this.$refs.wrapper;
el.style.height = 0;
}
},
mounted() {
const element = this.$refs.wrapper;
const { width } = getComputedStyle(element);
/* eslint-disable no-param-reassign */
element.style.width = width;
element.style.position = `absolute`;
element.style.visibility = `hidden`;
element.style.height = `auto`;
/* eslint-enable */
const { height } = getComputedStyle(element);
/* eslint-disable no-param-reassign */
element.style.width = null;
element.style.position = null;
element.style.visibility = null;
element.style.height = 0;
/* eslint-enable */
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height;
this.height = height;
console.log(this.height);
// setTimeout(() => {
// // eslint-disable-next-line no-param-reassign
// element.style.height = height;
// });
}
};
</script>
<style>
.wrapper {
transition: height 0.5s;
overflow: hidden;
background: green;
}
</style>
結局ポイントは、見えないようにどうやって高さを取得するか
結局ポイントは、見えないようにどうやって高さを取得するかなので、ライフサイクルを把握する事に尽きる。そこの工夫が場合によっては必要だが、原理はわかった。