株式会社HAMWORKS
💁

VTabs VTab VTabWindow VTabWindowItem 変更点(Vuetify 2 〜 Vuetify 3 アップデート)

2024/06/27に公開

利用環境

  • Vuetify 3.5.163.6.10 へアップデート

調べた背景

Vuetify 3 にアップデートする際に、VTabs と VTab VTabWindow VTabWindowItem に変更点があったのでまとめてみました。

https://vuetifyjs.com/en/api/v-tabs-window/

https://vuetifyjs.com/en/api/v-tabs-window-item/

アップデートに伴って、変更箇所を洗い出すためにドキュメントを確認したところ 👆のコンポーネントを知りました。
Vuetify の VTabs をVuetify 3へのアップデートを行うときに、VTabsWindow と VTabsWindowItem が追加されていた。
この2つコンポーネントが、自分が利用している 3.5.16 ありませんでした。(IDEで補完もされず、調べるきっかけに)

どのバージョンで追加されたかのか調べたところ、3.6.0 のAlphaで追加されたようです。

https://github.com/vuetifyjs/vuetify/releases/tag/v3.6.0-alpha.0

https://github.com/vuetifyjs/vuetify/pull/19248

3.6.0 のリリースノートにはありませんでした。
Alphaで取り込めたものはリリースノートはAlphaに入っているようです。

https://github.com/vuetifyjs/vuetify/releases/tag/v3.6.0

VTabsのマイグレーションは以下のみでしたが、👆のリリースで VTabsWindow と VTabsWindowItem を使う形になるようです。

https://vuetifyjs.com/en/getting-started/upgrade-guide/#v-tabs

  • v-tab-item has been removed, use v-window-item 3.6.0 以降は、v-tabs-window-item

最近のアップデートになるため、巷の記事をみると v-windw-item が利用する記事が多いようです。
v-window-item が非推奨になったわけではなく、 v-tabs に #window の slot が追加されたことで、本来のタブの構造を維持できるようになったようです。

https://github.com/vuetifyjs/vuetify/pull/19248/files#diff-096234b3e698985ff2eeb9a207290166fe047424fedf52048ab4867178efcf4aR35

以下のドキュメントにも記載があります。

https://vuetifyjs.com/en/components/tabs/#slots

VTabs / VTab

VTabs と VTab は、Vuetify 3 でのアップデートで変更点はありませんでした。
v-model を 定義することで現在の開いてるタブを管理するための変数を用意するままで Vuetify 2のHMTLコードでも動作します。
以下の例は、Composition APIを利用したコードになるため、tabs の ref を用意することで動作します。

VTabs / VTab
<script setup lang="ts">
import { ref } from 'vue'

const tabs = ref(null)
</script>
<template>
  <v-tabs v-model="tabs">
    <v-tab value="1">コンテンツ1</v-tab>
    <v-tab value="2">コンテンツ2</v-tab>
  </v-tabs>
</template>

VTabsWindow / VTabsWindowItem バージョン 3.6.0 以降

タブで表示するコンテンツを用意するために VTabsWindow と VTabsWindowItem を利用します。
v-tabs の 子要素として <template #window> を定義して VTabsWindowItem を利用することでタブのコンテンツ領域を確保できます。

タブの場合、アクセシビリティの観点からも要素はまとめておいた方が良いから、この仕様になったのかなと感じた。

VTabs / VTab
 <script setup lang="ts">
 import { ref } from 'vue'

 const tabs = ref(null)
 </script>
 <template>
   <v-tabs v-model="tabs">
     <v-tab value="1">コンテンツ1</v-tab>
     <v-tab value="2">コンテンツ2</v-tab>
+    <template #window>
+     <v-tabs-window-item value="1" :transition="props.transition" :reverse-transition="props.transition">
+       <v-card variant="flat">
+         <v-card-title>コンテンツ1</v-card-title>
+         <v-card-text>
+           コンテンツ1です。
+         </v-card-text>
+       </v-card>
+     </v-tabs-window-item>
+     <v-tabs-window-item value="2" :transition="props.transition" :reverse-transition="props.transition">
+       <v-card variant="flat">
+         <v-card-title>コンテンツ2</v-card-title>
+         <v-card-text>
+           コンテンツ2です。
+         </v-card-text>
+       </v-card>
+     </v-tabs-window-item>
+    </template>
   </v-tabs>
 </template>
  • アニメーションはスライドがデフォルトに設定されている
  • :transition:reverse-transition でアニメーションを設定できる

