Google Apps ScriptでVuetifyを使ってUIを作る

Google Apps Scriptは、HTML ServiceでウェブアプリケーションやスプレッドシートのダイアログのUIを構築可能です。しかし、標準ではUIを構築する為のライブラリやフレームワークを備えていないので、ユーザ自身が好きなフレームワークを導入して、HTML上に構築する必要があります。

自分は過去に、Google Apps Scriptのアプリをスマフォ向けとしては、framework7を使ってUIを構築しました。いずれ、framework7のまとめも作ってみたいと思います。今回はPC向けサイトとしてVuetifyを使って構築してみたいと思います。随時、自分がパーツを使うシーンがあれば、ここに追記していきたい。

図:現在作成中のElectronアプリでも使ってます

難易度:

目次

今回使用するライブラリ等

VuetifyはVue.jsのUIフレームワークになるので(ちょうど、jQueryとjQuery UIの関係になる)、Vue.jsも利用する必要があります。

※ダミーデータはOnline test data generator等で作成したものを利用しています。

Google Apps ScriptでVue.jsを使ってみる

事前準備

ウェブ上で公開されてる事前準備では、vueファイルの作成であったり、vue-cliを使うためにnpmでインストールといったような説明書きをよく見かけますが、そもそもjQuery同様にvue.jsやvuetifyは単独で利用出来るようになっているので、あの手順にしたがってプロジェクトを作る必要性はありません。また、Google Apps Scriptでは当然vueファイルやらnpmでインストールなんてできません。

Google Apps ScriptではいくつかのCDNで公開されてるライブラリをロードすることで、コード内で利用できるようになります。こちらのページのUsage with CDNがそれに該当します。

Headerタグ内でCSSをロード

  • Header部分ではCSSを読み込みます。
  • 他にもGoogle FontsやViewportの設定も指定が必要になります。

Bodyタグ内の一番下でJSをロード

  • Vue.jsやVuetify.jsといったライブラリのJSファイルは、Header内で読み込ませても、エラーになります
  • Bodyの一番最後で読み込ませ、また初期化の為のスクリプトも同じ場所で記述する必要があります。
  • new Vue内にて初期化する際に、vuetifyを指定し、そこでnew Vuetifyを呼び出すことで、Vuetifyを初期化しています

viewportの指定

今回はPC向けということなのですが、モバイル向けとしてviewportの指定が必要な場合があります。しかし、通常通りHTML内に記述しても、Google Apps Scriptの場合それらのタグは無視されてしまいます。これらは、HTML Serviceの引数として、addMetaTagでの指定が必要です。faviconなども同様です。

※faviconを指定するとウェブアプリのアイコンが指定のアイコンになります(サンプル)

色々なコントロールを使ってみる

Vuetifyのサイトには様々なUIコントロールのサンプルが掲載されています。しかし、これらのコードをそのままGoogle Apps ScriptのHTML Serviceで動かそうとした場合、動かないケースがままあります。今回はいくつかのコントロールをテストしてみて、実際にこれなら動くという形で、ちょっとだけ変えてあります。

非常に沢山のコントロールと、それぞれにオプションやテクニック、APIが用意されているので、表現力豊かなアプリケーションが構築可能です。

以下のコードはHeaderやBody下部の共通部分については省略しています。

Cards

地味によく使う、タイルのように扱えるコンポーネントです。他にもレイアウト関係のコンポーネントはたくさんあるのですが、その中では随一の使用率。それがこのCardsです。ダイアログであったり、Amazonの商品一覧のように並べたり、細かいテクニックも必要です。

Cardsそのものは他のコンポーネントのように特別スクリプトを必要としないのですが、Cards内でのボタンなどで必要とするケースが多々あります。今回は縦にCardsを掲示板のスレッドのように列挙してみます。

ソースコード

  • これで1枚分のCardsの表示になります。
  • v-containerで括れば、v-cardを縦に列挙した場合、Cards同士の間にきちんと隙間が出来てそれっぽく表示されます。ない場合は密着してしまいます。
  • v-dividerで区切り線を表記します。
  • v-card-text内にメインコンテンツを記述します。
  • v-card-actionsがボタンなどを表記します。
  • v-cardのレイアウトの作り方は多種多彩なので、様々なパターンを会得すると表現力がアップします。
  • データをもとにCardsを自動生成する場合(v-forなど)やボタンのアクションは別途スクリプトが必要です。

図:Cardsを縦に並べてみた

Snackbar

一般には、Toastと呼ばれてる短いメッセージを画面の下部などにフワっと表示させる為のもの。いちいちalertなどのダイアログを出すまでもないような「作業完了」や「データ無し」などの簡易なメッセージはこれらを使って実現すると、ユーザがいちいちOKボタンクリックといった鬱陶しい作業をしなくてすみます。スマフォなどでは結構おなじみの機能ですね。

ソースコード

HTML側コード

  • v-modelにて連結してるsnackbarの値がtrueの時に表示されます。
  • :timeoutにて自動で消える秒数(ミリセカンド)を指定しています。
  • 閉じる為のボタンもつけていますが、タイムアウト指定で自動で消えるようにもしています。
JS側コード

  • テキストはtextの値を変更すれば変える事が可能です。(vm.text = “とまと食べたい”などで変更可能)
  • timeoutの秒数は3000msを自分は指定しています。

実際に表示する場合は、上記のコードであれば、vm.snackbar = trueで表示、vm.snackbar = falseで非表示となります。指定秒数経過後には自動でfalseになります。

 

図:画面下部にフワっと表示されフワっと消える

注意点

fullscreenなDialogなどを表示してるケースで、snackbarを表示し、snackbarの閉じるボタンをクリックすると、Dialogのほうまで閉じてしまうバグ?のようなケースがあります。この問題の回避方法はStackOverflowに投稿されていましたが、ダイアログ側のプロパティとして「persistent」を追加する事で、snackbarの閉じるボタンをクリックしても、Dialogまで閉じられてしまうような事を回避する事が出来ます。

これは、snackbarがDialogの外側に位置してる為、クリックするとDialogの外側をクリックされたと判定され、Dialogが自動で閉じてしまう問題で、persistentを追加する事で、ダイアログの外側をクリックされても、閉じるのを阻止してくれる為、特にfullscreenなダイアログとsnackbarを併用する時には必須となります。

selectbox

UIパーツの中でももっとも基本的なパーツがv-selectタグを使ったコンボボックス or ドロップダウンリストです。このリストは表示する値とは別に反映する値を別に設定出来ますし、複数列を持つJSONの値をセットして運用することもできるのですが、Vuetifyのバージョンによるものなのか、不可解な動きをすることもあるので注意です。

ソースコード

