Google Apps ScriptでVuetify3を検証する【GAS】

前回、Vue3の検証を行いました。軽く触った感じではとりあえずは何とかなりそう。しかし、Vue.jsだけではウェブアプリは作れません。GUIフレームワークが必要です。2022年11月1日、Vue3に遅れてVuetify3がリリースされました。

ただし、Vuetify2よりもGUIコンポーネントはまだまだ少ないのと知見が全然足りない状況である点、ウェブを見た限りでは大きな変更も多い印象で、Vue.jsのように簡単に移行出来るのかどうか?見ていきたいと思います。

Google Apps ScriptでVuetify v2を使ってUIを作る【GAS】

今回利用するスプレッドシート

現時点でホームページに日本語の翻訳資料が掲載されておらず(v2は日本語が用意されてるのに)、いくつかの重要なコンポーネントが存在していないという有様なのと、Vue3の破壊的変更についてこれずに他のライブラリも移行が進まずという現状が垣間見れます。Vue3に関しては以下の記事を参考にしてみてください。

Google Apps ScriptでVue3を検証してみた【GAS】

Vuetify3の利用上の注意点

利用するCDNのファイル

リリースされたとは言え、現状ではコンポーネントの数が半分程度しか揃っておらず、BetaリリースのLabsで提供されてるものが一部ある状態で、正直な所メインで利用できるか?といったらかなり疑問符がつく状況。これもVue3移行が怪しいと言われてしまってる理由の1つで、故に足踏みせざるを得ない状況です。

Labsのコードはテスト目的利用のみとし、本番環境では使わないようにと注釈がある状態です。しかし、今回Vuetify2との比較という事もあるため、そこを踏まえた上で記述する事になります。今回は以下のライブラリを参照で使います。また、MaterialDesignIconも必要となるので、以下のライブラリをVue3のライブラリの後に読み込みます。

//body以下で読み込み
<script src="https://cdn.jsdelivr.net/npm/vuetify@3.3.8/dist/vuetify.min.js"></script>

//head内で読み込み
<link href="https://cdn.jsdelivr.net/npm/vuetify@3.3.8/dist/vuetify.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/7.0.96/css/materialdesignicons.min.css" rel="stylesheet">

破壊的変更

Vue3同様にというか、Vue3よりもかなり大規模に破壊的変更が入っています。また準備中のコンポーネントもある為、これまでのものをそっくり移植するといった事は難しいでしょう。こちらに移行ガイドがありますが、1個ずつ確認しながらの作業になると思います。

基本スタイルの変更

これまでは、Vueを定義した中にVuetifyがいるといった状態で記述してきました。しかし、Vuetify3からはVue3の変更に伴って以下のような大きな変更になっています。

これまでの書き方は以下の通り

<!-- Vuetifyを初期化する -->
<script>
     vm = new Vue({
            el: '#app',
            vuetify: new Vuetify(),
     })
</script>

しかし、Vuetify3ではVue3同様にまずは冒頭で初期化してから、useにて読み込む形になります(Vue2時代のプラグインの読み込みと同じ方式)

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify({
  theme: {
    defaultTheme: 'dark'
  }
})

//Vueを作成
var vm = createApp({
  data() {
    return {
      test: "テストの値"
    }
  },
})

//VuetifyとVueを適用
vm.use(vuetify)
vm.mount('#app')

Vue3でcreateAppで変数に格納後、それに対してuseにて別途用意しておいたvuetify変数を格納し、最期にmountにて反映するというフローです。useとmountはcreateAppにチェーンメソッドで繋げて

createApp().use(vuetify).mount("#app")

としても動作します。

注意点として、vum.use(vuetify)として、vm.mout("#app")とした場合、これまでのようにdataの中の変数やmethodを外部のFunctionから叩こうとするとエラーになります。これ自分も嵌まったのですが、createApp().use(vuetify).mount("#app")の場合は問題なく叩けますし、変数も変更できます。

この両者同じことしてるように思えて、vmに格納された中身が異なるようで、これまでvueのインスタンス外から変数の値や関数を叩いてた場合、動かなくなるので要注意です。この内容についてはこちらのサイトで詳しく解説されています。

スタイルの指定方法の変更

各コンポーネントのスタイルを指定するpropですが、これまでは例えば、smallだとかoutlinedだとか、いきなりタグの中で書いていたかと思いますが、他のプロパティ同様に「項目名=xxxx」といった指定方法に統一されました(一部はなぜか残ってるけど)。

例えばボタンに対するプロパティ指定の事例で言えば以下の通りになります。

<v-btn
        block
        class="text-none mb-4"
        color="indigo-darken-3"
        size="x-large"
        variant="flat"
        @click="tomato"
>
       確認して実行
