🐧

Nuxt3:ページ遷移前後で要素をシームレスに変化させる

2023/04/12に公開

はじめに

私はつい先日Vueを学び始めた完全初心者です。
記事の内容は、間違いを記載しないよう注意して記載していますが、もし間違いがあったときには
ぜひご指摘お願いします。

この記事を書くにあたって、参考にさせて頂いたサイトは以下です。
https://coliss.com/articles/build-websites/operation/javascript/native-like-animations-for-page-transitions.html

こちらのサイトで、記事タイトルのような挙動を実装してあります。
状態管理については、Vuexを用いて行われていますが
現在のNuxtのバージョン3.では、デフォルトでVuexがバンドルされていません。
Nuxt3における状態管理では、useStateを用いることが推奨されているため、
Vuexを用いることなく、参考サイトのような挙動を再現してみました。

実装結果

やりたいことはタイトル通りですが、イメージがわかない方もいるかもしれないので
ご参考程度に。

今回解説する挙動

応用したら可能な挙動

解説

ディレクトリ構造

/Project Root
├ app.vue
├ pages
   └index.vue
   └second.vue
├ layouts
   └default.vue
├ components
   └Navbar.vue
├ nuxt.comfig.ts

挙動

解説用に実装した挙動は、参考サイトのSVGを用いたデモと同じにしています。
FirstページとSecondページがあり、ページ遷移するごとにSVG要素がシームレスに変化します。

コード全体像

本筋と関係ない箇所が大半なので、まずは各ファイルのコードを全部記載します。

nuxt.config.ts
//ページトランジションを有効化
export default defineNuxtConfig({
  app: {
    pageTransition:{name:'page',mode:'out-in'},
  },
});
app.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<!-- ページトランジションを設定する場合は以下 -->
<style>
.page-enter-active,
.page-leave-active {
  transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
  opacity: 0;
  filter: blur(1rem);
}
</style>
index.vue
<template>
  <div>
    <h1>First Page</h1>
  </div>
</template>
second.vue
<template>
  <div>
    <h1>Second Page</h1>
  </div>
</template>
NavBar.vue
<template>
  <nav>
    <ul>
      <li><NuxtLink to="/">First</NuxtLink></li>
      <li><NuxtLink to="/second">Second</NuxtLink></li>
    </ul>
  </nav>
</template>

ここから本筋です!

default.vue
<template>
    <Html lang="ja" />
    <Navbar />
    <slot />
      <svg
        v-bind:class="className"
        xmlns="http://www.w3.org/2000/svg"
        width="447"
        height="442"
        viewBox="0 0 447 442"
      >
        <transition-group name="layout" tag="g">
          <rect
            class="items rect"
            ref="rect"
            key="rect"
            width="171"
            height="171"
          />
          <circle
            class="items circ"
            key="circ"
            id="profile"
            cx="382"
            cy="203"
            r="65"
          />
          <g class="items text" id="text" key="text">
            <rect x="56" y="225" width="226" height="16" />
            <rect x="56" y="252" width="226" height="16" />
            <rect x="56" y="280" width="226" height="16" />
          </g>
          <rect
            class="items footer"
            key="footer"
            id="footer"
            y="423"
            width="155"
            height="19"
            rx="9.5"
            ry="9.5"
          />
        </transition-group>
      </svg>
</template>

<script setup>
const route = useRoute();
const className = ref('');

watch(route, () => {
  switch (route.path) {
    case "/second":
      className.value = "active";
      return;
    default:
      className.value = "";
      return;
  }
})
</script>

<style lang="scss">
.items {
  transition: all 0.4s ease;
}

#text {
  transform-origin: 50% 50%;
}

svg {
  fill: #a8dadc;
}

.active {
  fill: #e63946;
  .rect {
    transform: translate3d(0, 30px, 0);
  }
  .circ {
    transform: translate3d(30px, 0, 0) scale(0.5);
  }
  .text {
    transform: rotate(90deg) scaleX(0.08) translate3d(-300px, -35px, 0);
  }
  .footer {
    transform: translate3d(100px, 0, 0);
  }
}
</style>