HTML側コード

  • v-modelで入力先の変数を指定しています(今回は取得レコードのfuninsakiという値に入れています)
  • 選択時にv-on:changeにてfuninchg関数を実行するようにしています。後述のJSONの他の値を自動的に別のv-text-fieldに入れるようにしています。
  • 表示するのはJSON値のfunin列を指定するために、item-textにfuninを指定しています。
  • 格納する値はJSON値のID列を指定する為に、item-valueにIDを指定しています。
  • 上記のtextとvalueの指定がない場合、JSONのtextとvalueの項目がデフォルトで選択されるようになっています。
  • :itemsのkaigaiがJSONデータを格納してる先。Vue初期化時のdataの中に予めkaigaiと次項のJSONを格納しておく必要があります。
今回割当てたメニュー用JSON

非常に単純な、ID列とfunin列の2つのカラムで構成されています。funinchg関数では、セレクトされるとfunin列の値を別のtext fieldに入れるようにしています。

JS側コード

  • vueのmethodsにfuninchg関数を用意。
  • eでは選択した項目のJSON値が全部入ってるので、ここからfuninの値を取り出します。
  • 取り出した値は直接選択レコードのJSONに格納すれば、v-modelで連結してるv-text-fieldに表示されます。
  • この手法は、HTML側でreturn-objectを指定しておく必要があります(そうでないと、選択した項目のJSON値が取得できない)

注意点

Vuetifyのv2で遭遇したのですが、このreturn-objectを指定してる場合、またそのv-selectが既存の変数にv-modelで連結してるケースで、item-valueやitem-textで列指定しているにも関わらず、v-selectのv-modelで連結してる項目にitem-valueの値ではなく、選択した項目のJSON値が[object,object]として入ってきてしまうケースがありました。

return-objectを外せばこのような問題は起きない反面、funinchg関数で選択したJSON値を取得できなくなってしまい、連動して他のtext-fieldに値を入れ込むことができなくなりました。なぜこのような事象が発生したのかは不明ですが、連動をさせないケースではreturn-objectは外しておきましょう。この問題は解決策が無いので、変数名変えたりv-modelでselectには連結しないで非連結とし、2つのtext-fieldに入れるようにするなどの迂回策が必要です。

Appbar

アプリケーションの顔とも言える、アプリ上部にあるヘッダに該当するナビゲーションバーです。この他にもファイルやヘルプなどのツールバーなどもあったりします(もちろん、モバイル向けの下部ナビゲーションも)。ただし、サンプルのままで動作しようとすると、このナビゲーションバーにコンテンツ部分が被ったりと色々と面倒な部分もありましたが、以下のコードで動作させることが可能です。

ソースコード

  • v-app-barにおいて、absoluteの指定があると下のコンテンツ部分に被ってしまうので省略
  • shrink-on-scrollの指定がある場合、スクロールするとバーが自動的に半分くらいのサイズにミニマムになる
  • scroll-targetの指定がある場合、スクロールするエリアは指定したID(今回だとmain_content を持つv-sheet)が対象
  • v-btnがナビゲーションのボタンの部分を担当する
  • v-app-bar-nav-iconは左サイドのハンバーガーメニューです

サンプル表示

Bottom Sheet

スマフォなどではおなじみのボタンをクリックすると、下からメッセージがせり上がってくるアレです。アプリで使うシーンとなると、普通のalertの代わりという、承認時のメッセージであったりといったダイアログよりも目立たせたいのが、使うポイントになるかと思います。

ソースコード

JS側コード

  • dataの中のsheetをフラグとし、falseで閉じます
  • 承認と却下用のコマンドをmethodsに加えて起きます。
HTML側コード

  • ボトムを開くをクリックすると、ボトムシートが開かれます
  • 承認、却下はそれぞれがmethodsに規定したコマンドを@Clickにて実行し、this.sheet=falseで閉じるようになっています。

サンプル表示

Dialog

アプリケーションで非常によく使う要素の1つがダイアログでしょう。jQueryなどを使わずにオシャレなダイアログを構築する事が可能です。その表現の数も非常に充実しており、よくあるYES/NO、フルサイズの設定用ダイアログ、ダイアログのネスト、プログレス表示用などなど、ユーザの要求を適切に受け取ることが可能です。

ソースコード

JS側コード

  • コード自体はBottom Sheetとほとんど同じものになります。
  • dataの中のdialogをフラグにして、falseの時に閉じます。
HTML側コード 

  • 承認実行ボタンを押すとダイアログが表示されます。
  • ダイアログの2つのボタンにそれぞれ@Clickで実行するmethodを指定しておきます。

サンプル表示

注意点

ElectronでVuetifyを使い、ダイアログを使っていた際に遭遇したトラブルなのですが、保存ボタンを押しthis.dialog = falseをした後に、confirmにて選択のメッセージを出した場合以下のトラブルが生じました。

  • 保存は実行され、再度ダイアログも開けるが、テキストボックスをクリックしてもフォーカスされず、入力ができなくなる
  • ただし、カレンダーやセレクトボックスは正常に動作する
  • 一度デスクトップをクリックして、再度Windowをクリックするとフォーカスして入力ができるようになる
  • しかし、一時的に治ってるだけで、再度Dialogを閉じると同様の症状が発生する

回避策としては、Electronであるならば、メインプロセス側でdialog.messageにて表示して分岐処理をさせるようにする。Google Apps Scriptの場合は、confirmを使わず別の確認メッセージを表示する手段を利用するようにしましょう。

Carousel

ブログパーツであったり、スマフォのアプリであったり、最近は様々なシーンで使われるようになったUIコントロールが「カルーセルスライダー」。主に写真であったり、プレゼンのスライドのようなものであったりに使われていますが、いざ自前で実装すると、結構大変です。このカルーセルをVuetifyは簡単に実装できるだけでなく、配列に値をpushするだけで自動で追加項目が反映するVue.jsのBindの特性が活かせるコントロールでもあります。

ソースコード

JS側コード

  • calitemという配列を格納してる変数に、連想配列でsrcを指定したデータを格納しておく
  • Vue.jsにてdataのitemsにcalitemを指定するとカルーセルスライダー用のデータとして利用される
  • addPictureメソッドにて、この配列に新たな画像データを動的に1個追加する
HTML側コード

  • v-carouselにて、itemsの中身をv-forでループ、バインドされたデータをカルーセルで表示するようになっています。
  • 画像が追加されると自動的にVue.jsにてそれらは動的に取り込まれすぐ反映してくれる。jQueryのようにリロードといったコードは不要となる。
  • サンプルでは、クリックすると、一番最後にハイビスカスの画像がカルーセルに追加されます。

サンプル表示

Navigation drawers

いわゆる、サイドバーのこと。例えば、ハンバーガーメニューをクリックすると左側から選択用のメニューが出てくるようなアレです。現代のアプリケーションを構築し、ナビゲーションをするには必須の項目ですね。MenusやLists、TreeViewを組み合わせてれば、リッチなUIを実現出来ます。

ソースコード

JS側

  • Drawerの開閉フラグは変数drawerが担当しています。
  • サイドバーの中身用のリストは、itemsの値から生成しています。
  • リストをクリックしたら発火する関数として、onListChangeを定義していて、そのタグ内のIDの値をalertで表示
  • onListChangeで引数で受けるIDはitemsで生成時にv-bindで動的に割り当てたものを利用しています。