</v-btn>

図:sizeやvariantでデザイン指定する

ヘルパークラスが変更されてる

前述のスタイル関係のプロパティ以外にも、classで指定してレスポンシブにサイズを変更するなどでよく使ってる「ヘルパークラス」に関しても変更が入っています。例えば、v-card-titleなどはこれまでflexboxとして描画されていましたが、Vuetify3からはdisplay:blockとして描画されているので、センタリングする為に「justify-center」を使っているのであれば、「d-flex」も追加が必要になっています。

但しスペーシングのヘルパークラス(paddingやmarginを設定するもの。pa-6といったクラス名)は変更が無いようです。但し、変更が無いだけで挙動が変更されてる場合も大いにあり得るので微調整は必要かと思います。

ES6コードでimport

GASではimport文といったES6での書き方はできませんが、HTML側では特殊な書き方をする事でES6スタイルでVue3およびVuetify3をImport文とES6の書き方で導入することが可能です。まだ、ES6の書き方はChrome等一部のブラウザのみでしか対応していないので、別途ES Module Shimsを導入して使えるようにします。利用するCDNのライブラリはesmと記述のあるライブラリになります(Vuetifyのesmモジュールはこちら

//HEAD内でES Module Shimsを追加
<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js"></script>

//Body以下でimportmapで取り込み
<script type="importmap">
  {
    "imports": {
      "vue": "https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.esm-browser.min.js",
      "vuetify": "https://cdnjs.cloudflare.com/ajax/libs/vuetify/3.3.8/vuetify.esm.min.js"
    }
  }
</script>

上記の状態で既にimportで取り込みが完了してるので、後はVueやVuetifyのページで書かれてるようなES6での構文でも記述が可能になります。あとは、vueとvuetifyを初期化してES6でコードを記述することが可能です。

<script type="module">
  //VueとVuetifyを導入する
  import { createApp, computed } from 'vue'
  import { createVuetify } from 'vuetify'

  const vuetify = createVuetify();

  //Vueを作成
  var vm = createApp({
    data() {
      return {
        test: "テストの値"
      }
    },
    methods: {
      tomato: function () {
        alert("とまと")
      }
    }
  }).use(vuetify).mount('#app')

</script>
  • type="module"でES6での構文を利用することが可能です。
  • import文でVueおよびVuetifyをロードします。

Vuetify3の実装例

Vuetify2と異なり、コンポーネントが少ない上に、かなり大幅に変更が随所に入ってる為、既存のv2のコンポーネントのタグをそのまま動かすことは出来ません。よって、1個ずつ確認しながら実装する事になります。特に前述のプロパティの指定方法やヘルパークラスなどが変わっている為、苦労する事になります。

今回は自分がよく使ってるコンポーネントについて検証をしてみます。

Appbar

アプリケーション上部のナビゲーションバーです。これまでは色々とプロパティやらクラスやら指定して、v-mainが被らないようにしたりといった小細工が必要だった部分が随分と素直になっています。v-mainおよびv-sheetを併用するのは変わらないですが、これまでよりは扱いやすくなっています。

HTML側コード

<v-app>
  <v-app-bar
    color="teal-darken-4"
    image="https://picsum.photos/1920/1080?random"
    scroll-behavior="collapse"
    scroll-threshold="37"
  >
    <v-app-bar-nav-icon></v-app-bar-nav-icon>
    
    <v-app-bar-title>
      <span>南国フルーツ図鑑</span>
    </v-app-bar-title>

    <v-spacer></v-spacer>
        
    <v-btn icon="mdi-magnify"></v-btn>
  </v-app-bar>
  
  <v-sheet>
    <v-main>
      <v-container>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
        ここにコンテンツを記述する<br>
      </v-container>
    </v-main>
  </v-sheet>
</v-app>
  • shrink-on-scrollの代わりにscroll-behaviorとscroll-thresholdの2つを指定して、どれくらいスクロールしたらバーを縮小するかを指定可能になりました。
  • デフォルトで特にヘルパークラスを使わなくても、v-mainで括っておけばスクロール時にコンテンツ部分がバーとバッティングすることがなくなりました。
  • v-btnのiconプロパティにMaterialDesignIconを指定して、ツールバーのボタンを表示可能です
  • それ以外はこれまでとほとんど変わっていません。
  • 今回のケースではJS側で特に何もやることがありません。

図:ツールバーは結構素直に実装出来た

Navigation drawers

前述のアプリケーションのツールバーと併用するであろう「サイドバーことナビゲーションドロワー」。v-app-bar-nav-iconにアクションを割り当てて、ドロワーのオンオフを行います。v2の際には結構小技を加えていました(項目をクリックしたらドロワーをsetTimeoutで自動で閉じるなど)。v-listなどと併用して構築することになります。

JS側コード

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      drawer: false,
      group: null,
      items: [
        ['mdi-email', 'Mail','メール開くよ'],
        ['mdi-account-supervisor-circle', 'Member', 'メンバー一覧'],
        ['mdi-clock-start', 'Task', '予定タスク'],
      ],
    }
  },
  watch: {
    group () {
      this.drawer = false
    },
  },
  methods: {
    onListChange(event){
      let targetId = event.currentTarget.id;
      alert(targetId);
      setTimeout(() => {
        //ドロワーをオフにする
        this.drawer = false
      }, 0)
    }
  }
}).use(vuetify).mount('#app')
  • ドロワーのリストはitems変数に今回は二次元配列で格納しています(アイコン等を表示したい為)
  • 基本的な項目はVuetify2の時とほとんど変わりません。
  • v-list-itemをクリックしても、drawerは閉じないので、setTimeoutにて自動的に閉じるようにonListChange関数に追記しています。

