Laravel9でVuetify3(Beta)を試してみる

Vuetify3.0がなかなかリリースされないので他のUIも使ってみたけどやっぱりVuetify!もうすぐリリースされるらしいけどBeta版を試してみる。Laravel9/Vite/Vue3でインストール&設定をしてみましょう。

開発環境

Windows10
Docker Desktop
WSL2
Laravel9
Laravel Sail
Vue3.2.40

Vuetify3(Beta)他のインストール

$ sail npm install vuetify@next
$ sail npm install vite-plugin-vuetify
$ sail npm install -D sass

Viteの設定

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
import vuetify from "vite-plugin-vuetify"	// 追加

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue(),
        vuetify(),	// 追加
    ],
    server: {
        hmr: {
            host: 'localhost',
        },
    },
});

app.jsでの組み込み設定

import './bootstrap';
import VueScrollTo from 'vue-scrollto'
import { createApp } from 'vue'
import 'vuetify/lib/styles/main.sass'				// 追加
import { createVuetify } from 'vuetify'				// 追加
import App from './components/App.vue'				// 追加

const vuetify = createVuetify()

const app = createApp(App)
app.use(vuetify)									// 追加
app.mount("#app")

Viteプラグイン「vite-plugin-vuetify」は使用しているコンポーネントを自動でimportしてくれるので下記のような記述は不要。

import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
const vuetify = createVuetify({
  components,
  directives,
})

アイコンフォント(Material Design Icon)

①デフォルトのアイコンフォント @mdi/fontを使用する場合

mdi/fontのインストール

$ sail npm install -D @mdi/font

app.js

import '@mdi/font/css/materialdesignicons.css'	// 追記

Vueファイル

<div>
  <v-icon icon="mdi-account" class="ma-4" />
  <v-btn
    icon="mdi-account"
    color="success"
    class="ma-4"
  ></v-btn>
</div>

②mdi/jsパッケージを使用する場合

必要なアイコンのみカスタムインポートすることでバンドルサイズを小さくすることができる。

mdi/jsのインストール

$ sail npm install -D @mdi/js

app.js

import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'		// 追記
import '@mdi/font/css/materialdesignicons.css'				// 追記

// 変更
const vuetify = createVuetify({
  icons: {
    defaultSet: 'mdi',
    aliases,
    sets: {
      mdi,
    },
  },
})

Vueファイル

<div>
  <v-icon :icon="mdiAccount" class="ma-4" />
  <v-btn
    :icon="mdiAccount"
    color="success"
    class="ma-4"
  ></v-btn>
</div>

<script setup>
  import { mdiAccount } from '@mdi/js'
</script>

iconはv-bindディレクティブ(:icon)になっていることに注意!

サンプル作成

・Vuetify3のv-app-barとv-navigation-drawerを使用してドロワーメニューの画面レイアウトを作成。
・HomeページにはVuetify3でButton、Icon、Alert、Cardを配置。
・Vue-Router4で画面遷移。
・AboutページではscrollBehaviorとvue-scrolltoでスクロール制御。

Route::get('/app/{any?}', function() {
    return view('app');
})->where('any', '.*');
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
import vuetify from "vite-plugin-vuetify"

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
        vue(),
        vuetify(),
    ],
    server: {
        hmr: {
            host: 'localhost',
        },
    },
});
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">

    <!-- Scripts -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
  <div id="app"></div>
</body>
</html>
import './bootstrap';
import router from './router'
import VueScrollTo from 'vue-scrollto'
import 'vuetify/lib/styles/main.sass'
import { createApp } from 'vue'
import { createVuetify } from 'vuetify'
import App from './components/App.vue'
import '@mdi/font/css/materialdesignicons.css'

const vuetify = createVuetify()

const app = createApp(App)
app.use(router)
app.use(VueScrollTo, {
  offset: -60,
})
app.use(vuetify)
app.mount("#app")
import { createRouter, createWebHistory, useRouter } from 'vue-router'
import VueScrollTo from 'vue-scrollto'
const BASE_URL = '/app'

import Home from './components/Home.vue'
import About from './components/About.vue'
import NotFound from './components/404.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  },
  {
    path: '/404',
    name: '404-NotFound',
    component: NotFound,
  },
  {
    path: '/:pathMatch(.*)',
    redirect: '/404',
  },
]

