この記事の 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>

結局ポイントは、見えないようにどうやって高さを取得するか

結局ポイントは、見えないようにどうやって高さを取得するかなので、ライフサイクルを把握する事に尽きる。そこの工夫が場合によっては必要だが、原理はわかった。