HTML側コード

<v-app>
  <v-navigation-drawer
    v-model="drawer"
    image="https://cdn.vuetifyjs.com/images/backgrounds/bg-2.jpg"
    color="blue-grey darken-4"
  >
    <v-list>
      <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4">
      
        <v-list-item v-for="([icon, text,args], i) in items" :key="i" v-on:click="onListChange($event)" v-bind:id="args" density="compact" nav>
          <v-list-item :prepend-icon="icon" :title="text"></v-list-item>
        </v-list-item>

      </v-list-item-group>
    </v-list>
  </v-navigation-drawer>

  <v-app-bar
    color="teal-darken-4"
    image="https://picsum.photos/1920/1080?random"
    scroll-behavior="collapse"
    scroll-threshold="37"
  >
    <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
    
    <v-app-bar-title>
      <span>南国フルーツ図鑑</span>
    </v-app-bar-title>
  </v-app-bar>

  <v-sheet>
    <v-main>
      <v-container>
        <v-card-text>
          ここにコンテンツを記述する
        </v-card-text>
      </v-container>
    </v-main>
  </v-sheet>
</v-app>
  • Drawerは結構随所に変更と修正が入ってる感じです。
  • これまでDrawerの背景画像はsrcで指定していましたが、Vuetify3ではimageにて指定するようになりました。
  • Drawerのフォント色はv-list-itemではなくv-navigation-drawer側に指定します。
  • これまではアイコン表示の為に別にv-list-item-iconで定義していたものは、v-list-itemの中のprepend-iconで指定するように統合されました。
  • v-forで回す場合動的にプロパティに変数値を入れるので、:prepend-iconという形で指定してv-forの値を格納するとリストが生成されます。
  • 以前はCSSのz-indexでnavigation drawerがv-app-barよりも手前に表示するように調整していましたが、Vuetify3ではDrawerを先に書いてからv-app-barを後に書くだけで、サイドバーが最前面表示になります。

図:無事に理想のサイドバーが出来上がった

Snackbar

いちいちアラートメッセージをダイアログや通知を表示して、ユーザに何か作業をさせようとするのは具合が悪い。ということで使う一般的にToastと呼ばれる小さなメッセージボックスがSnackbarで、Vuetify3でも健在です。自分もアプリで頻繁に使いますが仕組みがとっても単純です。

但し、Vuetify2ではfullscreen Dialog表示中にSnackbarの閉じるをクリックすると、Dialogまで閉じるという不可解な挙動があったりして、回避策が必要でした。

JS側コード

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      snackbar: false,
      text:"",
      timeout: 3000,
    }
  },
  methods: {
    snackman: function () {
      this.text = "カルビーのポテトチップス";
      this.snackbar = true;
    }
  }
}).use(vuetify).mount('#app')
  • 殆ど、Vuetify2の時代と変わらず
  • snackman関数で文字と表示のオンを指定しています。

HTML側コード

<v-app>
  <v-btn
    class="text-none mb-4"
    color="indigo-darken-3"
    variant="tonal"
    @click="snackman"
  >
    すなっくばー
  </v-btn>

  <v-snackbar
    v-model="snackbar"
    :timeout="timeout"
  >
    {{ text }}

    <template v-slot:actions>
      <v-btn
        color="blue"
        variant="text"
        @click="snackbar = false"
      >
        閉じる
      </v-btn>
    </template>
  </v-snackbar>
</v-app>
  • v-btnをクリックすると、snackman関数を動作させて、snackbarを表示するようにしています。
  • :timeoutで自動で表示が消えるミリセカンドの秒数のプロパティを指定します。snackbar変数のtrue,falseでオンオフ切り替え
  • 殆ど修正する内容が無く、動作させることが出来ました。