HTML側

  • v-navigation-drawerにbottomを指定すると、サイドバーではなくアンダーバーになります。
  • v-list-itemではv-forにてitemsから値を取得し、v-list-itemを生成しています。
  • v-on:clickにてリストアイテムクリック時にonListChangeを実行しています。引数に$eventを指定すると、他の属性値(例えばidなど)を関数側で取得できます。
  • v-bindにて、itemsのargsをIDとして割り当ててます。
  • itemsのiconはMaterial iconの文字列を指定、textはリストのタイトル。それぞれをVue.jsで割当

サンプル表示

Autocomplete

少ない選択肢ならば、コンボボックスなどのv-selectを使って表現をすれば良いのですが、非常に多くの選択肢がある場合には選択肢が逆に利便性を損ねてしまいます。そこで利用するのが、オートコンプリート。ユーザの入力ワードでGoogleのサジェストのように機能するものです。

しかし、公式サイトにあるサンプルコードだと、単純な一次元配列の選択肢を表示するだけで、業務の現場で必要とされる

  • 選択肢の項目に複数の関連情報を表示する(社員番号と社員名など)
  • 選択したら、そのデータの塊を取得して、他の入力項目に流し込みたい(選択したレコードの取得)
  • 選択肢データはMySQLやSQLiteに格納済みデータをそのまま利用したい(JSONデータで選択肢にそのまま利用する)
  • 検索時は社員IDのみだけでなく、全レコードの列項目も検索対象にしたい。

といったものが満たせません。これらの要求を満たすようにAutoCompleteを実装してみたいと思います。1000以上ある選択肢でもユーザが絞り込んで選択できるスグレモノです。

ソースコード

HTML側コード

  • AutocompleteのChangeイベントにはseleresult関数を指定。引数には$eventを指定してあると選択したJSONレコードがまるまる取得出来ます。
  • 選択肢の項目はitemsという変数を指定しています(初期値は空っぽです)
  • slotのselectionには、選択したレコードのうち、今回はIDの値を残すようにしています。
  • slotのscopeには、絞り込み結果を表示するテンプレートを割り当ててます。v-cardで括らないと選択できないリストになってしまうので、注意。
  • 今回は検索結果には、社員番号と社員名、会社名の3項目を1つのCardとして表示するようにしてます。
  • $eventで取得したJSONデータの塊はそのまま、変数selerecに流し込みます。
  • autocomplete以外のテキストボックスなどは、selerecのJSONにv-modalで連結しておきます。selerecにデータが入ってきた時点で自動的にテキストボックスに値が反映します。
JS側コード

  • loading変数がAutocompleteの表示フラグを担当する変数です。
  • common変数はJSON形式での社員マスタの全データが入ってくる検索先の塊になります。
  • items変数がautocompleteと連結してるフィルタしたデータを格納する先になります。
  • watchにて文字が入力されたことを感知し、searchメソッドを実行します。
  • searchメソッドでは、querySelectionsメソッドに入力値を投げ入れます。
  • querySelectionsメソッドでは、変数commonの値に対してフィルタを掛けます。eにはcommonの1レコードが入ってきてループのように処理が連続で行われます。
  • eの値をJSON.stringifyで加工する事で、indexOfをしたときにレコードの全列を検索対象にする事が出来ます。
  • indexOfで検索値が存在した場合には、0以上の値が返ってくるので、それをもってtrueとすると、そのレコードは選択肢に表示されます。
  • 選択後にloadingをfalseにすることで、選択肢を非表示にしています。
  • seleresultメソッドでは選択後に、選択した対象のレコードのJSONをまるごとselerecに入れています。こうすることで、他のテキストボックスなどに自動的に値が反映されます。

図:こんな感じで選択肢の表示をフルカスタマイズ出来る

Simple Table

次項にあるような高度なDataTableではなく、ずらずらっと列挙するタイプのコンポーネントがシンプルテーブルコンポーネントです。高さなどが指定できる上にヘッダー部分の表示を固定化もできるので、例えばAccessで言うところのサブフォームのような表記が可能になります。コンポーネント内でスクロールバーが表示されて(DIVBOXみたい)列挙されてる値を閲覧可能なので、シンプルながら中々使い勝手の良いパーツです。

ソースコード

JS側コード

  • シンプルテーブル自体に関しては特にメソッド等はなく、vueを初期化すれば使えます。
  • 今回はレコード削除ボタンを用意してるので、deleteItem関数だけmethodsに加えています。
  • 実際に現場で利用する場合は、新規に追加するコード、追加時に値のValidationや合計作業時間の合計などを計算するメソッドを用意します。
  • recdataに対して値を追加したり削除することで、テーブル側の表記が変わります。
HTML側コード

  • fixed-headerにてタイトル行を固定化出来ます。スクロールしてもタイトル行は常に表示の状態になります。
  • Data Tableと異なり内容はTableタグそのもの。なので、THEADやTR/TD、TBODYなどを記述して構築します。
  • テーブル内容はデータの塊であるdesserts変数の中身をv-forにて取り出し、各セルにはめ込んでいく形になります。
  • 今回はレコードを削除する為のdeleteItem関数を割り当てたボタンをアクション列に用意しています。

図:農作業タスクの時間を登録してみた

Table

jQuery系で自分がよく使ってるものに、「jQuery DataTables」という非常に優れたプラグインがあるのですが、これをVuetifyのTableコンポーネントで実現する事が可能です。また、Vue.jsであるので、データの入れ替え等にまつわる動作がとっても楽ちんで、jQueryのように更新だのなんだので独特のコードを書かずとも変数入れ替えるだけでOKなので、とっても便利です。

ソースコード

JS側コード

  • headersでは、列名やソートの有無、フィルタ可能かどうかを追加できます。filterableでfalseを指定した列は、検索対象外になります。また、valueは連結するデータ(今回で言えば、dessertsがそれになる)のどの値を繋げるかを指定します。
  • 初期化後にvm.dessertsに対して、JSON形式のデータを投げ込めば、値の入れ替えや追加が可能です。
HTML側コード

  • Table本体は、v-data-tableのタグの部分のみ。そこへ検索用のテキストフィールドを追加しているだけです。
  • 標準だと日本語化されていない場所(items-per-page-textなどの部分)は、footer-propsの中で定義して置き換えることが可能です。Rows Per Pageを1ページ毎のレコード数に今回置き換えています。
  • 移動用のアイコンも同様に標準のものから、マテリアルアイコンを指定して置き換える事が可能です。
  • マルチソート、グルーピング、行選択などなど、幅広いオプションが用意されているので、気にいる形式に仕立て上げる事が可能です。社員マスタなどのマスタ系を弄るのに向いていると思います。

図:ダミーデータを10件ずつ表示してみた

常に全件表示とリサイズ対応

