Vuetify を使って困ったところ小ネタ集

Vue CLI + TypeScript + SCSS + Vue-Router + Vuex なプロジェクトに、$ vue add vuetifyVuetify を追加した。

Vuetify はマテリアルデザインを実現するフレームワークだが、細かなところでデザインを調整したい時に上手いやり方が分からないことがあり、ちょくちょく調べている。

今回はそんな、「ちょっとググッて調整した Vuetify 周りの小ネタ」を紹介する。

目次

Overlays 表示時に縦スクロールバーが消えて画面幅がズレるのがウザい

Dialog (v-dialog) や Navigation Drawer (v-navigation-drawer) などの Overlays を表示する時に、html 要素の縦スクロールバーが消えて、画面幅が若干広がって画面全体が左右にズレるのがキモい。html 要素に独自の CSS クラスが振られて、overflow-y が制御されているのが原因だ。

次のようにページ全体のデザインに指定を入れれば回避できるが、オーバーレイの裏にある画面全体をスクロールできてしまうので、一長一短。

// ↓ scoped 属性を書かないことでグローバルに適用させる
<style lang="scss">

html,
html.overflow-y-hidden {
  overflow-y: scroll !important;
}

そうそう、views/components/ でスタイルを書く時に、

<style lang="scss" scoped>

scoped 属性を付けることで、スタイルのスコープ化 (カプセル化) が出来るようになる。地味に知らなかった。

scrollablev-dialog の縦スクロールバーが見えたり消えたりするのがウザい

先程のページ全体の縦スクロールバーと同様の話。

<!-- scrollable を付与することで、高さが出来た時に `v-card-text` に縦スクロールバーが付く -->
<v-dialog v-model="isShowDialog" persistent scrollable>
  <v-card>
    <v-card-title class="grey lighten-2">タイトル</v-card-title>
    <v-divider></v-divider>
    
    <v-card-text>
      ココに高さが発生する可変な内容…
    </v-card-text>
    
    <v-divider></v-divider>
    <v-card-actions>
      <v-btn color="primary" v-on:click="isShowDialog = false">閉じる</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>

この場合も、v-card-text に縦スクロールバーが付いたり付かなかったりして、Dialog 内の幅がズレるので、次のように直す。

<v-dialog v-model="isShowDialog" persistent scrollable>
  <v-card>
    <!-- ↓ 次のように CSS クラスを振る -->
    <v-card-text class="always-show-scrollbar">
      ココに高さが発生する可変な内容…
    </v-card-text>
  </v-card>
</v-dialog>
.always-show-scrollbar {
  overflow-y: scroll !important;
}

最初からスクロールバーは出しときゃいいじゃん派。

v-list-item で省略せず折り返し表示させたい

リストを構築する v-list-item は、中のテキストが長いと省略表示されてしまう。コレを折り返して全て表示させたい場合は、次のような CSS クラスを割り当てると良い。

<v-list-item-content>
  <v-list-item-title>タイトル</v-list-item-title>
  <v-list-item-subtitle class="wrap-text">  <!-- ← クラスを振っている -->
    長いテキスト…
  </v-list-item-subtitle>
</v-list-item-content>

v-list-itemwhite-space: nowrap 指定があるので、次のような CSS クラスで打ち消してやる。

.wrap-text {
  word-break: break-all;
  white-space: normal;
}

ページヘッダのタイトルにリンクを貼る

ページのヘッダを構成する v-app-bar。その中に配置した v-toolbar-title が、画面全体のタイトルを表現する形になる。

せっかくならこの v-toolbar-title にリンクを振りたいのだが、v-bind:to とかが出来ない。

<v-app id="app">
  <v-app-bar app color="primary" dark>
    <!-- ↓ 以下の v-bind:to 属性は動かない -->
    <v-toolbar-title v-bind:to="'/'">
      タイトル
    </v-toolbar-title>
  </v-app-bar>
  
  <v-main>
    <v-container fluid>
      <!-- カードやリストは v-bind:to が書けるのに… -->
      <v-card v-bind:to="'/hoge'">
        カード
      </v-card>
      
      <v-list dense>
        <v-list-item link v-bind:to="'/home'">
          <v-list-item-action>
            <v-icon>mdi-home</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title>Home</v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-container>
  </v-main>
</v-app>

で、仕方がないので、Vue-Router の router-link を中に入れてやることにした。このままだとリンクっぽい見た目になってしまうので、セットでテキストの色味も調整する。

<v-app id="app">
  <v-app-bar app color="primary" dark>
    <v-toolbar-title>
      <!-- ↓ 素直に router-link を書く -->
      <router-link to="/" class="toolbar-title">
        タイトル
      </router-link>
    </v-toolbar-title>
  </v-app-bar>
</v-app>
#app {
  // v-toolbar-title 内のリンク色を直す
  .toolbar-title {
    color: inherit;
    text-decoration: none;
  }
}

コレでよき。

v-text-fieldrequired 属性だけ書いても必須項目にはならない

フォーム内のテキストボックスを必須入力にさせたく、次のように書いてみた。