アニメーションの種類

  • v-expand-transition
  • v-fab-transition
  • v-fade-transition
  • v-scale-transition
  • v-scroll-x-transition
  • v-scroll-y-transition
  • v-slide-x-reverse-transition
  • v-slide-x-transition
  • v-slide-y-reverse-transition
  • v-slide-y-transition
  • v-tab-reverse-transition
  • v-tab-transition
  • v-toggle-slide-x-reverse-transition
  • v-toggle-slide-x-transition
  • v-toggle-slide-y-reverse-transition
  • v-toggle-slide-y-transition

いくつか標準でアニメーションのAPIが用意されています。
タグなどでアニメーションさせたい場合は、👆のAPIを利用します。

https://vuetifyjs.com/en/styles/transitions/#api

  • 属性に使う場合は、 v- は不要
  • コンポーネントとして利用するときは、 v- が必要

Vuetify 2との違い(アニメーションをオフにする)

  • :transition='false'' だけでアニメーションのオンオフを行っていた。Vuetify 3では、 :transition="none":reverse-transition="none" を指定することでアニメーションを完全に停止できます。

参考記事

https://unching-star.hatenablog.jp/entry/2024/01/06/010159

VWindow と VTabsWindow の違い

以下のコミットをみる限りでは、機能面では基本的な違いはありません。
コミットにも VTabsWindow と VtabsWindowItem は、それぞれ VWindow と VWindowItem をインポートして拡張しているようです。

https://github.com/vuetifyjs/vuetify/pull/19248/files#diff-c9687676670d342fc1f460cf9ff9367c3adb8674a8e9c02fd0d10927eb696f1d

https://github.com/vuetifyjs/vuetify/pull/19248/files#diff-583a75fc94628b56bb0d50b42b6caedcd24dbee9300c788b3a4bbb642ced8278

構造的な部分で言えば、VWindowの場合ではプロック要素にならず、VWindowItem ごとにブロック要素(v-sheetなど)を噛ませる必要がありました。
以下のサンプルコードでは、tabsの子要素にする場合は、タブの領域の横並びで表示されてしまいます。

https://nuxt3-vuetify-storybook.pages.dev/?path=/story/features-vtabswindow--default

<v-tabs> の外で定義してブロック要素になるように調整する必要がありました。

<template>
  <p>このページは VWindow VTabs の外に配置して利用した場合のTabsコンポーネントです。</p>
  <p>VWindowをタブのように使う場合は、v-sheetなどブロック要素にした上で、v-tabsの外で定義しなければならない。</p>
  <v-tabs v-model="tabs">
    <v-tab value="1">
      コンテンツ1
    </v-tab>
    <v-tab value="2">
      コンテンツ2
    </v-tab>
    <v-tab value="3">
      コンテンツ3
    </v-tab>
  </v-tabs>
  <!-- v-tabsの外で定義する   -->
  <v-window v-model="tabs">
    <v-window-item value="1" :transition="props.transition" :reverse-transition="props.transition">
      <!--  ここでTabの領域を確保する -->
      <v-sheet
        tile
      > 
        <v-card variant="flat">
          <v-card-title>コンテンツ1</v-card-title>
          <v-card-text>
            コンテンツ1です。
          </v-card-text>
        </v-card>
      </v-sheet>
    </v-window-item>
    // 省略
  </v-window>
</template>

https://nuxt3-vuetify-storybook.pages.dev/?path=/story/features-vtabswindowoutside--default

VTab の to の ページ遷移

ページ遷移で利用する場合は、v-tab に to を指定することで可能です。
内部的には vue-router を利用していますので、vue-router のインストールが必要になります。

https://vuetifyjs.com/en/api/v-tab/#props-to

型情報は vue-routerの RouteLocationRaw になります。

https://router.vuejs.org/api/#RouteLocationRaw

ページ遷移だけになるため、位置情報を保持する必要性がないため v-modelは不要です。
カレントの処理は routerのパスで判別していることから、自動的にカレントがつくようです。

<template>
  <v-tabs>
    <v-tab to="/">
      Home
    </v-tab>
    <v-tab to="/about">
      About
    </v-tab>
  </v-tabs>
</template>

Storybook上で Vue Routerを動かす