このTable機能非常に便利なのですが、デフォルトだと10件ずつ表示であったり、また、100件表示したら画面に収まらずまた、高さがウィンドウに合わせてくれません。そこでこれらに対応して

  • リサイズ時にv-data-tableの高さを自動調整する
  • 常に全件を表示するようにする
  • ヘッダーは固定化してスクロールしても常に表示

を装備したいと思います。simple-tableでも良いのですが、あちらはソートが出来ない等やはり不便なので、tableにてsimpel-tableの表現を実現するのが目的です。

※headerのJSONデータにwidthを入れておくとtableの各列の幅を規定する事が可能です。

HTML側コード

  • id=gridareaというdivでv-data-tableを囲っておきます。また、このdivにv-resize時イベントとして、updateDateTableHeightという関数を割り当てて起きます。
  • なお、gridarea自体をリサイズ時に画面に合わせる場合はjQueryで以下のようなコードを記述しておきイベント発生時には常に実行するようにします。
  • v-data-tableのheightは、:height=”dataTableHeight”とし、変数の値で動的に変化するようにしておきます。
  • fixed-headerをつけることでスクロールしてもヘッダ部分は常に固定表示されます。
  • hide-default-footerにてrows per pageなどのフッター部分を非表示にします。
  • :items-per-page=”-1″を指定する事で、常に全件表示になります。

JS側コード

  • データ取得して返り値をrecdataに入れるonSuccess関数については記載を省略しています。
  • v-resizeでupdateDataTableHeightが実行されて、リサイズした際のフィットするサイズをdataTableHeightへ入力しています。
  • mountedにてマウント完了後にGAS側へデータを要求するようにしています。

図:通知一覧などの全件を表示したい場合に利用する。

テーブルの値をその場で修正したい

テーブルに表示されてる内容のうち、1個だけをその場で修正したいみたいな事例は結構あります。レコード全部を修正可能にしたいのであれば、別途Dialogを用意して値を呼び出して保存する仕組み等を用意すれば良いですが、インスタントにそこだけちょっと編集したいといった場合には、ちょっとこれでは大掛かりです。

そこで用意されてるのがDataTableに用意されてるv-edit-dialog。これを既存のv-data-tableに加えてあげれば、その場でサクっと修正する機能をつけることが可能です。

HTML側コード

  • 今回はrecdataの中にあるtimevalという列の値を編集対象にしているので、v-slot:item.timevalとして設定しています
  • saveやcancelにメソッド割当がありますがなくても問題ありません。自分の場合、セーブをした場合には、例えば登録データのtimevalの合計を計算してプロパティに再格納などを書いていたりします。
  • 標準だと英語表記になってしまうので、cancel-textやsave-textで表示名を変更しています。
  • :rulesでvalidationの指定も可能です。
  • 本体側のDialogで提出をしない限り、この変更が大元のデータ群にマージされないようにしてあります。
  • 保存を実行した時になにか追加のコードがなくても、きちんと保存はされます。

図:時間の文字をクリックすると直接編集のダイアログが出てくる。

レコード編集時の注意点

前項にてアクションにボタンを追加していますが、このうち編集を行う関数として、viewrec関数をサンプル中に記載しています。この時例えば別途用意したダイアログなどで編集を行う場合には、そのレコード用の一時的な変数に値を代入し、ダイアログの各フォームパーツにv-modelで参照するようにするわけなのですが、この時、viewrecには引数としてitemがつけられています。このitemは1行分のレコードデータのみが入っています。

問題なのは、このitemをそのまま一時的な変数に入れて編集をすると、「大元のレコードデータが直接書き換わる」点です。1行分しかデータが入っていないのですが、このitemは大元のレコードの参照になっているので、まだ保存する気が無いのに大元のデータが編集されてしまうので問題です(VBAで言うところの参照渡しみたいな状態)。この場合一時変数にはitemの値を直接入れずに以下のようなスタイルで入れるようにしましょう。

  • itemはJSON.stringifyにてjsonという変数に格納する
  • このjsonの値を一時的な変数であるにJSON.parseしてから入れてあげる

これだけです。これでVBAで言うところの値渡しの状態になるので、ダイアログ上で編集をしても大元のデータが書き換わったりしなくなります。直接書き換わるほうが便利なケースもありますけれどね。ちなみに、ダイアログ側は以下のような形にしてあります。

  • v-modelにより一時的な変数temppjを参照させています。
  • temppjは前述のコードで大元のデータの塊であるitemからのデータをdialogを開くときに入れています。
  • 別途セーブするボタンを押した時に、はじめて大元のデータ塊に対してマージする仕組みが別途必要です。

データを入れ替えた時にスクロール位置をトップに戻す

v-data-tableなどの値をダイナミックに入れ替えた時、その時点でのスクロール位置からそのままではトップ位置に戻ってくれません。そのためそのままだとユーザはいちいちトップまでマウスで掴んでスクロールさせて上げる必要があります。

これでは非常に不便ですが、v-data-tableにはそのような問題を解決するメソッドも用意されていません。そこで使うのが、やはりjQuery。v-data-tableはclassとして、「.v-data-table__wrapper」が指定されています。また、v-data-tableにidを指定しても良いでしょう。この時、このテーブルに対して以下のコードを実行することで、v-data-tableの一番上の位置までスクロールバーを戻してくれます。

地味に結構遭遇するパターンなので、jQueryもまだまだ手放せないですね。

列を固定化する

v-data-tableには現在、列を固定化して横スクロール時にそのまま表示しつづける為のオプションが存在しません。2018年から要望は出ているものの現在まで搭載されず、フォークしたデモでは実装してる方もいるのですが、本プロジェクトにはマージされていません。

v-data-tableの実体はtableそのものなので、CSSで固定化するのが現実的な方法なのですが、1列の場合と複数列の場合で結構苦労しました。以下のようなコード体系で実現が可能です。

CSS側コード

  • 2列目の固定は別に用意が必要。違いは、leftの値。1列目の幅(headersで1列目のヘッダの幅を指定しておく)の分だけ、指定しておく必要がある
  • ヘッダ部分だけでなく、セルの部分も固定化しないと、ヘッダだけが固定化してスクロールしてしまうので注意
HTMLコード

  • fixed-headerはつけておいたほうが良いでしょう(これは縦スクロールでヘッダを固定化するオプション)
  • 外側のIDがgridareaのDIVで今回は括っています。このIDを使ってCSSで指定しています。

図:2列固定化しました

多数のレコードを表示すると激重になる

Vuetifyのv-data-tableにて、非常に多数のデータを一度に表示してみるとわかるのですが、アプリ全体の動きが激遅になります。だいたい500件表示を超えた辺りから、だんだんモッサリしてきて、10000件表示だと表示されるまでも相当待たされた挙げ句、スクロールがもたついて正直使えるレベルではありません。

もともとページネーションで数十件ずつ表示などの運用が前提のコンポーネントなので、このような使い方をしたい場合には、素直にページネーションを利用するか?Cheetah Gridのような高速なグリッドコンポーネントに乗り換えたほうが、小細工をするより遥かに楽になれます。