コードは、Vue3のスタイルであるComposition APIを用いて記載しています。

ポイントとなるNuxt(Vue)の機能は以下です

  • layouts
  • ref
  • useRoute
  • watch

layoutsについて

Nuxtの機能です。
layoutsディレクトリに配置してvueファイルは、
NuxtLayoutタグで利用することが可能であり、
これはページ遷移時も、画面内にとどまります。
(ページ毎に適応するレイアウトを変化させることも可能です)

詳細はドキュメントを参照
https://nuxt.com/docs/api/components/nuxt-layout

refについて

Vue備え付けの機能です。
これは、引数に与えた値をリアクティブにすることができる関数です。
Vueではimportして使用する必要がありますが、Nuxtではその必要がありません。便利。

詳細はドキュメント参照
https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive

useRoute

これは、Vue Routerの機能です。
現在のルートを取得することができます。
pathにアクセスすることで、ルートからのパスを取得できます。

詳細はドキュメント参照
https://nuxt.com/docs/api/composables/use-route

https://zenn.dev/mm67/articles/nuxt3-use-route

watch

Vue備え付けの機能です。
リアクティブな要素を監視して、その値が変化する度にコールバックを実行することができます。
これも、Nuxtではインポートなしに使用可能です。

詳細はドキュメント参照
https://ja.vuejs.org/guide/essentials/watchers.html#basic-example

実装手順

以上を踏まえて、今回の実装の肝の部分を解説します。

変化させたい要素に変化前後のスタイルを当てる

今回の例では、svg要素に変化前後(ページ遷移前後)に当てたいスタイルを当てています。
スタイルの出しわけ方法としては、クラスのつけ外しです。
secondページへ遷移したらsvg要素に「active」クラスが付与される想定でスタイリングします。

必要な値を用意

default.vue
<script setup>
const route = useRoute();
const className = ref('');
</script>

classNameを後程、svgのクラスへバインドします。

パスの変化を監視して、classNameを変化させる

default.vue
watch(route, () => {
  switch (route.path) {
    case "/second":
      className.value = "active";
      return;
    default:
      className.value = "";
      return;
  }
})

watchを用いてuseRouteの変化を監視し、変化があれば分岐処理します。
パスが「/second」の時はclassNameを「active」とします。
デフォルトは「''」空文字です。
注意点としては、refで定義したリアクティブな値にscript内でアクセスするときには
.valueを付ける必要があります。

classNameをsvg要素のクラスにバインドする

default.vue
<template>
<svg v-bind:class="className" ...
</template>

v-bindを用いてclassを記載することで、refで定義したリアクティブな値「className」を
svg要素のクラスとして反映することができます。
こうすることで、ページ遷移ごとに「active」クラスの付け外しを行うことができます。

実装完了です。

課題と学び

今回の実装で、ページ遷移前後でHTMLの特定の要素のクラスを付け外しを行うことでシームレスな変化を実現することができました。
しかし、これはつまりアニメーションの変化はCSSでのスタイリングに依存しているとも言えると思います。
クラスが付与されている時といない時のスタイルをどのように当てるか、トランジションさせる前提でスタイルを当てる必要もあります。
その分、実装の手間が増えることや、実装可能な範囲の制限が出てくる可能性すらあります。

記事冒頭で示した以下のレイアウトについて

このレイアウトを普通に実装するとなると、フレックスボックスや、グリッドレイアウトを活用するかと思います。
しかし、この実装ではクリックしたカードアイテムの位置を中央にトランジションさせる必要があるため
そのような実装方法では実現が難しくなります。
どちらの方法でも試してみましたが、個別表示からリスト表へ戻る際にカクつきます(段落ちのトランジションは不可能)
そのため、全ての要素をabsolute配置しています。

この例から分かる通り、実装上の制限が出てくることは明確であり、今後の課題と言えます。

しかしながら、今回実装をしてみて
layoutsを用いて遷移前後でシームレスに要素を変化させるための一手法を知ることができました。
これも、ツイート内でご助言下さったエスイチさん(https://twitter.com/CS451364464?s=20)のおかげです!ありがとうございます!

Discussion