const router = createRouter({
  history: createWebHistory(BASE_URL),  // set BASE_URL
  routes,
  // scrollBehavior(to, from, savedPosition) {
  //   if (to.hash) {
  //     return { el: to.hash, behavior: 'smooth', top: 60 }
  //   } else if (savedPosition) {
  //     return savedPosition
  //   } else {
  //     return { top: 0 }
  //   }
  // },
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      VueScrollTo.scrollTo(to.hash)
      return {}
    } else if (savedPosition) {
       return savedPosition
    } else {
      VueScrollTo.scrollTo('#app')
      return {}
    }
  },
})

export default router
<template>
<v-app>
  <v-app-bar prominent :elevation="4" color="orange accent-1">
    <v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
    <v-toolbar-title>Vuetify</v-toolbar-title>
    <v-spacer></v-spacer>
  </v-app-bar>
  <v-navigation-drawer
    v-model="drawer"
    clipped
    theme="dark"
    color="deep-orange darken-4"
  >
    <v-list
    >
      <v-list-item
        exact
        v-for="(item, i) in items"
        :key=i
        :to=item.to
      >
        <v-list-item-title v-text="item.title"></v-list-item-title>
      </v-list-item>
    </v-list>
  </v-navigation-drawer>

  <v-main>
    <div class="pa-3">
      <router-view></router-view>
    </div>
  </v-main>

</v-app>
</template>

<script setup>
  import { ref } from 'vue'

  const drawer = ref(null)
  const items = [
    {
      title: 'Home',
      to: '/',
    },
    {
      title: 'About',
      to: '/about',
    },
  ]
</script>

<style scoped>
html {
  overflow: auto !important;
}
</style>

<v-main>の中に<router-view>を配置しないといけないのね。

<template>

  <div>
    <h2>Button</h2>
    <v-btn color="primary" class="mr-2 mb-2">
      Normal
    </v-btn>
    <v-btn color="secondary" flat class="mr-2 mb-2">
      Secondary
    </v-btn>
    <v-btn variant="outlined" color="error" class="mr-2 mb-2">
      Error
    </v-btn>
    <v-btn disabled class="mr-2 mb-2">
      Disabled
    </v-btn>
    <v-btn color="success" class="mr-2 mb-2">
      Success
      <v-icon
        dark
        right
      >
        mdi-checkbox-marked-circle
      </v-icon>
    </v-btn>
  </div>

  <div class="mt-3">
    <h2 class="mt-3">Icon</h2>
    <v-icon icon="mdi-home" class="mr-2"></v-icon>
    <v-icon icon="mdi-login" color="error" class="mr-2" />
    <v-icon icon="mdi-account" color="success" class="mr-2" />
    <v-icon icon="mdi-account" class="mr-2" size="x-large" />
    <v-btn
      icon="mdi-heart"
      color="pink"
      class="mx-2"
    ></v-btn>
    <v-btn
      color="primary"
      variant="outlined"
      icon="mdi-pencil"
      size="x-large"
    ></v-btn>
  </div>

  <div class="mt-3">
    <h2 class="mt-3">Alert</h2>
    <v-alert type="success" class="mb-1">I'm a success alert.</v-alert>
    <v-alert type="info" class="mb-1">I'm an info alert.</v-alert>
    <v-alert type="warning" class="mb-1">I'm a warning alert.</v-alert>
    <v-alert type="error" class="mb-1">I'm an error alert.</v-alert>
  </div>

  <div class="mt-3">
    <h2 class="mt-3">Card</h2>
    <v-card
        max-width="344"
        variant="outlined"
    >
      <v-card-item>
        <div>
          <div class="text-overline mb-1">
            OVERLINE
          </div>
          <div class="text-h6 mb-1">
            Headline
          </div>
          <div class="text-caption">Greyhound divisely hello coldly fonwderfully</div>
        </div>
      </v-card-item>
      <v-card-actions>
        <v-btn variant="outlined">
          Button
        </v-btn>
      </v-card-actions>
    </v-card>
  </div>

</template>
<template>
  <h1>About</h1>
  <v-card
    max-width="300"
    variant="outlined"
    class="mb-2"
  >
    <v-list density="compact">
      <v-list-item><router-link to="#sample">Sample</router-link></v-list-item>
      <v-list-item><router-link to="#example">Example</router-link></v-list-item>
    </v-list>
  </v-card>
  <p>about</p>
  <p>about</p>
  <p>about</p>
  <p>about</p>
  <p>about</p>
  <p>about</p>

  <h2 id="sample">Sample</h2>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>
  <p>sample</p>

  <h2 id="example">Example</h2>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  <p>example</p>
  …
</template>

http://localhost/app/