他にもこちらのサイトにあるように、様々なGridコンポーネントが存在します。

Validation

フォームの入力内容がきちんとボックスの内容に適合してるものなのかチェックする為の機能がvalidation。例えば数値しか受け付けたくない場所に文字や、メールアドレス欄なのにRFCの規則に従っていないメアドとか、半角全角、入力必須などがValidationの対象です。この機能を使わないとなると、サーバに送信時などで全部の入力欄について自分で一個ずつJavaScriptで検査してNGなら止めるみたいなコードを書かなければなりません

少数ならそれでも良いのですが、業務用アプリケーションのように入力欄の多い場合、非常にメンテナンス性が落ちます。これを簡単にしてくれるのがValidation機能です。Vuetify標準で搭載されてるものの他に、Vue.jsのプラグインとして、VeeValidateやVueValidateといったValidation専用のプラグインもリリースされています。

ソースコード

HTML側コード

  • 保存ボタンにはsaverecord()を呼び出すように割り当てて起きます。(保存時に一括Validationチェックもやらせる為)
  • 各入力欄のv-text-field等やv-select等には、:rules=[rules.required]などを割り当てます。これがValidationチェックの項目で、JS側で定義しておく必要があります。複数割り当てる場合は、配列内でカンマ区切りで、[rules.required, rules.numchk]といた具合に割り当てることが可能
  • rulesが定義されていない入力欄はValidationの対象外になります。
  • 全入力欄は必ずv-formでくくり、ref=”formman”のように名前をつける事が必要です
JS側コード

  • vuetifyの初期化のdataの中にrulesは定義します
  • rulesの中に複数タイプのValidationルールを定義し、HTML側はrules.numchkといった形で呼び出します。
  • required: value => { ここに処理 }といった形で構文を定義し、valueに対してチェックを掛けます。最後にreturnで返すのですが、trueもしくはfalseのどちらかの値を返すようにします(falseでValidationはNGとなる)
  • Validationはほとんどの場合で正規表現を使って検証します。
  • 業務用だと必須項目、メアド、日付の形式、カタカナ、ローマ字、数値で概ねValidationのチェックは可能(数値の場合は文字数カウントなんてのもありますね)
  • 保存ボタンに割り当てたsaverecord()はmethodsに定義する。Validationは入力中でも働くのですが、送信時に一括でチェックも可能。
  • this.$refs.formman.validateにて一括チェックします。formmanがformに割り当てた名前になります。
  • validateのチェック結果、全部が適合してると判断されるとtrueが帰ってくるので、これをもとにGAS側へデータを送って書き込ませるようにします。

図:入力欄チェックはデータの整合性を保つのに必要

Expansion Panel

いわゆるアコーディオン式に畳めるパネルメニューです。クリックする事で開かれば広がり、普段はたたまれているので、効率よくコンテンツを収納出来るUIパーツの一つ。v-ifやv-forと組み合わせて、配列からパネルデータを生成しながら使うシーンが多いと思います。

HTML側コード

  • multiple指定で複数パネルを同時に開く事が可能です。
  • v-forで配列データからループでパネルを構成しています。

JS側コード

  • v-expansion-panelsでv-modelにてpanelを指定しておいて、panelにて配列にて数値を指定すると、指定番目のパネルをデフォルトで開いた状態にしてくれます。
  • 今回はv-forで動的にv-expansion-panelを生成してるだけなので、0番目と1番目が開いた状態に。

図:こんな感じのパネルを生成可能です。

組み合わせの妙技

テキストボックスとカレンダー連動+α

非常に便利でリッチなUIを提供してくれるVuetifyですが、公式ドキュメントだけでは実務では困るケースが結構あります。特に今回社内向けアプリを作ってて困ったのが、テキストボックスとカレンダーの連携。困った点は以下の通り

  • テキストボックスクリックでカレンダー表示だと手修正時に面倒(ボタンを別途用意する必要性)
  • カレンダーを要するものが1個ならばVue.js内に用意した変数と連動で良いが、たくさんある場合にはこの手は非常に面倒(カレンダーの数だけ用意が必要)
  • Vuetifyのカレンダーは、yyyy-mm-ddの形式でなければならないが、テキストボックス内に表示されるのはyyyy/mm/dd(このまま渡すとエラーになる)
  • 値を読み書きする対象は、単一の値ではなくJSONの塊であるため、ロジックが必要(テキストボックスへの連結はv-modelで連結で良いのだが・・・)

これらを解消するにはちょっと複雑な仕組みが必要です。

ソースコード

HTML側コード

  • 入社日付テキストボックスとはv-modelにてJSONデータであるselerecのjsondateに連結
  • テキストボックス、ボタン、カレンダーであるv-date-pickerはv-menuで囲っておく(これ全体が一つの塊になる)
  • ボタンクリック時には、カレンダーを呼び出すonBindName関数をイベントとして割り当てておく(v-on=’on’も必要です)
  • v-date-pickerはv-modelにてpickerという後述のComputedのget, setの変数を割り当てておく(これが日付データの受け渡しを担当)
  • v-date-pickerはlocale=”ja-jp”で日本語化。しかし、日がくっついてくるので、:day-format=”date => new Date(date).getDate()”にて、綺麗にフォーマットする
  • CSSにてカレンダーの土日の文字色は変更可能です。
  • onBindName関数の引数には対象となるJSONのkey名を入れておき、読み書き対象の場所を特定できるように指定しておく。
JS側コード

  • グローバル変数binderにはonBindNameが取得した引数を格納。読み書き先のJSONのkey名を格納しておく(これにより、たくさんのカレンダー用の変数を用意する必要がなくなる)
  • onBindNameにエラートラップをしてる理由は、値が空である場合にエラーとなるので、これを回避する為。
  • onBindName関数にて、特定のJSONの日付データを取得。ただし、replaceを使って正規表現にて「スラッシュ」を「ハイフン」に変更して、dateに格納しておく
  • v-onにてカレンダーが開かれると、v-date-pickerが表示される。すると、v-modelで連結してるcomputedのPickerにあるgetが発火。dateに格納されてる日付データを取得し、カレンダーに反映する
  • カレンダー側で日付を選択すると、computedのpickerにあるsetが発火。日付データはvalに入ってるので、これを今度は「ハイフン」を「スラッシュ」に変更して、dateおよびJSONの対象のkeyの値に直接格納する
  • カレンダー外をクリックすると、カレンダーは自動的に閉じる。カレンダーの表示非表示用の変数もこれで省略しているので、たくさんの変数を用意する必要がない

ちなみに、selerecには、1レコード分の様々なデータがkeyと値のペアでたくさん格納されている。

図:この手は非常によく使うテクニック

テキストボックスとタイムピッカー連動+α

前項でカレンダーピッカーとテキストボックスを連動させましたが、同時に割と人事労務系だと使う機会のあるのが「タイムピッカー」。こちらも前項同様テキストボックス横のボタンをクリックするとタイムピッカーが表示されて選ぶと時刻をテキストボックスに入れることができるようにします。ただ使いやすいかどうかというと・・・個人的にはシンプルなVue.jsプラグインである「vue2-timepicker」のほうが簡単な気がします。

