nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【Vue】【CSS】Vue.jsで"""最初から開いた状態"""でアコーディオンしたい

アコーディオン??

元々jQueryで実現されていたアニメーションで、 イベントに応じて、隠れた要素が伸縮して出てくる、メニューとかでよく使われて「アコーディオンメニュー」なんて言ったりする。 CSSだけだと以下のような見えないチェックボックスをイベントでチェックして表現したりする。

webdesignday.jp

以下は引用させていただいた。

See the Pen accordion menu by Kiyonobu Kasuga (@kiyonobu-kasuga) on CodePen.

一方 transition ディレクティブなんていう便利なコンポーネントが元々ある Vueは、 アニメーションの描写をもう少しスマートに書くことができる。

jp.vuejs.org

引用だと以下のサイトが詳しい。

lab.astamuse.co.jp

See the Pen vue-accordion - fin by 35n139e (@35n139e) on CodePen.

へえ、じゃあそれでいいじゃん。

さて、本題だが、以上のコードで1つ問題がある。 それが表題にあるような、「開いた状態でのアコーディオン」だ。 これはつまり、普通メニューにしろなんにしろ、アコーディオンと言えば閉じた状態からアニメーションを開始するわけだが、 「最初から開いた状態」で作るとただコピペでは作れない。

実際に、シンプルな構成で作ってみる。 「最初から閉じている状態」ならうまくいく。

See the Pen test-accordion-vue01 by nnao45 (@nnao45) on CodePen.

では、この開閉を制御するisShowをtrueで初期状態にしてみよう、

See the Pen test-accordion-vue02 by nnao45 (@nnao45) on CodePen.

うん、うまくいかない。

試行錯誤

思考過程が見たい人はどうぞ

そもそもどうアニメーションしているのか。

初めに紹介したが、Vue.jsには便利な <transition> コンポーネントというやつがありnameで指定したtransitionはCSSで更にアニメーションの動作上にCSSを充てることができる。

例えばtransitionのnameはexpandとすると、上記の例にみたように

.expand-enter-active,
.expand-leave-active {
    transition: height .5s ease-in-out;
    overflow: hidden;
}

.expand-enter,
.expand-leave-to {
    height: 0;
}

のように <name>-enter だのそういうふうに固定名で指定してやるといい。 加えて、transitionコンポーネントをそのアニメーションの動作の各地点でheightを0pxから元ある高さに伸ばす事でアニメーションさせるわけだ。

Vue.component('my-component',{
  template: `
  <div class="app">
        <div class="components">
            <p class="title">アコーディオン1</p>
            <transition
               name="expand"
               @before-enter="beforeEnter"
               @enter="enter"
               @before-leave="beforeLeave"
               @leave="leave"
            >
                <div v-show="isShow" class="body">
                    <p>アコーディオン1の中身</p>
                    <p>アコーディオン1の中身</p>
                    <p>アコーディオン1の中身</p>
                </div>
            </transition>
        </div>
        <button v-on:click="isShow = !isShow">ボタン</button>
    </div>
  `,
  data() {
        return {
            isShow: true
        };
    },
    methods: {
        beforeEnter(el) {
            el.style.height = "0";
        },
        enter(el) {
            el.style.height = el.scrollHeight + "px";
        },
        beforeLeave(el) {
            el.style.height = el.scrollHeight + "px";
        },
        leave(el) {
            el.style.height = "0";
        }
    }
});

上記は多分ネットで一番やられているコピペだと思う。

なにが悪いのか

要はトランジションクラスの指定が最初から空いている状態ではうまくいかないのであろう Vueの公式を見ると、

v-enter: enter の開始状態。要素が挿入される前に適用され、要素が挿入された 1 フレーム後に削除されます。

v-enter-active: enter の活性状態。トランジションに入るフェーズ中に適用されます。要素が挿入される前に追加され、トランジション/アニメーションが終了す ると削除されます。このクラスは、トランジションの開始に対して、期間、遅延、およびイージングカーブを定義するために使用できます。

v-enter-to: バージョン 2.1.8 以降でのみ利用可能です。 enter の終了状態。要素が挿入された 1 フレーム後に追加され (同時に v-enter が削除されます)、トランジション/アニメーションが終了すると削除されます。

v-leave: leave の開始状態。トランジションの終了がトリガされるとき、直ちに追加され、1フレーム後に削除されます。

v-leave-active: leave の活性状態。トランジションが終わるフェーズ中に適用されます。leave トランジションがトリガされるとき、直ちに追加され、トランジション/アニメーションが終了すると削除されます。このクラスは、トランジションの終了に対して、期間、遅延、およびイージングカーブを定義するために使用できます。

v-leave-to: バージョン 2.1.8 以降でのみ利用可能です。 leave の終了状態。leave トランジションがトリガされた 1 フレーム後に追加され (同時に v-leaveが削除されます)、トランジション/アニメーションが終了すると削除されます。

https://jp.vuejs.org/images/transition.png

https://jp.vuejs.org/v2/guide/transitions.html#トランジションクラス

問題点を洗い出す

考察するに開いた状態では一番最初の状態がenterの状態、つまり「DOMが挿入された」という風にとらえられないと考えられる。 こういう場合は requestAnimationFrame がを併用し、要素の再描写を強制的にさせる方法があるそうである。

参考: Transition to Height Auto With Vue.js - Markus Oberlehner

加えてアニメーションの動作の最後には高さを auto にして調整してもらいたいため、enterのafterとleaveのafterも設定しておくこととする。

    methods: {
        enter(el) {
            const height = getComputedStyle(el).height;
            el.style.width = null;
            el.style.position = null;
            el.style.visibility = null;
            el.style.height = 0;
            getComputedStyle(el).height;
            requestAnimationFrame(() => {
                el.style.height = height;
            });
        },
        afterEnter(el) {
            el.style.height = "auto";
        },
        leave(el) {
            const height = getComputedStyle(el).height;

            el.style.height = height;
            getComputedStyle(el).height;

            requestAnimationFrame(() => {
                el.style.height = "0";
            });
        },
        afterLeave(el) {
            el.style.height = "auto";
        }

上記のサイトにも書いてあるが、

The leave() method, which is triggered as soon as the element is hidden or removed from the DOM, retrieves the current height of the element and sets it explicitly in order to make it possible to animate back to 0.

ということで、DOMを消すときはちゃんと requestAnimationFrame0 にアニメーションするように指定すると安定するそうだ。

結論

こうなった

See the Pen test-accordion-vue04 by nnao45 (@nnao45) on CodePen.

gist.github.com

感想

HTML/CSSむずかしすぎ