図:変わらず利用できました。

Dialog

Vuetify3に於いてもダイアログ関係は健在ですが、多少機能拡張されてる面があったり、Vuetify2では存在した謎の不具合などが解消されています。基本的なスタイルは変わらないです。以前あった、ダイアログ表示時にテキストボックスに文字を入れてボタンを押し、Alertを出した後に再度テキストボックスを触ろうとすると、入力が出来なくなるといった問題が修正されています。

JS側コード

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      dialog: false,
      testtext: "",
    }
  },
  methods: {
    dialogman: function () {
      this.dialog = true;
    },
    onApprove : function () {
      alert("閉じたよ")
      this.dialog = false;
    }
  }
}).use(vuetify).mount('#app')
  • 基本的には大きな変更はなし。これまでのスタイルで記述が可能です。

HTML側コード

<v-dialog v-model="dialog" persistent max-width="290" activator="#menu-popup">
  <!-- カードで整形する -->
  <v-card>
    <v-card-title class="headline">
      この申請を承認しますか?
    </v-card-title>
    <v-card-text>Vuetify3でダイアログテスト</v-card-text>

    <v-text-field
      v-model="testtext"
      label="テスト入力"
    ></v-text-field>

    <v-card-actions>
      <v-spacer></v-spacer>
      <v-btn color="blue darken-1"  @click="onApprove">
        実行
      </v-btn>
      <v-btn  color="red darken-1"  @click="dialog = false">
        キャンセル
      </v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>
  • persistentのプロパティはそのまま(ダイアログの外部をクリックしても閉じさせない為のプロパティ)
  • activatorにて#menu-popupを指定するとポップアップダイアログとなる。
  • v-btnの内側にdialogタグを追加できるようになっており、その場合、activatorはparentを指定する。するとコマンド無でdialogがtrueとなる
  • 但し、FullscreenなDialog表示後にsnackbarを表示=>閉じるをクリックするとDialogまで閉じてしまうバグは健在。(persistent追加で回避できる)
  • Fullscreenなダイアログの場合はactivatorは付与しない
  • v-card-actionsは必ずダイアログの下部に追加されるようになります。

図:ダイアログ関係は本当によく使う

selectbox

前述までに出ていたv-btnなどのフォームコンポーネントの1つであるドロップダウンリストであるv-select。JSON形式でラベル・値・表示名等を指定しつつ、選択時の返り値をどうするか?など結構嵌りポイントも多い部品です。

JS側コード

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      items:[
        {
          id: 1,
          name: "アメリカ"
        },
        {
          id: 2,
          name: "イギリス"
        },
        {
          id: 3,
          name: "ドミニカ"
        }
      ],
      funinsaki:"",
    }
  },
  methods: {
    //赴任先を選んだら発火する
    readprop(item){
      console.log(item)
      this.funinsaki = item.name;
      console.log(this.funinsaki)
    },
  }
}).use(vuetify).mount('#app')
  • JS側はこれまでとあまり変わり無し。readpropは今回はreturn-objectとしてるのでJSONオブジェクトが返ってくる

HTML側コード

<v-app>
  <v-container>
    <v-select
      v-model="funinsaki"
      :items="items"
      item-value="id"
      item-title="name"
      label="出張先選択"
      hint="君が飛びたい先を選択"
      @update:model-value="readprop"
      persistent-hint
      return-object
    ></v-select>
  </v-container>
</v-app>
  • v-modelで連結先はfuninsakiとしてある
  • 値の塊は:itemsとしてitemsを指定。この値は配列ではなくJSONオブジェクトの配列を指定してある
  • item-textではなくitem-titleに変更されてる(これが選択リストの一覧に出てくる)
  • item-valueで選択時に選ぶ項目をJSONオブジェクトのid項目に指定
  • return-objectとしてるのでitem-valueではなく選択されたJSONオブジェクトが返る(無い場合はitem-valueが返却される)
  • 選択時にイベント発火はこれまで、v-on:changeでしたが、これが@update:model-valueに変更。GASで使う場合はウェブ上で書かれてるような@update:modelValueではないので注意(この辺の説明がほとんどされていないのがVue3の問題点)
  • 他の注意点はこれまでと同じ

図:表示はname, 値はidが選択される

Validation

form内容の値のチェックをする為のvalidation機能。送信前に内容をチェックするのに正規表現等を用いてチェックをし、入力ルールに従っていない場合には送信を阻止するために重要な仕組みです。