ソースコード

HTML側コード

  • 入社日付テキストボックスとはv-modelにてJSONデータであるselerecのworkmanに連結
  • テキストボックス、ボタン、カレンダーであるv-time-pickerはv-menuで囲っておく(これ全体が一つの塊になる)
  • ボタンクリック時には、カレンダーを呼び出すonBindName2関数をイベントとして割り当てておく(v-on=’on’も必要です)
  • v-time-pickerはv-modelにてpicker2という後述のComputedのget, setの変数を割り当てておく(これが時刻データの受け渡しを担当)
  • onBindName2関数の引数には対象となるJSONのkey名を入れておき、読み書き対象の場所を特定できるように指定しておく。
  • v-time-pickerにはrefとしてpickerを指定しておきます。後のコードで利用します
  • v-time-pickerの表記は今回は24時間表記を使うので、format=”24hr”を指定しています。
JS側コード

  • グローバル変数binderにはonBindName2が取得した引数を格納。読み書き先のJSONのkey名を格納しておく(これにより、たくさんの時刻用の変数を用意する必要がなくなる)
  • onBindName2にエラートラップをしてる理由は、値が空である場合にエラーとなるので、これを回避する為。
  • v-onにてタイムピッカーが開かれると、v-time-pickerが表示される。すると、v-modelで連結してるcomputedのPicker2にあるgetが発火。timeに格納されてる日付データを取得し、ピッカーに反映する
  • タイムピッカー側で時刻を選択すると、computedのpicker2にあるsetが発火。指定のテキストボックスにhh:mmの形式で代入してくれる。
  • タイムピッカー外をクリックすると、ピッカーは自動的に閉じる。
  • タイムピッカーのバグ回避のために、再度同じテキストのタイムピッカーを開いた時に初期選択が「分」になってるのをリセットするために、this.$refs.picker.selectingHourをtrueにする。このためにv-time-pickerにrefとしてpickerを指定してある

図:こんな感じの時間選択が出てくる

タブとツールバーの連携技

自分のアプリでは、レコードをクリックすると、その詳細な内容を編集するダイアログを表示させ、その中ではジャンル毎に「タブ」でわけています。このタブをツールバーの中に配置しており、保存ボタンを別途用意しており、編集後に保存ボタンを押すことで、MySQL側へとデータを投げる仕組みになっています。

タブをクリックする事でジャンル毎の中身を切り替えて表示するのですが、各コンテンツ内容がダイアログの大きさよりも大きい場合、スクロールバーが表示されますが、ツールバーまでスクロールしてしまい、具合が悪いです。コンテンツ部分だけをスクロール出来るように細工が必要です。

ソースコード

ツールバーを固定化する為のCSS

  • ツールバー部分はposition:stickyにて上部に固定化する
HTML側コード

  • v-toolbarの中にv-tabsや各種ボタンを配置する
  • v-tab-sliderにてタブ下部のスライダーの色を指定する事が可能です。
  • 各タブのhrefには下部のコンテンツ部分のv-tab-itemの各valueの項目に切り替わる仕組みです。
  • v-toolbarにはCSSで固定化するためにclass=”fixed-bar”を指定しています。
  • <v-app-bar app dense fixed>をtoolbarにくくり、コンテンツ部分をv-contentでくくることで、似たようがことが実現はできますが、レイアウトが崩れたりするので、自分はCSSでの調整を利用しています。こちらのほうがスッキリ決まります。

図:標準のタグよりもCSS調整のほうが綺麗に決まることもある

Vue.jsのv-forとv-ifとの連携技

VuetifyはVue.jsを土台にGUIを実現する為のフレームワークであるため、Vuetifyだけで実現が難しそうに思える事であっても、Vue.jsとしての機能を工夫する事で、実現可能な事が多数あります。

例えばマスタにフォルダのリスト、サブマスタにそのフォルダに属するファイルのリストを記述したデータがあった場合、フォルダ毎にファイルを抽出してCardなどで表示するといったような場合がソレになります。但し、Vuetifyの各コンポーネントに於いて同時にv-forとv-ifを使うことは推奨されていないので、ここで工夫が必要になります。

  • 外側のコンポーネント(マスタ側)ではv-forでマスタデータを順番に回す
  • その内側のコンポーネントで、マスタ側の1個目のIDのものだけを表示する為に、v-ifで判定する
  • 次に、その内側のコンポーネント(サブマスタ側)では、v-forでサブマスタデータを順番に回す
  • さらにその内側のコンポーネントで、マスタ側のIDとサブマスタ側の親IDとが一致するものをv-ifで判定させてレンダリングさせる。

この三段構えが必要になります。コード的には

となります。これでv-forでマスタ、サブマスタを回しつつ、両方に共通するIDで一致するデータをレンダリング用コンポーネントに描画するといった流れを構築可能です。コードだけで実現するよりも、逆に複雑に見えますが、Vue.jsの記法だけでデータの塊から条件に合うデータのみを抽出して表示が出来るので、結構使うシーンがあるのではないかと。

Vue.jsのプラグイン

この項目は、Vuetifyではないのですが、Vue.jsのプラグインとして、Vuetifyのような様々なUIコンポーネントを別途追加して、アプリケーションの機能を強化できるものを紹介しています(実際に自分のプログラムでも利用しています)。これらは、Vue.useで追加し、独特のタグでHTMLへ追加して使うのですが、非常に強力なプラグインがたくさんリリースされています。

使い方そのものは、Vuetifyと似ているのですが、一部はちょっと厄介な仕様なものもあったりするので、GASで使う場合にはテクニックが必要なものもあります。

v-tootip

Vueitfyにもtooltipの項目があるのですが、ややこしい上に動かないケースがあったりで困っていたのですが、v-tooltipというプラグインを利用することで簡単に実装できました。ツールチップの場所や細かい挙動などオプションもあったりして、地味な機能ながらアプリを華やかに演出してくれます。

※2021/1月現在、作者によるCDNでリリースされているコンポーネントがVue.js v3.0対応に変わってしまい、Vue.js 2.x系で使ってる人はCDNをロードしても動作しなくなっています。v2.0.3のv-tooltipをダウンロードし、v-tooltip.min.jsをアプリに組み込む形で利用する必要があります。

ソースコード

HTML側コード

  • CSSはツールチップのデザインを担当しています
  • ツールチップを表示したい項目にv-tooltip.bottom=でコメントを入れればOK。bottomの他にもtopとすれば上部に表示されます。今回のケースではアイコンの下に表示がなされます。
JS側コード

  • CDNでライブラリは提供されているので、vuetifyやvue.jsのJSファイルとともに追加すればOK
  • 追加したら、Vue.use(VTooltip)を実行すればそのHTML内でツールチップが有効になります。
  • 別途用意したjsフォルダ内に、v-tooltip.min.jsを格納してから呼び出しています。CDNは利用していません(CDN側はVue.js v3.0対応となっているため、互換性がありません)