Nuxt の利用環境では、ファイルベースルーティングになるため別途StorybookにはVue Router の設定が必要です。
viteで使ってる場合はrouterの設定をまとめておくと良いですが、ここでは直接 .storybook/preview.ts にroutes の設定を加えました。
非同期コンポーネントとして component: () => import('../pages/index.vue') 書いてる理由はビルドしたタイミングでpageのコンポーネントを取り出せないためこのような書き方にしました。

preview.ts に 検証したい routes を設定して、VueRouterをStorybook上で動作するようにします。

https://zenn.dev/sa2knight/books/storybook-7-with-vue-3/viewer/vue-router

この書籍を参考に組み合わせ実装しましたが、createRouterで historymemory を切り替えるようにしています。
Nuxtの場合は、Storybook上でしか確認しないことから 直接 memory を呼び出しても良いかもしれません。

.storybook/preview.ts
import type { Preview } from "@storybook/vue3";
import { setup } from '@storybook/vue3'
+ import * as VueRouter from "vue-router";

// Routes
+ const routes: VueRouter.RouteRecordRaw[] = [
+   {
+     path: '/',
+     name: 'Home',
+     component: () => import('../pages/index.vue')
+   },
+   {
+     path: '/about',
+     name: 'About',
+     component: () => import('../pages/about.vue')
+   }
+ ]

+ const createRouter = (type: 'memory' | 'history') => {
+   const history = type === 'memory' ? VueRouter.createMemoryHistory() : VueRouter.createWebHistory();
+   return VueRouter.createRouter({ history, routes });
+ };

+ const router = createRouter('memory');

// Styles
import vuetify from "../utils/vuetify";
import VueApexCharts from 'vue3-apexcharts'

setup((app) => {
  if (app) {
    app.use(vuetify)
    app.use(VueApexCharts)
+     app.use(router)
  }
})

export const decorators = [
  (story: any) => ({
    components: { story },
    template: '<v-app><story /></v-app>',
  }),
]

/** @type { import('@storybook/vue3').Preview } */
const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

preview.ts の設定でルーターを呼び出す設定を完了したら、利用しているコンポーネントのストーリーにルーターの設定を追加します。
ここでは、VTabsTo というコンポーネントをサンプルで用意しました。

http://localhost:6006/?path=/story/features-vtabsto--default

pages側の設定には、Nuxtの特有の機能があるため単純に起動だけではエラーでハマります。
そのため、Storybook上で確認するための pages.vue を用意していたほうがいいかもしれません。

https://zenn.dev/sa2knight/books/storybook-7-with-vue-3/viewer/vue-router#ページコンポーネント用のストーリーを作成する

設定自体は、👆のコードを参考に作成しました。

VTabsTo.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import { useRouter } from 'vue-router'
import VTabsTo from './VTabsTo.vue'

type Story = StoryObj<typeof VTabsTo>;

function createPageStory (name: string): StoryObj {
  return {
    render: () => ({
      setup () {
        const pageLoaded = ref(false)
        const router = useRouter()
        router.push({ name }).then(() => {
          pageLoaded.value = true
        })

        return { pageLoaded }
      },
      template: '<template v-if="pageLoaded"><router-view /></template>'
    })
  }
}

const meta: Meta<typeof VTabsTo> = {
  title: 'Features/VTabsTo',
  component: VTabsTo,
  tags: ['autodocs'],
  argTypes: {},
  render: args => ({
    components: { VTabsTo },
    setup () {
      return { args }
    },
    template: '<VTabsTo v-bind="args" />'
  })
}
export const Default: Story = {
  args: {}
}

export default meta

export const HomePage = createPageStory('Home')
export const AboutPage = createPageStory('About')

サンプルで呼び出せるかの検証になることから、このテンプレートに明示的に <router-view /> を追加しています。

VTabsTo.vue
<template>
  <v-tabs>
    <v-tab value="1" to="/">
      コンテンツ1
    </v-tab>
    <v-tab value="2" to="/about">
      コンテンツ2
    </v-tab>
  </v-tabs>
  <router-view />
</template>

まとめ

VTabs のタブ部分をアップデート作業をしていた気づきをまとめました。
Vuetify 3自体がまだまだ発展途上な部分があるため、利用しているバージョン・ドキュメントの差異・リリースノートの確認は引き続き行っていく必要がありそうです。

このようなAPIが存在しない場合は、バージョンによって定義されていないことがほとんどです。
運用中以外のアップデート作業では、常に最新版を使うようにしましょう。

株式会社HAMWORKS
株式会社HAMWORKS

Discussion