<v-form v-model="form.isValid">
  <v-text-field v-model="form.name" required label="名前"/>
  <v-btn v-bind:disabled="!form.isValid" v-on:click="onSubmit" color="primary">送信する</v-btn>
</v-form>

v-formv-model が Boolean になっていて、内部のフォーム部品たちのバリデーションが全部 OK になると true になるという素敵な仕組み。コレを使って Submit するボタンの disabled 属性を変えてやれば、「送信」ボタン押下時に改めてバリデーションチェックを実装する必要がない。

v-text-field には required 属性を振ったし、コレで OK か?と思ったら、どうも上手くバリデーションされていない様子。

調べてみると、コレは HTML5 の required 属性を渡しているに過ぎず、v-formv-model で対応するバリデーションチェックには引っかからないようだ。

というワケで、やはり v-bind:rules 属性は必須というワケだった。なんなら required 属性の方は効果がないから消してしまっても良いくらい。

<v-form v-model="isValidForm">
  <!-- v-bind:rules は配列を渡すので、[] 内に required 関数を入れている -->
  <v-text-field v-model="name" required v-bind:rules="[required]" label="名前"/>
  <v-btn v-bind:disabled="!isValidForm" v-on:click="onSubmit" color="primary">送信する</v-btn>
</v-form>
export default {
  name: 'MyComponent',
  // data は次の書き方をするとインデントが少なく済む (`data() { return {}; }` とするとインデントが一段増える)
  data: () => ({
    isValidForm: false,
    name: ''
  }),
  methods: {
    // 自分でバリデーション関数を用意する
    required: (value) => `${value || ''}`.trim() !== '' || '必須入力です。',
    onSubmit() { /* Submit 処理 */ }
  }
}

バリデーションルールは data 側に持たせる例もある。

<v-text-field v-model="name" required v-bind:rules="nameRules" label="名前"/>
export default {
  name: 'MyComponent',
  data: () => ({
    isValidForm: false,
    name: '',
    nameRules: [
      (value) => `${value || ''}`.trim() !== '' || '名前は必須入力です。',
      // ココに同様に関数を追加していくことで、複数のバリデーションを実現できる
    ]
  })
}

この柔軟性、一度覚えると書きやすいが、書き方を忘れたり、他人のコードを読解する時になると辛くなってくる。

v-list の間に罫線を引く

v-list の行間に罫線を引く場合。1行目の上に余分に罫線が出たり、最終行の下に余計な罫線が出来たりするのを回避するため、ちょっとした調整が必要だった。

<v-card>
  <v-list dense subheader>
    <!-- template 要素自体はレンダリングされない -->
    <template v-for="(myItem, index) in myItems">
      <!-- 1行目以外 (2行目以降) に上線を引く -->
      <v-divider v-bind:key="index" v-if="index >= 1"/>
      
      <v-list-item v-bind:key="myItem.id">
        <v-list-item-content>
          <v-list-item-title>{{ myItem.name }}</v-list-item-title>
        </v-list-item-content>
      </v-list-item>
    </template>
  </v-list>
</v-card>

罫線は v-divider で引けば良いのだが、イイカンジに表示するために index 値を見て v-if で表示要否を決めている。

このように複数要素を v-for で回して表示させたりしたい時に、template 要素が使える。template 要素自体は最終的にレンダリングされないので、CSS の適用順も変わらず、単純に v-forv-if などの制御文を逃して書けるので見通しやすくなる。

テーマカラーを自分で決める

テーマカラーを自分で決める際は、src/plugins/vuetify.ts に書き込めば良い。Theme Generator というサイトで、カラーコードもしくは色変数名で出力できるので、使ってみると良いだろう。

指定の仕方はこんな感じ。ダークテーマの時も同じ色味が使われるようにしておくことで、部分的に dark 指定を入れている時も色味がおかしくならない。

import Vue from 'vue';
import Vuetify from 'vuetify/lib';

// ↓ 他の文献では違うパスを見かけたが、vuetify@2.3.2 ではコレを import するのが正しいみたい
import colors from 'vuetify/lib/util/colors';

Vue.use(Vuetify);

// https://theme-generator.vuetifyjs.com/ をベースに作った
const customTheme = {
  primary  : colors.pink.base,   // #e91e63
  secondary: colors.teal.base,   // #009688
  accent   : colors.cyan.base,   // #00bcd4
  error    : colors.red.base,    // #f44336
  warning  : colors.amber.base,  // #ffc107
  info     : colors.blue.base,   // #2196f3
  success  : colors.green.base   // #4caf50
};

export default new Vuetify({
  // ↓ 以下のように書く
  theme: {
    themes: {
      light: customTheme,
      dark : customTheme
    }
  }
});

あとは以下の要領でテーマ名を指定してやれば良い。

<v-btn color="primary">通常ボタン</v-btn>
<v-btn color="warning">警告ボタン</v-btn>

<p class="warning--text">警告文</p>
<p class="error--text">エラー文</p>

Vuetify は文字色指定だけ 【色名】--text と逆転して書く形になるのが違和感。w

以上

何やらちょこちょこ調べながら書くのが大変ではあるが、さすがは Material Design。見栄えは良いな…。