図:アイコンにマウスカーソルで表示されます

Vue-loading

Vuetify標準でも、いわゆるプログレスサークルことローディングサークルな機能は備わっているのですが、もっと手軽に装備できるものとして、Vue-loadingがあります。例えば、GAS側からデータを取得して反映するまで待機する時であったり、重たい処理をする時には、これが備わっているとクールですね(ユーザ側もなにかしてると認識できるので)

非常にシンプルな作りなので、余計なオプション設定もほとんどなく、簡単に利用できます。

ソースコード

ヘッダ

読み込ませるCDNは以下の通りです。

HTML側コード

  • ローダーの表示は、:active.syncが担当。isLoadingがtrueなら表示、falseなら非表示になる
  • :is-full-pageの値がtrueの場合、全画面にローダー表示となります。普通はこれはtrueのままで良いです。
  • on-cancelがある場合、cancel時のイベントをmethodsに指定可能です。
  • can-cancelがtrueの場合、ローダー表示中にクリックするとローダーの表示を停止することが可能です。
JS側コード

  • Vue.useでまずコンポーネントを読み込ませましょう。
  • dataにはisLoadingとfullPageの2つのプロパティを用意
  • componentsでは、Loadingでコンポーネントを指定しておきます
  • onCancelがキャンセル時イベント

随所でローディングサークルを表示したい場合には、上記のコードであれば、vm.isLoading = trueで表示、vm.isLoading = falseで非表示となります。表示したのに、データ反映後にfalseにしないと表示されっぱなしになってしまうので、注意が必要です。

図:真ん中にサークルがくるくる周り背景は薄くなる

VueCtkDateTimePicker

Vuetify標準のタイムピッカーは正直使いにくいです。また、vue2-timepickerも使ってみたのですが、実体はinput要素であり正直、Vue.jsの概念からすると扱いにくかった。ということで自分が採用したのが、VueCtkDateTimePickerです。割と簡単に時間のチョイスができるので、おすすめです。

ヘッダ

読み込ませるCDNは以下の通りです

HTML側コード

  • いくつかオプションはあるものの、時間を選択するのであれば、このコードがベスト
  • また、DatePickerでもあるので、複合的な日時の選択用モジュールとしても利用できます。
  • formatやformattedでは、HH:mmで指定します。hh:mmの場合は00:30といった選択ができなくなってしまいます。

JS側コード

  • 基本的にはモジュールを初期化すればすぐに使えます。

図:今回は時間の選択機能だけで利用

FullCalendar

現在、別件で工数管理をしたいという要望が出てきたので、同じくVue.js + Vuetifyを利用して従業員の人に工数登録をしてもらうアプリケーションを構築中です。手際よく登録できるよう可能な限り手数を削って登録できるように構築しているのですが、そこでやはり欠かせないのが「カレンダー」。といっても、VuetifyのDate Pickerなカレンダーではなく、Googleカレンダーのようなフル規格のカレンダーが必要です(これがないと入力漏れや入力手間の削減が難しい)

そこで今回利用してみてるのが「FullCalenderライブラリ」です。Vue.jsのプラグインとして実装されているものなのですが、ちょっと他のプラグインとは違うコーディング方法が必要な箇所があるので備忘記録として残しておきます。

※今回はv4.x系を使っています。v5.0はまた大分コードが異なるので暇があったら実装したい。

ソースコード

Vue.jsの拡張プラグインとして、CDN配布されているので、vue.jsの読み込み後に以下のコードで追加で読み込むようにします。まずは、JSライブラリ

次にCSSをHEADセクションに追記しておきます。

追加のCSSとしては以下のようなものを表記のカスタマイズとして使っています。

HTML部分の記述

今回は、Vuetifyのタブの1つの中にカレンダーを表示しようと思っています。

  • Vue.jsではHTMLに色々イベント名を直書きしても、それらはすべて小文字として扱われてしまいます。そこで、scriptタグにてidがcalendarという名前でfullCalendarタグの中身をラッピングしています。
  • これは特に@dateClickのイベント名対策で、このラッピングを行わないと、HTML上に記述してもイベントが認識されません。
  • dateClickでhandleDateClickという関数が、カレンダーの1マスをクリック時のイベント発火が行われ明日。
  • localeは日本語を指定したいので、jaを指定しています。
  • custom-buttonsは後で出てきますが、既存の月移動のボタンをオーバーライドでちょっと上書きする形で指定してます。
  • カレンダーの高さはアプリのタブ内の大きさによって自動でフィットするように変数heightを指定してあり、リサイズ時にはサイズを正確に算出してこの変数に値を書き込ませています。
  • タグ側でeventLimitの設定を記述しておき、変数eventLimitがtrueの場合、マス目に入り切らないイベントがあった場合、「+2 event」といった表示でまとめてくれます。
JS側コード

  • グローバルのevents変数はカレンダーイベントを格納しておくためのものです。ここにスプレッドシートから取得したデータを流し込んだりするとカレンダーに反映します。
  • イベント発火などはキャメルケースでないと実行ができないので、独自のcalendarというIDをもったモジュールを作ってそこに対して、色々Vueの初期化を記述しています。
  • dateの中のheightでは、リサイズ時などにはカレンダーがフィットするようにサイズを自動調整するコードを記述してあります。
  • eventsにはグローバル変数のevents変数を割り当てておきます。外部からデータの入出力を行う上で重要です。
  • customButtonsではカレンダーの前の月、次の月のボタンの内容をオーバーライドする形でコードを記述。
  • this.$refs.fullCalendar.getApi()で、FullCalendarの各種API呼び出しが可能になり、カレンダーをAPIから操作出来ます。Vueを使っている為、古い記事にあるようなjQueryを使って操作するようなことは出来ません。
  • prevとnextの2つのボタンに対して、本来の機能以外の機能を追加しています。これからここにその月のデータを移動時に取得して、eventsに流し込むコードを記述する予定です。
  • calendarApi.getDateで移動時の1日目の日付を取得できるので、ここから月と年を取得しスプレッドシートからデータをJSONの形で取得し流し込むわけです。
  • events.pushでJSON形式であれば新たにイベントを追加登録が可能です。
  • methodsのhandleDateClick関数ではクリックした日付のマスの情報を取得可能です。これで登録済みデータを探索させて、ダイアログなどで編集や削除の機能を追加できますね。
  • データ量の削減の為に、基本表示してる月のデータだけを都度、スプレッドシートから取得して表示させ、移動時にその月のデータを取り直して、eventsの中身を書き換えるようにしています。
  • eventLimitはtrueにしておきます。falseの場合、マス目に収まらないイベント数があった場合、マス目が自動で拡張してしまいます。

図:まだ開発途中。これからもっとカスタマイズしていく

特定の日のマス目背景色変更