JS側コード

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      name : "",
      mail : "",

      //validationルール
      rules:{
        //必須項目のvalidation
        required: value => !!value || "必ず入力してください", 

        //メアドのvalidation
        mail: value => {
          var pattern = /^(([^<>()[\]\\.,;:\s@]+(\.[^<>()[\]\\.,;:\s@]+)*)|(.+))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
          if(value == "" || value == undefined){
            return false || 'メアドが入力されていません'
          }else{
            return pattern.test(value) || 'メアドの形式がオカシイ'
          }
        },
      }
    }
  },
  methods: {
    //送信処理
    async saverecord(){
      //validation状態を取得
      const { valid } = await this.$refs.formman.validate()

      //入力内容のチェック
      if (valid) {
        alert("無事通過したよ")
      }else{
        alert("入力内容にオカシナ点があるよ")
      }
    },
    
    //validationのリセット
    reset () {
      this.$refs.formman.reset()
    },
  }
}).use(vuetify).mount('#app')
  • 変更点は、v-formのrefのチェックを行うsaverecordのコードに於いて、async/awaitで同期的に処理が必要になった点。
  • resetやresetValidationは依然として利用可能です。
  • ruleの作成方法に特に変更点はありません。

HTML側コード

<v-app>
  <v-form ref="formman">
    <v-text-field
      v-model="name"
      label="名前"
      :rules="[rules.required]"
    ></v-text-field>

    <v-text-field
      v-model="mail"
      label="メールアドレス"
      :rules="[rules.mail]"
    ></v-text-field>

    <v-btn color="success" block class="mt-2" @click="saverecord">送信</v-btn>
    <v-btn block class="mt-4" color="error" @click="reset">リセット</v-btn>
  </v-form>
</v-app>
  • 2つのテキストボックスに:rulesで必要なルールをセットします。
  • @clickにてそれぞれのボタンに対して関数をセット。validationチェック自体は関数側で行う
  • v-formにてrefを設定し、関数側でthis.$refs.formman.validateにて一括で入力内容のチェックを行わせます。

図:入力内容チェックが働きました

まだ正式版じゃないコンポーネント

Vuetifyで当たり前のように使っていたコンポーネントのうち、いくつかがまだ正式版としてリリースされていない状態にあります。その中で最もよく使われていたものについて現状の対応状況を調査してみました。

ロードするライブラリ

公式サイトや数々のブログで、Labsのライブラリのロードに関して、その殆どがimport構文でローカルのライブラリを導入する事しか記述がなく、こういった点が衰退を招くポイントになっていたりします。調べてみたところ、LabsのコードをCDNのライブラリからロードして使う場合には、標準のCDNライブラリではなく、以下のlabsの文字が入ったライブラリに差し替えて利用する必要があります。

//JSライブラリ
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuetify/3.3.8/vuetify-labs.min.js"></script>

//CSSライブラリ
<link href="https://cdnjs.cloudflare.com/ajax/libs/vuetify/3.3.8/vuetify-labs.min.css" rel="stylesheet">

GASでは通常CDNからの利用しか出来ませんので、上記の点は大きな嵌りポイントになってるので要注意です。

Table

概要

Vuetify3でスプレッドシートのデータをレコードとして列挙する「v-data-table」ですが、2023年7月現在実はこの非常に重要な機能がまだ正式版ではなくLabsという項目に属しており、一応使えるもののベータ版?ともいえる状態であるため、プロダクトには使えない状態です。V-tableという簡易的なものはあるのですが、非常に低機能であるので、これまでのものを移植しようとすると躓くVuetify3の大きな問題点の1つです。

※VueプラグインのCheetah GridについてはVue3対応はしていない様子(もともとVue.js対応が弱かったので今後は自分は使わないかも)。ag-grid-vue3がvue3に対応してるので、高速な表示はこっちを推奨です。

※旧Vuetify2のv-data-tableに似た構造のvue3-easy-data-tableというコンポーネントもリリースされています。

※現在最新版はレスポンシブ対応ができていない状況のようで。3.3.9でも正式にリリースできていないのが・・・・

標準的な使い方

JS側コード
//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      itemsPerPage: 5,  //アイテム表示件数
      headers: [
        { title: 'デザート名', key: 'name', align: 'start'},
        { title: '値段', key: 'price', align: 'end'},
        { title: 'カロリー', key: 'calorie', align: 'end' },
        { title: '国名', key: 'country', align: 'start' },
      ],
      desserts: [
        { name: 'タピオカ', price: 350, calorie: 346.1, country: "ブラジル" },
        { name: 'ナタデココ', price: 230, calorie: 80, country: "フィリピン" },
        { name: 'パンナコッタ', price: 400, calorie: 211, country: "イタリア" },
        { name: 'クリームブリュレ', price: 400, calorie: 354, country: "フランス" },
        { name: 'アップルパイ', price: 380, calorie: 304, country: "アメリカ" },
        { name: 'ピカロン', price: 290, calorie: 390, country: "ペルー" },
        { name: 'イチゴ大福', price: 180, calorie: 148, country: "日本" },
        { name: '愛玉子', price: 150, calorie: 1.68, country: "台湾" },
        { name: 'ワッフル', price: 250, calorie: 256, country: "ベルギー" },
      ]
    }
  },
  methods: {

  }
}).use(vuetify).mount('#app')
  • headerのtextはtitleに、valueはkeyに変更されているので要注意。
  • これまで数値の右揃えは非常に手間だったのが、alignのendで右揃えになるように追加されています。
  • 値を格納する変数に関しては特に大きな変更点は標準的な利用では見当たらず
HTML側コード
<v-app>
  <v-data-table
    v-model:items-per-page="itemsPerPage"
    :headers="headers"
    :items="desserts"
    item-value="name"
    class="elevation-1"
  ></v-data-table>
</v-app>
  • 表示件数を指定するitem-per-pageがv-modelでの指定に変更されています。-1を指定すれば常に全件表示になります。

図:参照するCDNが異なるので注意

これまで装備してきたものを検証

ヘッダの固定と高さ指定

全件表示して、v-data-tableの高さを固定化し、スクロールで見ていくスタイルの場合、オプションのプロパティや変数をこれまでも用意してきましたが、これらはVuetify3でも変わらず利用が可能になっています。

<v-app>
  <v-data-table
    v-model:items-per-page="-1"
    :headers="headers"
    :items="desserts"
    item-value="name"
    class="elevation-1"
    :height="dataTableHeight"
    fixed-header
  ></v-data-table>
</v-app>
  • :heightで高さの数値を入れてある変数を指定する(リサイズ時はここを適正にすれば良い)
  • fix-headerプロパティを入れればスクロールしても、ヘッダーはスクロールせず固定化が出来る
アクションを追加

各レコードに対するアクションボタンを追加するのも非常によく利用します。こちらはiconのプロパティの指定の仕方が変わっている点を除けば、そこまで大きな変更はありません。

<v-app>
  <v-data-table
    v-model:items-per-page="itemsPerPage"
    v-model:sort-by="sortBy"
    :headers="headers"
    :items="desserts"
    item-value="name"
    class="elevation-1"
    :height="dataTableHeight"
    fixed-header
  >
    <template v-slot:item.actions="{ item }">
      <v-icon dense class="mr-2" @Click="preview(item)" color="green" icon="mdi-eye" size="small"></v-icon>
      <v-icon dense class="mr-2" @Click="editprop(item)" color="red" icon="mdi-circle-edit-outline" size="small"></v-icon>
      <v-icon dense class="mr-2" @Click="updatedocs(item)" color="blue" icon="mdi-update" size="small"></v-icon>
    </template>
  </v-data-table>
</v-app>
  • iconプロパティ内でmdiの値を指定すればmaterialiconが表示される。
  • sizeプロパティでsmallとすればこれまでと同じような表示になります。

図:これまでのようなアクションボタンも追加可能

特定の値に対してハイパーリンクを貼る

v-data-tableの特定の列の値に対してハイパーリンクを張る場合、元データの値を使って自動的に実現したいです。しかし、Vuetify2とは若干異なる点があり、ちょっと苦労しましたが、調べてみたところ、以下のようなコードでハイパーリンクを貼れるようになります。dessertsデータにはurlとして各レコードにURLを入れています。

<v-app>
  <v-data-table
    v-model:items-per-page="itemsPerPage"
    v-model:sort-by="sortBy"
    :headers="headers"
    :items="desserts"
    item-value="name"
    class="elevation-1"
    :height="dataTableHeight"
    fixed-header
  >
    <template v-slot:item.actions="{ item }">
      <v-icon dense class="mr-2" @Click="preview(item)" color="green" icon="mdi-eye" size="small"></v-icon>
      <v-icon dense class="mr-2" @Click="editprop(item)" color="red" icon="mdi-circle-edit-outline" size="small"></v-icon>
      <v-icon dense class="mr-2" @Click="updatedocs(item)" color="blue" icon="mdi-update" size="small"></v-icon>
    </template>

    <template v-slot:item.url="{ item }">
      <a target="_blank" :href="item.selectable.url">
        外部リンク
      </a>
    </template>

  </v-data-table>
</v-app>
  • 前述のアクションボタンのようにv-slot:item.urlにてまずはitemを取得させる
  • itemに直接レコードが入っているのではなく、selectable項目内にいるので、その値を取得させる。:hrefでバインディング

同じようなテクニックを応用すれば、関数を使っての文字数制限表示やclassを使った特定文字の場合のフォントカラー変更なども可能だと思われます。

図:ハイパーリンクも設定できました

多数のレコードを表示用の新機能