今回、該当の日付にイベント登録があるマス目の背景色を変更するというものを実装しました。FullCalendar自体にマス目の背景色のオプションが無く、イベント自体の背景色は簡単に実装できるのに意外と難儀しました。ちなみにイベント自体の背景色変更は、変数eventsの各値に以下のような感じでbackgroundColorプロパティと色指定をつけるだけです。

このマス目ですが、実は通常のHTMLのテーブルで出来ているので、イベントデータをロードした時に、以下のようなコードでtdに対してjQueryでクラスを付与し、対応するクラスのCSS側で色の設定をしておけば、OKです。

CSS

  • workmanという名前のCSS設定と色指定を用意しておきます。
HTML側コード

  • FullCalendarのタグに新たに「@eventRender=”eventRender”」を追加し、レンダリング時のイベントを付与する
JS側コード

  • vue初期化のmethodsに新たにeventRender(info)という関数を用意
  • この関数はイベントレンダリング時に1日毎に実行される。
  • 各日付のセルはデータが入ってる場合には、カスタムデータ属性としてdata-dateが付与されているので、それが存在するものにaddClassでworkmanを追加する
  • data-date属性yyyy-mm-ddの形式でなければならないので、infoから取得した日付情報を加工して合わせてあげる。
  • イベントの削除などの場合はデータを再取得するような書き方をすればOK。

図:データのある日は背景色が青になる

Google Calendarのカレンダー表示

FullCalendarはプラグインを導入する事で、Google Calendarを呼び込めるようになります。今回はGoogle提供の「日本の休日」カレンダーを取り込んで表示してみます。また、既存のeventsの内容と同居できるようにもし、休日名は日付の文字列の先頭に付与するようにもしようと思います。

事前準備

Google Calendarデータを利用するためには現在、Google Cloud ConsoleよりAPIキーを必要としています。そのためまずはこのAPIキーを取得します。以下の手順で取得します。

  1. Google Cloud Consoleへログインする
  2. 左上のメニューからAPIとサービス⇒ライブラリを開く
  3. 検索窓よりCalendarを検索し、Google Calendar APIをクリックする
  4. 追加ボタンをクリック
  5. 左上のメニューからAPIとサービス⇒認証情報を開く
  6. 認証情報作成をクリックし、APIキーをクリック
  7. キーが生成されるのでコピーしておく
  8. キーを制限をクリックする
  9. APIの制限にて、キーを制限にチェックを入れて、Select APIsではGoogle Calendar APIを選択し保存をクリック
  10. アプリケーションの制限はお好みで

図:APIキーは必ず制限をつけましょう

CSS

  • Google Calendarの休日の文字色と、今回はGoogle側イベントはイベントとしては非表示にするので、そのためのCSSを追加
HTML側コード

  • FullCalendarタグのオプションに、新規に:googleCalendarApiKeyと:eventSourcesを追加し、これまで使ってた:eventsは削除する
JS側コード

  • 追加でgoogle-calendarのプラグインであるmain.min.jsをロードする
  • 変数googleCalendarにプラグインをロードするコードを記述
  • calendarPluginsにgoogleCalendarを追加する
  • googleCalendarApiKeyにコピーしておいたAPIキーを記述する
  • 通常のeventsとeventSourcesは列挙してしまうと同居が出来ないので、eventSourcesの中にeventsはソースの1つとして記述する
  • 同じくeventSourcesにGoogle CalenderのカレンダーIDと割り当てるクラス名(今回はholidayman)を記述する。公開されていないカレンダーは読み込めません。
  • Calender API使用では認証は必要ありません。
  • 日本の休日カレンダーのIDは、ja.japanese#holiday@group.v.calendar.google.comとなります。
  • eventRenderイベントにて、イベントのclassにholidaymanが含まれていた場合は、マス目の先頭に休日名を追加するコードを記述します。

eventsとeventSourcesを併記してしまうと、月の移動時に休日名が消えてしまったり、そもそも表示されなかったりと不具合が発生します。2つ以上のソースを利用する場合は必ず、eventSorcesのみを使用し、中にイベントデータは併記する。複数のGoogle Calenderを利用する場合も同じです。その場合、Class名は分けましょう。

図:休日名がカレンダーに併記出来ました

一度にたくさんのイベントを同時に追加すると起きる問題

該当する日をクリックし、ダイアログを表示して複数のイベントを同時に6件ほど登録するコードを書いてみた所、なぜか表示されるのは4件だけで、月移動をするときちんと6件表示されるという謎の現象に遭遇しました。そこで、データを追加時にFullCalendarに装備されてるAPIを用いて処理をしたらうまく表示されました。

しかし、FullCalendarのVueとダイアログ側のVueは別々に初期化してるので、以下のようにFullCalendar側のAPIをグローバルから扱えるようにします。

calapiはグローバル変数です。これを他のメソッドより扱い、データの再取得時の処理で以下のようにしました。

地味に嵌ってました。1件ずつだとこの処理をせずともきちんと、データの再取得で表示されるのですが・・・・

VuetifyのCSSが邪魔してしまうシーン

これらプラグインとVuetifyをあわせて使ってる場合、時としてVuetifyのCSSが効いてしまい、自分がカスタマイズをする際に困るシーンがあります。特にv-appで指定される.v-applicationのCSSは面倒で、この場合、CSSを別途定義して一度設定をクリアしてから再度、別のclass指定でCSSを定義するようにすると機能する事があります。

  • 上記はあるボタンの文字色に関するCSS。v-application指定の文字色だと困るので、一度color:unsetで解除してます
  • 2つめの設定はall:unsetで全CSS設定を解除した上で、新たに別のCSSを追加で設定している事例です。

VuetifyのCSSは複雑に入り組んでいる為、必ずしもこれで思った通りの表示になるわけじゃないのですが、どのCSSが効いてしまってるのか?を見つけて一つずつ解除してあげるのが肝要です。

Cheetah Grid

業務用アプリケーションを構築する上で、特にPC向けで構築する場合にほぼ必須になるコンポーネントが「Grid」。VuetifyにもGridはあるのですが、欲しいのはこういうものじゃない・・・これまでは、w2ui-gridやSlickGridなどを使ってましたが、前者は以前は高速だったのですが、現在は重く切り替えも非常にもたつく、後者は高速なのですが既にメンテされていないし、構築が結構面倒。

ということで見つけたのが、Cheetah Grid。Vuetifyじゃなく、Vue.jsのプラグインとして実装されているものになりますが、マテリアルデザインで表現されます。DOMではなくCanvasで描画している為、非常に高速で大量のレコードデータを表示しても、スクロールや操作でブラウザがもたつく事がありません。

列の固定やマルチプルカラム(複数列にまたがるカラムタイトルを作れる)などが可能で、非常に高速に表示が出来るというので、手を出してみました。デモページはこちらになります。Vue.jsとの相性も良いみたいです。表示する為のGridのデータの仕様はこちらになります。Vue.jsで使う場合のドキュメントはこちら。

ソースコード

Vue.jsの拡張プラグインとして、CDN配布されているので、vue.jsの読み込み後に以下のコードで追加で読み込むようにします。

GAS側コード