ただ単純にこれまでのVuetify2を踏襲してるだけでなく、これまで多くのデータをv-data-tableで表示させた場合、激重になるケースに対する対応策として、新機能である「Virtual Tables」という機能も追加されています。ページネーションは無いですが、並び替えとフィルタをサポートしており、ごく一部のレコードだけレンダリングするようにすることで、大規模データであっても、表示が重くならないという話です。

まだ試していないのですが、いずれ大規模データで実験をしてみたいと思います。

Date Picker

概要

これまた非常によく利用するであろう日付ピッカーのモジュールも驚くべきことにまだ正式対応していないコンポーネントです。見切り発車感が強い感じがこういった点から感じられます。そのため、一般利用となるとVueのプラグインであるVue3 Date Pickerを使うケースで対応してる人が多いようです。とはいえ、もともとVuetifyの日付ピッカー他タイムピッカーも含めてこの手のコンポーネントとっても弱くて、サードパーティのモジュール使う機会が多かったように思えます。

CSS

<style>
    /*  DatePickerの土日の色 */
  .v-date-picker-month__day--week-start {
    color:blue
  }

  .v-date-picker-month__day--week-end {
    color:red
  }
</style>
  • 日付の土曜日と日曜日だけ色を青と赤に変えたい場合のCSSです。

HTML側コード

<v-app>
  <div class="d-flex justify-center">
    <v-locale-provider locale="jp">
      <v-date-picker
        ok-text="設定"
        cancel-text="キャンセル"
        title="日付をチョイス"
      >
      </v-date-picker>
    </v-locale-provider>
  </div>
</v-app>
  • localeはv-locale-providerでjpを指定すると日本語化が出来るけれど、相変わらず「日」が付いてくる
  • しかもこれまでの「日」を削りとるdate-formatオプションが無いので、関数で削ることが出来ないという・・・
  • ok-textやcancel-text, titleを設定すると日本語化出来る

図:完成度がちょっとよろしくない

Vueプラグインで対応する

前述の未完成コンポーネントではプロダクトを作るのはちょっとアレな状態なので、VuetifyではなくVue3のプラグインモジュールを使って実装をするパターンです。これまでだと、vue.use(コンポーネント名)といったような形でロードしてから使っていたと思いますが、Vue3からは少々異なってるので注意が必要。

以下の事例はVue3DatePickerを使って日付ピッカーを装備する事例になります。

Vue3DatePicker

CDNライブラリをロードする

Vuetifyではなくサードパーティのライブラリで且つ、Vue3のプラグインとなるので、まずはそれらをロードする必要があります。

//vue3datepickerのJSライブラリ
<script src="https://unpkg.com/@vuepic/vue-datepicker@latest"></script>

//CSSライブラリ
<link rel="stylesheet" href="https://unpkg.com/@vuepic/vue-datepicker@latest/dist/main.css">

JSライブラリはvue3の次に読み込むようにし、CSSはHead内でロードしておきます。

コンポーネント登録

Vueにロードしておいたコンポーネントを登録します。以下のようにcreateAppでcomponentとして登録します。date変数だけdataに追加しておきます。

//ビューの初期化
const { createApp } = Vue

//Vuetifyの初期化
const { createVuetify } = Vuetify

//ダークテーマも適用しておく
const vuetify = createVuetify()

//Vueを作成
var vm = createApp({
  data() {
    return {
      date: "",
    }
  },
  methods: {
    //日付フォーマットの修正用関数
    format(dateman){
      let date = new Date(dateman);

      const day = date.getDate();
      const month = date.getMonth() + 1;
      const year = date.getFullYear();

      return `${year}/${month}/${day}`;
    }
  },
  components: { Datepicker: VueDatePicker },
}).use(vuetify).mount('#app')

HTML側コード

HTML側にDatePickerを表示するのはとっても簡単。以下のタグを追加するだけです。

<v-app>
  <Datepicker v-model="date" locale="ja" :format="format" week-start="0" placeholder="日付を選択" :highlight-week-days="[0, 6]">
  </Datepicker>
</v-app>
  • 選択時の日付の形式をformat関数にて修正しています
  • localeにてjaを選択すれば日本語表記になり、また余計な日などは付きません。
  • 他にもいろいろ機能が備わってるので、これ一本で日付や時間関係はカバーできます。
  • week-startが0で日曜日からの開始になります。
  • 但し、土日だけフォント色を変えるのが出来なかった・・・

図:Veutify3のモジュールより出来が良い

ag-grid-vue3

CDNライブラリをロードする

v-data-tableの代わりとして、高機能で高速なAgGridのVue3対応版を使ってみることにしました。但しこのコンポーネントは単一コンポーネントで使われることが前提且つimportでモジュールを追加する必要があり、単純にライブラリを参照で追加しても動作しません。またいくつかの別のモジュールも必要です。公式リファレンスはこちら。過去にノーマルのag-gridを使ったケースは以下のエントリーで記述しています。

Google Apps Scriptでag-gridを使ってみる【GAS】

さらにag-grid-community.min.jsag-grid-vue3バージョンも合わせる必要があるため、今回は30.0.5のCDN版を利用しました。

//Head内で読み込み
<link href="https://cdn.jsdelivr.net/npm/ag-grid-community@30.0.5/styles/ag-grid.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/ag-grid-community@30.0.5/styles/ag-theme-alpine.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/ag-grid-community@30.0.5/dist/ag-grid-community.min.js"></script>

<!-- ES6モジュールを導入 -->
<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js"></script>

vue.jsより下にて、前述のES6のimportmapを利用する仕組みを使ってimportを行います。

<script type="importmap">
  {
    "imports": {
      "ag-grid-vue3": "https://cdn.jsdelivr.net/npm/ag-grid-vue3@30.0.5/+esm"
    }
  }
</script>

コンポーネント登録

今回はVueとVuetifyはそれぞれ通常法方法でロードしていますが、ag-grid-vue3がimportで取り込んでる為、type="module"にてコンポーネントの登録を行う必要があります。

<script type="module">
  //ビューの初期化
  const { createApp } = Vue

  //Vuetifyの初期化
  const { createVuetify } = Vuetify

  //Vuetifyをロード
  const vuetify = createVuetify();
  import { AgGridVue } from "ag-grid-vue3";

  //Vueを作成
  var vm = createApp({
    data() {
      return {
        columnDefs: [
          { headerName: "Make", field: "make" },
          { headerName: "Model", field: "model" },
          { headerName: "Price", field: "price" },
        ],
        rowData: [
          { make: "Toyota", model: "Celica", price: 35000 },
          { make: "Ford", model: "Mondeo", price: 32000 },
          { make: "Porsche", model: "Boxster", price: 72000 },
        ],
      }
    },
    components: {
      AgGridVue
    }
  }).use(vuetify).mount('#app')

</script>
  • import文にてAgGridVueを取り込みます。
  • createApp内のcomponentsに於いて、取り込んだAgGridVueを登録します。

HTML側コード

また、GASのウェブアプリで利用する場合、公式サイトやブログなどに記述されてるような書き方(キャメルケース)では、データやヘッダの入ったcolumnDefsやrowDataをカスタムタグに割り当てても読み込むことが出来ません。必ず、ケバブケースの書き方でプロパティを書く必要があるため要注意です。

<v-app>
  <v-container>
    <ag-grid-vue
      style="width: 100%; height: 500px"
      class="ag-theme-alpine"
      :column-defs="columnDefs"
      :row-data="rowData">
    </ag-grid-vue>
  </v-container>
</v-app>
  • カスタムタグはag-grid-vueを利用し:rowDataは:row-data、:columnDefsは:column-defsとケバブケースでプロパティをセットします。
  • ここで使っていないプロパティに関してもキャメルケースで書かれてる資料がほとんどなので、ケバブケースでの書き方にするようにしましょう。イベント等に於いても同様です。

図:なんとか利用できました

6万件データ表示

前回のag-gridの記事でも利用した6万件データをスプレッドシート側から引っ張ってきて、ag-grid-vue3に突っ込んで表示してみました。7列6万件のデータで、おおよそスクリプト実行から10秒で表示され、表示後のスクロールやソートなども非常に高速でモタつくことがありません。従来のVuetify2のv-data-tableだとスクロールのたびに重たくなっていたのですが、今回はVue3対応でこの出来ですので、大量のデータを取り扱う場合には、ag-grid-vue3は非常に最適な選択肢と言えます。

今回は、2列目まで固定し、他の列はリサイズ可、ソートも可という設定値を施してあります。サンプルページはこちらになります。

columnDefs: [
  { headerName: "ID", field: "id", sortable: true, pinned: 'left', width: 80, cellDataType: 'text' },
  { headerName: "JAN", field: "jan", sortable: true, pinned: 'left', width: 130, cellDataType: 'text' },
  { headerName: "GS1", field: "gs1", sortable: true, width: 130, resizable:true },
  { headerName: "医薬品名", field: "drugname", sortable: true, width: 200, resizable:true },
  { headerName: "メーカー", field: "maker", sortable: true, width: 150, resizable:true },
  { headerName: "先行品", field: "senkou", sortable: true, width: 130, resizable:true },
  { headerName: "ジェネリック", field: "generic", sortable: true, width: 130, resizable:true},
],

図:ビッグデータ表示も難なく可能

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)