Google Apps ScriptでVuetify v2を使ってUIを作る【GAS】
Google Apps Scriptは、HTML ServiceでウェブアプリケーションやスプレッドシートのダイアログのUIを構築可能です。しかし、標準ではUIを構築する為のライブラリやフレームワークを備えていないので、ユーザ自身が好きなフレームワークを導入して、HTML上に構築する必要があります。
自分は過去に、Google Apps Scriptのアプリをスマフォ向けとしては、framework7を使ってUIを構築しました。いずれ、framework7のまとめも作ってみたいと思います。今回はPC向けサイトとしてVuetifyを使って構築してみたいと思います。随時、自分がパーツを使うシーンがあれば、ここに追記していきたい。
※2023年12月31日をもってVue.js v2がサポートアウトする関係で、Vuetifyについてもv3移行が促されいます。基本的な使い方は同じですが、変更されてる部分もあるため、v3については以下のエントリーを参考にv2から移植を行ってください。
図:現在作成中のElectronアプリでも使ってます
目次
- 1 今回使用するライブラリ等
- 2 事前準備
- 3 色々なコントロールを使ってみる
- 3.1 Cards
- 3.2 Snackbar
- 3.3 selectbox
- 3.4 Checkbox
- 3.5 Appbar
- 3.6 Bottom Sheet
- 3.7 Dialog
- 3.8 Carousel
- 3.9 Navigation drawers
- 3.10 Autocomplete
- 3.11 Simple Table
- 3.12 Table
- 3.13 Validation
- 3.14 Expansion Panel
- 3.15 List
- 3.16 Chips
- 4 組み合わせの妙技
- 4.1 Expantion Panel内のリストをv-forで作る
- 4.2 テキストボックスとカレンダー連動+α
- 4.3 テキストボックスとタイムピッカー連動+α
- 4.4 タブとツールバーの連携技
- 4.5 Vue.jsのv-forとv-ifとの連携技
- 4.6 Cheetah Grid
- 5 関連リンク
今回使用するライブラリ等
- Vuetify - A Material Design Framework for Vue.js
- サンプルスプレッドシート
- MaterialDesignIcons.css Version 7.2.96
VuetifyはVue.jsのUIフレームワークになるので(ちょうど、jQueryとjQuery UIの関係になる)、Vue.jsも利用する必要があります。
※ダミーデータはOnline test data generator等で作成したものを利用しています。
※MaterialDesignIconsの検索はこちらのサイトを利用しています。
事前準備
ウェブ上で公開されてる事前準備では、vueファイルの作成であったり、vue-cliを使うためにnpmでインストールといったような説明書きをよく見かけますが、そもそもjQuery同様にvue.jsやvuetifyは単独で利用出来るようになっているので、あの手順にしたがってプロジェクトを作る必要性はありません。また、Google Apps Scriptでは当然vueファイルやらnpmでインストールなんてできません。
Google Apps ScriptではいくつかのCDNで公開されてるライブラリをロードすることで、コード内で利用できるようになります。こちらのページのUsage with CDNがそれに該当します。
Headerタグ内でCSSをロード
1 2 3 4 5 |
<head> <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> </head> |
- Header部分ではCSSを読み込みます。
- 他にもGoogle FontsやViewportの設定も指定が必要になります。
Bodyタグ内の一番下でJSをロード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<body> ・・・・前略・・・・ <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <!-- Vuetifyを初期化する --> <script> new Vue({ el: '#app', vuetify: new Vuetify(), }) </script> </body> |
- 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を指定するとウェブアプリのアイコンが指定のアイコンになります(サンプル)
1 2 3 4 5 6 7 8 9 10 11 12 |
function doGet(e){ //パラメータを取得する var param = e.parameter.param1; //paramを元にHTMLをレンダリング var html = HtmlService.createHtmlOutputFromFile(param) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .setFaviconUrl('https://drive.google.com/uc?id=1rPShWER6M-z0IxcVSFa6Cx1MLPE-6MCf&.png') .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'); return html; } |
色々なコントロールを使ってみる
Vuetifyのサイトには様々なUIコントロールのサンプルが掲載されています。しかし、これらのコードをそのままGoogle Apps ScriptのHTML Serviceで動かそうとした場合、動かないケースがままあります。今回はいくつかのコントロールをテストしてみて、実際にこれなら動くという形で、ちょっとだけ変えてあります。
非常に沢山のコントロールと、それぞれにオプションやテクニック、APIが用意されているので、表現力豊かなアプリケーションが構築可能です。
以下のコードはHeaderやBody下部の共通部分については省略しています。
Cards
地味によく使う、タイルのように扱えるコンポーネントです。他にもレイアウト関係のコンポーネントはたくさんあるのですが、その中では随一の使用率。それがこのCardsです。ダイアログであったり、Amazonの商品一覧のように並べたり、細かいテクニックも必要です。
Cardsそのものは他のコンポーネントのように特別スクリプトを必要としないのですが、Cards内でのボタンなどで必要とするケースが多々あります。今回は縦にCardsを掲示板のスレッドのように列挙してみます。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<v-container> <v-card> <v-card-title class="headline grey lighten-2"> Privacy Policy </v-card-title> <v-card-text> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="dialog = false" > I accept </v-btn> </v-card-actions> </v-card> </v-container> |
- これで1枚分のCardsの表示になります。
- v-containerで括れば、v-cardを縦に列挙した場合、Cards同士の間にきちんと隙間が出来てそれっぽく表示されます。ない場合は密着してしまいます。
- v-dividerで区切り線を表記します。
- v-card-text内にメインコンテンツを記述します。
- v-card-actionsがボタンなどを表記します。
- v-cardのレイアウトの作り方は多種多彩なので、様々なパターンを会得すると表現力がアップします。
- データをもとにCardsを自動生成する場合(v-forなど)やボタンのアクションは別途スクリプトが必要です。
図:Cardsを縦に並べてみた
きれいに並べてみる
カードをきれいにタイル状に並べるには、地味にテクニックが必要です。普通にv-rowとv-colで並べてもきれいに並ぶこともないですし、ウィンドウのリサイズ時に合わせてレスポンシブに横のタイル数が変化する事もありません。そこで、これらをきれいに並べるには以下のようなHTMLとCSSでの調整が必要になります。
CSS側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* vuetifyのcontainerクラスのCSSをリセット */ .container { margin-right: unset !important; margin-left: unset !important; max-width: unset !important; } /* vuetifyのレスポンシブタイル配置の調整用 */ .col { padding: unset !important; padding-bottom : 5px !important; } .box_srcollbar2 { overflow:auto; width:100%; } |
- Vuetify標準のCSSですと、タイル間の隙間が広がりすぎてたりするので、これを詰めたり、タイルエリアのスクロールエリアの為のCSS調整が必要です。
HTML側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!-- 作業項目を列挙する --> <div id="gridarea2" style="width: 100%;" v-show="acpanel == 1" v-resize="updateDataTableHeight"> <div class="box_srcollbar2" :height="dataTableHeight" id="boxman2"> <v-container> <v-row cols=12 sm=10 md=8 lg=4 xl=3 justify="center" align-content="center"> <v-col v-for="(item, i) in items" :key="i"> <v-card outlined tile hover height=300 width=350 class="d-flex flex-column" height="100%"> <v-img :src="item.imgurl" height="50px" ></v-img> <v-card-title class="titlehead2" v-text="item.hospname"></v-card-title> <v-card-text v-text="item.description"></v-card-text> <v-spacer></v-spacer> <v-card-actions> <v-card-text v-text="item.area"></v-card-text> <v-spacer></v-spacer> <v-btn class="ma-2 white--text blue" @Click="select($event)" elevation="2" > 開く </v-btn> </v-card-actions> </v-card> </v-col> </v-row> </v-container> </div> </div> |
- box_scrollbar2にて、div内にスクロールバーを追加しています。
- リサイズ時にbox_scrollbar2エリアの縦横サイズをフィットするようにupdateDataTableheight関数をv-resizeで呼び出しています
- v-rowでは画面のサイズ毎に1行に並べるタイル数を指定しています(リサイズ時にこれに応じてタイルの数が変わる)
- v-colでは、itemsに格納されたリストデータに応じてv-cardをレンダリングさせています。
- v-cardではclass指定でd-flex, flex-columnが必要です。また、オプション指定でtileやoutlinedも必須
- あとはv-cardの中身を記述していく。縦横のサイズとv-rowのタイル数は関係してくるので、注意
- v-cardにflatプロパティを追加すると、縁取りが消えます。レイアウト構築の場合縁取りは邪魔だったりするので付けておくと良いでしょう。
JS側
1 2 3 4 5 6 7 8 |
//リサイズ時にdatatableの高さを変更する updateDataTableHeight() { //simple-tableの高さを動的変更 var layoutHeight = $(window).height() - 100; this.dataTableHeight = layoutHeight; $('#gridarea2').css('height', layoutHeight + 'px'); $('#boxman2').css('height', layoutHeight2 + 'px'); } |
- リサイズ時に呼び出されるbox_scrollbar2とgridarea2のサイズをウィンドウに合わせるコード
図:こんな感じにレスポンシブに並べる
Snackbar
一般には、Toastと呼ばれてる短いメッセージを画面の下部などにフワっと表示させる為のもの。いちいちalertなどのダイアログを出すまでもないような「作業完了」や「データ無し」などの簡易なメッセージはこれらを使って実現すると、ユーザがいちいちOKボタンクリックといった鬱陶しい作業をしなくてすみます。スマフォなどでは結構おなじみの機能ですね。
ソースコード
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- メッセージ Toast--> <v-snackbar v-model="snackbar" :timeout="timeout" > {{ text }} <template v-slot:action="{ attrs }"> <v-btn color="blue" text v-bind="attrs" @click="snackbar = false" > 閉じる </v-btn> </template> </v-snackbar> |
- v-modelにて連結してるsnackbarの値がtrueの時に表示されます。
- :timeoutにて自動で消える秒数(ミリセカンド)を指定しています。
- 閉じる為のボタンもつけていますが、タイムアウト指定で自動で消えるようにもしています。
JS側コード
1 2 3 4 5 6 7 8 9 |
vm = new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ snackbar: false, text: 'データがありませんでした', timeout: 3000, }), }); |
- テキストはtextの値を変更すれば変える事が可能です。(vm.text = "とまと食べたい"などで変更可能)
- timeoutの秒数は3000msを自分は指定しています。
- timeoutは-1を指定すると自動的に閉じなくなります。
実際に表示する場合は、上記のコードであれば、vm.snackbar = trueで表示、vm.snackbar = falseで非表示となります。指定秒数経過後には自動でfalseになります。
図:画面下部にフワっと表示されフワっと消える
注意点
fullscreenなDialogなどを表示してるケースで、snackbarを表示し、snackbarの閉じるボタンをクリックすると、Dialogのほうまで閉じてしまうバグ?のようなケースがあります。この問題の回避方法はStackOverflowに投稿されていましたが、ダイアログ側のプロパティとして「persistent」を追加する事で、snackbarの閉じるボタンをクリックしても、Dialogまで閉じられてしまうような事を回避する事が出来ます。
これは、snackbarがDialogの外側に位置してる為、クリックするとDialogの外側をクリックされたと判定され、Dialogが自動で閉じてしまう問題で、persistentを追加する事で、ダイアログの外側をクリックされても、閉じるのを阻止してくれる為、特にfullscreenなダイアログとsnackbarを併用する時には必須となります。
1 2 3 4 5 |
<v-dialog v-model="dialog" fullscreen hide-overlay persistent transition="dialog-bottom-transition"> //ここにダイアログの内容を記述する </v-dialog> |
selectbox
UIパーツの中でももっとも基本的なパーツがv-selectタグを使ったコンボボックス or ドロップダウンリストです。このリストは表示する値とは別に反映する値を別に設定出来ますし、複数列を持つJSONの値をセットして運用することもできるのですが、Vuetifyのバージョンによるものなのか、不可解な動きをすることもあるので注意です。
ソースコード
HTML側コード
1 2 3 4 5 6 7 8 9 10 |
<v-select :items="kaigai" item-text="funin" v-model="selerec.funinsaki" item-value="ID" label="出張先" hint="企業名を選択" v-on:change="funinchg" persistent-hint return-object> </v-select> |
- 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
1 2 3 4 5 6 |
[ {ID:1,funin:'米国'}, {ID:2,funin:'英国'}, {ID:3,funin:'ベトナム'}, {ID:4,funin:'インドネシア'} ] |
非常に単純な、ID列とfunin列の2つのカラムで構成されています。funinchg関数では、セレクトされるとfunin列の値を別のtext fieldに入れるようにしています。
JS側コード
1 2 3 4 5 6 |
methods: { funinchg(e){ //赴任先を選んだら関連項目を他のエリアに代入する this.selerec.funin_country = e.funin; }, } |
- vueのmethodsにfuninchg関数を用意。
- eでは選択した項目のJSON値が全部入ってるので、ここからfuninの値を取り出します。
- 取り出した値は直接選択レコードのJSONに格納すれば、v-modelで連結してるv-text-fieldに表示されます。
- この手法は、HTML側でreturn-objectを指定しておく必要があります(そうでないと、選択した項目のJSON値が取得できない)
注意点
値がobjectで入ってしまった
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に入れるようにするなどの迂回策が必要です。
特定の値が出てこない
v-selectにて、grouparrという選択肢のJSONデータを格納してる際に、確かに格納されてるハズの特定の値が選択肢に出てこないケースがありました。格納されてる値は以下のような形
1 2 3 4 |
[ {"id":1, "busyo":"空手部","grouparr":"karate@test.com"} {"id":2, "busyo":"帰宅部","grouparr":"kitaku@test.com"} ] |
この値を取得してgrouparrという変数に格納しました。しかし、v-selectで以下のような形で記述をした際に、上記の項目のgrouparrのメアドが同じものがあった場合、そのどちらかが選択肢に現れないという現象に遭遇。
1 2 3 4 5 6 7 8 9 10 11 12 |
<v-select :items="grouparr" item-text="busyo" item-value="grouparr" v-model="affiliation" label="所属部署" hint="所属部署を選択" persistent-hint return-object :rules="[rules.required]" v-on:change="funinchg" ></v-select> |
上記のitem-valueの項目を除外し、v-on:changeにて選択した値をaffiliationに格納する形に修正したところ、問題なく表示が出来ました。おそらく変数名のgrouparrと要素名が同じgrouparrであったが為に発生した問題だと思いますが、要素のgrouparrが異なる値の場合はしっかり選択肢に出てくるので、重複した値をつけてると排除されてしまうのだと思います。
故にitem-valueをなくしてv-on:changeのfuninchg関数で迂回し、affiliationに格納することで正しい挙動にすることが出来ました。
Checkbox
GUIコンポーネントの中では最も基本的な項目であるCheckboxですが、VuetifyのCheckboxはオンオフや値の取り方・適用方法がちょっと普通とは異なる為、嵌りポイントが多いです。
ソースコード
1 2 3 4 5 6 7 8 9 10 |
<div id="app"> <v-app> <v-checkbox label="正社員" color="green" v-model="checked1" hide-details ></v-checkbox> </v-app> </div> |
- labelにチェックボックスのラベル文字を入れます
- colorでチェックボックスの色を指定する事が可能です。
- 現在、v-checkboxはv-modelで変数と連結し、true/falseでオンオフが出来るようになっています。
v-modelや:valueではなく、:input-valueでvueの変数と連結する必要があります。値のオンオフは、1がオン、0がオフとなるため、true/falseでコントロールが出来ません。
Appbar
アプリケーションの顔とも言える、アプリ上部にあるヘッダに該当するナビゲーションバーです。この他にもファイルやヘルプなどのツールバーなどもあったりします(もちろん、モバイル向けの下部ナビゲーションも)。ただし、サンプルのままで動作しようとすると、このナビゲーションバーにコンテンツ部分が被ったりと色々と面倒な部分もありましたが、以下のコードで動作させることが可能です。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<div id="app"> <v-app> <v-app-bar color="light-blue lighten-5" app color="#fcb69f" shrink-on-scroll scroll-target="#main_content"> <v-app-bar-nav-icon></v-app-bar-nav-icon> <v-toolbar-title class="ml-2"> 南国フルーツ図鑑 </v-toolbar-title> <v-spacer></v-spacer> <v-btn icon> <v-icon>mdi-magnify</v-icon> </v-btn> <v-btn icon> <v-icon>mdi-heart</v-icon> </v-btn> <v-btn icon> <v-icon>mdi-dots-vertical</v-icon> </v-btn> </v-app-bar> <v-sheet id="main_content" class="overflow-y-auto" max-height="600"> <v-main class="text-center ma-6" style="height: 1500px;"> ここにコンテンツを記述する </v-main> </v-sheet> </v-app> </div> |
- 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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<script> new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ sheet: false, }), methods:{ onApprove(){ alert("承認しました"); this.sheet = false; //BottomSheetを閉じる }, onReject(){ alert("却下しました"); this.sheet = false; //BottomSheetを閉じる }, } }) </script> |
- dataの中のsheetをフラグとし、falseで閉じます
- 承認と却下用のコマンドをmethodsに加えて起きます。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<v-app id="app"> <v-container> <v-row class="text-center"> <v-col col="12"> <div class="text-center"> <v-main class="text-center ma-6"> ここにコンテンツを記述する </v-main> <v-bottom-sheet v-model="sheet" inset> <!-- ボタンを表示する --> <template v-slot:activator="{ on, attrs }"> <v-btn color="blue" dark v-bind="attrs" v-on="on">ボトムを開く</v-btn> </template> <!-- ボトムシートの中身 --> <v-sheet class="text-center" height="200px"> <v-btn class="mt-6" text color="error" @Click="onApprove">承認</v-btn> <v-btn class="mt-6" text color="error" @Click="onReject">却下</v-btn> <div class="my-3">この申請を処理してください。</div> </v-sheet> </v-bottom-sheet> </div> </v-col> </v-row> </v-container> </v-app> |
- ボトムを開くをクリックすると、ボトムシートが開かれます
- 承認、却下はそれぞれがmethodsに規定したコマンドを@Clickにて実行し、this.sheet=falseで閉じるようになっています。
Dialog
アプリケーションで非常によく使う要素の1つがダイアログでしょう。jQueryなどを使わずにオシャレなダイアログを構築する事が可能です。その表現の数も非常に充実しており、よくあるYES/NO、フルサイズの設定用ダイアログ、ダイアログのネスト、プログレス表示用などなど、ユーザの要求を適切に受け取ることが可能です。
ソースコード
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
new Vue({ el: '#app', vuetify: new Vuetify(), data () { return { dialog: false, } }, methods:{ onApprove(){ alert("承認しました"); this.dialog = false; //dialogを閉じる }, onReject(){ alert("却下しました"); this.dialog = false; //dialogを閉じる }, } }) |
- コード自体はBottom Sheetとほとんど同じものになります。
- dataの中のdialogをフラグにして、falseの時に閉じます。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<v-app id="app"> <v-row class="text-center"> <v-dialog v-model="dialog" persistent max-width="290"> <!-- 承認ボタン --> <template v-slot:activator="{ on, attrs }"> <v-btn color="primary" dark v-bind="attrs" v-on="on"> 承認実行 </v-btn> </template> <!-- カードで整形する --> <v-card> <v-card-title class="headline"> この申請を承認しますか? </v-card-title> <v-card-text>承認を実行すると、本人にもメールで結果が通知され、申請書が発行されます</v-card-text> <v-card-actions> <v-spacer></v-spacer> <v-btn color="blue darken-1" text @click="onApprove"> 実行 </v-btn> <v-btn color="red darken-1" text @click="onReject"> キャンセル </v-btn> </v-card-actions> </v-card> </v-dialog> </v-row> </v-app> |
- 承認実行ボタンを押すとダイアログが表示されます。
- ダイアログの2つのボタンにそれぞれ@Clickで実行するmethodを指定しておきます。
注意点
フォーカスが奪われて入力できなくなる
ElectronでVuetifyを使い、ダイアログを使っていた際に遭遇したトラブルなのですが、保存ボタンを押しthis.dialog = falseをした後に、confirmにて選択のメッセージを出した場合以下のトラブルが生じました。
- 保存は実行され、再度ダイアログも開けるが、テキストボックスをクリックしてもフォーカスされず、入力ができなくなる
- ただし、カレンダーやセレクトボックスは正常に動作する
- 一度デスクトップをクリックして、再度Windowをクリックするとフォーカスして入力ができるようになる
- しかし、一時的に治ってるだけで、再度Dialogを閉じると同様の症状が発生する
回避策としては、Electronであるならば、メインプロセス側でdialog.messageにて表示して分岐処理をさせるようにする。Google Apps Scriptの場合は、confirmを使わず別の確認メッセージを表示する手段を利用するようにしましょう。
また、似たような症状で、CKEditorなどで表示したダイアログを合わせ技で使うとテキストボックスにフォーカスできなくなる現象もあります。回避策は以下のエントリーを参考にしてみてください。
Google Sitesとの相性が悪い
上記と同じくFullscreenなDialogを表示してる場合、通常のウェブアプリとして表示した場合は特に問題なく表示できるのですが、このウェブアプリに対してtransitionを「dialog-bottom-transition」としてる場合、Google Sitesにフルスクリーンで貼り付けると異様にダイアログの領域が小さく表示される現象が発生しています。また、フルスクリーン時にツールバーを加えているのですが、これも領域外にはみ出してしまい操作が出来なくなる。
このような場合はtransition指定を外して以下のようにすると現象が発生しなくなります。
1 2 3 4 |
<v-dialog v-model="dialog5" fullscreen persistent :retain-focus="false" no-click-animation> </dialog> |
これは、CKEditorでフォーカスが勝手にダイアログを奪われるというケースでも同様の手法を使っています。Google Sitesに貼り付けた場合にだけ起きる現象なので非常に特殊な事例ですが、transitionは外しておくのがよさそうです。
Carousel
ブログパーツであったり、スマフォのアプリであったり、最近は様々なシーンで使われるようになったUIコントロールが「カルーセルスライダー」。主に写真であったり、プレゼンのスライドのようなものであったりに使われていますが、いざ自前で実装すると、結構大変です。このカルーセルをVuetifyは簡単に実装できるだけでなく、配列に値をpushするだけで自動で追加項目が反映するVue.jsのBindの特性が活かせるコントロールでもあります。
ソースコード
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var calitem = [ { src: 'https://drive.google.com/uc?id=1pAuuo3qeFilFciiF6dCYP7_hrbxU8avZ&.jpg' }, { src: 'https://drive.google.com/uc?id=1lUdAgnko7q0gMmMqWghrI3xIh1xrOP4T&.jpg' }, { src: 'https://drive.google.com/uc?id=1SrJo8FmqM32TlVIuXwsJENqa2DLu2_Tv&.jpg' }, { src: 'https://drive.google.com/uc?id=15EOT__Zj7od8inQKLMk-5vixcOa_SgWz&.jpg' }, { src: 'https://drive.google.com/uc?id=13IifvE4R8Wnwc_OmCfoJ9lJWfbysZ_cy&.jpg' } ]; new Vue({ el: '#app', vuetify: new Vuetify(), data () { return { items: calitem } }, methods:{ addPicture(){ calitem.push({src:'https://drive.google.com/uc?id=1cBWBxtb3pXat_4zDI5vbBQOsU5U3llj1&.jpg'}) console.log(calitem); } } }) |
- calitemという配列を格納してる変数に、連想配列でsrcを指定したデータを格納しておく
- Vue.jsにてdataのitemsにcalitemを指定するとカルーセルスライダー用のデータとして利用される
- addPictureメソッドにて、この配列に新たな画像データを動的に1個追加する
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 |
<v-app id="app"> <template> <v-btn block @Click="addPicture"> 画像追加 </v-btn> </template> <v-carousel> <v-carousel-item v-for="(item,i) in items" :key="i" :src="item.src" reverse-transition="fade" transition="fade"> </v-carousel-item> </v-carousel> </v-app> |
- v-carouselにて、itemsの中身をv-forでループ、バインドされたデータをカルーセルで表示するようになっています。
- 画像が追加されると自動的にVue.jsにてそれらは動的に取り込まれすぐ反映してくれる。jQueryのようにリロードといったコードは不要となる。
- サンプルでは、クリックすると、一番最後にハイビスカスの画像がカルーセルに追加されます。
いわゆる、サイドバーのこと。例えば、ハンバーガーメニューをクリックすると左側から選択用のメニューが出てくるようなアレです。現代のアプリケーションを構築し、ナビゲーションをするには必須の項目ですね。MenusやLists、TreeViewを組み合わせてれば、リッチなUIを実現出来ます。
ソースコード
JS側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
new Vue({ el: '#app', vuetify: new Vuetify(), data:()=> ({ 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){ targetId = event.currentTarget.id; alert(targetId); } } }) |
- Drawerの開閉フラグは変数drawerが担当しています。
- サイドバーの中身用のリストは、itemsの値から生成しています。
- リストをクリックしたら発火する関数として、onListChangeを定義していて、そのタグ内のIDの値をalertで表示
- onListChangeで引数で受けるIDはitemsで生成時にv-bindで動的に割り当てたものを利用しています。
HTML側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<v-app id="app"> <v-card class="mx-auto overflow-hidden" height="500" width="100%"> <v-system-bar color="deep-purple darken-3"></v-system-bar> <!-- アプリのタイトル --> <v-app-bar color="deep-purple accent-4" dark prominent> <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon> <v-toolbar-title>タスク管理 山猫さん</v-toolbar-title> </v-app-bar> <!-- Drawer部分 --> <v-navigation-drawer v-model="drawer" dark absolute temporary src="https://cdn.vuetifyjs.com/images/backgrounds/bg-2.jpg"> <v-list nav dense> <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" link v-on:click="onListChange($event)" v-bind:id="args"> <v-list-item-icon> <v-icon>{{ icon }}</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title>{{ text }}</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-item-group> </v-list> </v-navigation-drawer> <v-card-text> ここにコンテンツを記述する </v-card-text> </v-card> </v-app> |
- v-navigation-drawerにbottomを指定すると、サイドバーではなくアンダーバーになります。
- v-list-itemではv-forにてitemsから値を取得し、v-list-itemを生成しています。
- v-on:clickにてリストアイテムクリック時にonListChangeを実行しています。引数に$eventを指定すると、他の属性値(例えばidなど)を関数側で取得できます。
- v-bindにて、itemsのargsをIDとして割り当ててます。
- v-list-itemに対して@Clickでコマンドを指定することで選択時の挙動を制御出来ます。其の際に自動的にDrawerを自動で閉じるには以下のコードをコマンドに追加しておく必要があります。
1234setTimeout(() => {//ドロワーをオフにするthis.drawer = false}, 0) - itemsのiconはMaterial iconの文字列を指定、textはリストのタイトル。それぞれをVue.jsで割当
- v-list-item-groupにv-modelでgroupを指定していますが、group(ドロワーの選択項目)は上から0〜を割り振られているので、groupの値を変更することでアクティブ(選択状態)にすることが可能です。
Autocomplete
少ない選択肢ならば、コンボボックスなどのv-selectを使って表現をすれば良いのですが、非常に多くの選択肢がある場合には選択肢が逆に利便性を損ねてしまいます。そこで利用するのが、オートコンプリート。ユーザの入力ワードでGoogleのサジェストのように機能するものです。
しかし、公式サイトにあるサンプルコードだと、単純な一次元配列の選択肢を表示するだけで、業務の現場で必要とされる
- 選択肢の項目に複数の関連情報を表示する(社員番号と社員名など)
- 選択したら、そのデータの塊を取得して、他の入力項目に流し込みたい(選択したレコードの取得)
- 選択肢データはMySQLやSQLiteに格納済みデータをそのまま利用したい(JSONデータで選択肢にそのまま利用する)
- 検索時は社員IDのみだけでなく、全レコードの列項目も検索対象にしたい。
といったものが満たせません。これらの要求を満たすようにAutoCompleteを実装してみたいと思います。1000以上ある選択肢でもユーザが絞り込んで選択できるスグレモノです。
ソースコード
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<v-app> <v-form ref="form"> <v-row justify="center"> <v-card> <v-card-text> <v-container> <v-row> <v-col cols="12"> <v-autocomplete @change='seleresult($event)' append-icon="mdi-magnify" :loading="loading" :items="items" :search-input.sync="search" v-model="select" no-filter flat hide-no-data hide-details return-object placeholder="検索文字を入力"> <template slot="selection" slot-scope="{ item, selected }"> {{item.ID}} </template> <template slot="item" slot-scope="{ item, tile }"> <v-card class="mx-auto" width="100%"> <v-list-item two-line> <v-list-item-content> <v-list-item-title>社員名:{{item.empname}}</v-list-item-title> <v-list-item-subtitle>社員場号:{{item.emp_id}} - 会社名:{{item.corpname}}</v-list-item-subtitle> </v-list-item-content> </v-list-item> </v-card> </template> </v-autocomplete> </v-col> </v-row> </v-container> </v-card-text> </v-card> </v-row> </v-form> </v-app> |
- 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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
vm4 = new Vue({ el: '#app4', vuetify: new Vuetify(), data: () => ({ dialog: false, notifications: false, sound: true, widgets: false, selerec: {}, common: common, items: [], search: null, select: null, loading: false, }), watch: { search (val) { console.log(val); if(val == "" || val == undefined){ //何もしない }else{ val && val !== this.select && this.querySelections(val) } }, }, methods: { querySelections(v) { setTimeout(() => { this.items = this.common.filter(e => { //JSONデータを配列にする var ret = JSON.stringify(e); //検索キーワードが配列に含まれているかチェック var includeman = ret.indexOf(v); //配列に含まれていたら、trueを返すとフィルタされた値がitemsに入る if(includeman > 0) return true; }) this.loading = false }, 500) }, seleresult(event){ //eventに一連の値が入ってるので、これをそのままselerecに入れる if(event == "" || event == undefined){ //何もしない }else{ this.selerec = event; } }, } }) |
- 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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//Body読み込み時に初期化する function initial() { //Vue.jsを初期化 vm = new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ desserts: recdata, editedIndex: "", editedItem: "", }), methods: { deleteItem (item) { //配列から削除 this.editedIndex = this.desserts.indexOf(item) this.editedItem = Object.assign({}, item) this.desserts.splice(this.editedIndex, 1) this.$nextTick(() => { this.editedItem = Object.assign({}, this.defaultItem) this.editedIndex = -1 }) }, } }) } |
- シンプルテーブル自体に関しては特にメソッド等はなく、vueを初期化すれば使えます。
- 今回はレコード削除ボタンを用意してるので、deleteItem関数だけmethodsに加えています。
- 実際に現場で利用する場合は、新規に追加するコード、追加時に値のValidationや合計作業時間の合計などを計算するメソッドを用意します。
- recdataに対して値を追加したり削除することで、テーブル側の表記が変わります。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<v-simple-table fixed-header height="300px"> <template v-slot:default> <thead> <tr> <th class="text-left"> PJコード </th> <th class="text-left"> PJ名 </th> <th class="text-left"> 作業時間 </th> <th class="text-left"> アクション </th> </tr> </thead> <tbody> <tr v-for="item in desserts" :key="item.name"> <td>{{ item.pjcode }}</td> <td>{{ item.pjtitle }}</td> <td>{{ item.timeval }}</td> <td> <v-icon small class="mr-2" @click="deleteItem(item)"> mdi-delete </v-icon> </td> </tr> </tbody> </template> </v-simple-table> |
- fixed-headerにてタイトル行を固定化出来ます。スクロールしてもタイトル行は常に表示の状態になります。
- Data Tableと異なり内容はTableタグそのもの。なので、THEADやTR/TD、TBODYなどを記述して構築します。
- テーブル内容はデータの塊であるdesserts変数の中身をv-forにて取り出し、各セルにはめ込んでいく形になります。
- 今回はレコードを削除する為のdeleteItem関数を割り当てたボタンをアクション列に用意しています。
図:農作業タスクの時間を登録してみた
Table
jQuery系で自分がよく使ってるものに、「jQuery DataTables」という非常に優れたプラグインがあるのですが、これをVuetifyのTableコンポーネントで実現する事が可能です。また、Vue.jsであるので、データの入れ替え等にまつわる動作がとっても楽ちんで、jQueryのように更新だのなんだので独特のコードを書かずとも変数入れ替えるだけでOKなので、とっても便利です。
ソースコード
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
var vm; //レコードデータ var recdata = []; //Body読み込み時に初期化する function initial() { //Vue.jsを初期化 vm = new Vue({ el: '#app', vuetify: new Vuetify(), name: "table", data: () => ({ search: "", headers: [ { text: '社員番号', align: 'start', filterable: true, value: 'emp_id', }, { text: '氏名', value: 'empname' }, { text: '戸籍氏名', value: 'koseki' }, { text: '性別', value: 'seibetsu' }, { text: 'フリガナ', value: 'kana' }, { text: 'ローマ字', value: 'roma' }, { text: '入社日', value: 'join_date' }, { text: '社員区分', value: 'empkubun' }, { text: '採用地', value: 'saiyou' }, { text: '会社名', value: 'corpname' }, { text: '人事領域', value: 'corpcode' }, { text: '職種', value: 'jobname' }, { text: '部署', value: 'busyo' }, ], desserts: recdata, }) }) //現在の社員マスタをSQLiteから取得する google.script.run.withSuccessHandler(onSuccess).gettabledata(); } |
- headersでは、列名やソートの有無、フィルタ可能かどうかを追加できます。filterableでfalseを指定した列は、検索対象外になります。また、valueは連結するデータ(今回で言えば、dessertsがそれになる)のどの値を繋げるかを指定します。
- 初期化後にvm.dessertsに対して、JSON形式のデータを投げ込めば、値の入れ替えや追加が可能です。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<v-app id="app"> <v-card> <v-toolbar dark prominent src="https://cdn.vuetifyjs.com/images/backgrounds/vbanner.jpg" class="fixed-bar"> <v-toolbar-title>社員マスタ</v-toolbar-title> <v-spacer></v-spacer> <v-btn icon dark text @click="importman"> <v-icon>mdi-import</v-icon> </v-btn> </v-toolbar> <v-card> <v-card-title> <v-text-field v-model="search" append-icon="mdi-magnify" label="検索" single-line hide-details ></v-text-field> </v-card-title> <v-data-table :headers="headers" :items="desserts" :search="search" sort-by="emp_id" :sort-desc="true" :footer-props="{ showFirstLastPage: true, firstIcon: 'mdi-arrow-collapse-left', lastIcon: 'mdi-arrow-collapse-right', prevIcon: 'mdi-minus', nextIcon: 'mdi-plus', items-per-page-text:'1ページ毎のレコード数' }" ></v-data-table> </v-card> </v-card> </v-app> |
- Table本体は、v-data-tableのタグの部分のみ。そこへ検索用のテキストフィールドを追加しているだけです。
- 標準だと日本語化されていない場所(items-per-page-textなどの部分)は、footer-propsの中で定義して置き換えることが可能です。Rows Per Pageを1ページ毎のレコード数に今回置き換えています。
- 移動用のアイコンも同様に標準のものから、マテリアルアイコンを指定して置き換える事が可能です。
- マルチソート、グルーピング、行選択などなど、幅広いオプションが用意されているので、気にいる形式に仕立て上げる事が可能です。社員マスタなどのマスタ系を弄るのに向いていると思います。
- sort-byで並べ替えをする列の項目を指定。emp_idというキー名で指定してる。
- この時同時に:sort-desc="true"を指定すると降順となる。
- widthのオプションがv-data-tableには無いため、外側にv-cardにwidthで指定して括ったり、v-colならばcolのsmの値などを調整して幅を指定する必要があります、
図:ダミーデータを10件ずつ表示してみた
常に全件表示とリサイズ対応
このTable機能非常に便利なのですが、デフォルトだと10件ずつ表示であったり、また、100件表示したら画面に収まらずまた、高さがウィンドウに合わせてくれません。そこでこれらに対応して
- リサイズ時にv-data-tableの高さを自動調整する
- 常に全件を表示するようにする
- ヘッダーは固定化してスクロールしても常に表示
を装備したいと思います。simple-tableでも良いのですが、あちらはソートが出来ない等やはり不便なので、tableにてsimpel-tableの表現を実現するのが目的です。
※headerのJSONデータにwidthを入れておくとtableの各列の幅を規定する事が可能です。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<div id="gridarea" style="width: 100%;" style="display:block" v-resize="updateDataTableHeight"> <template> <v-data-table :height="dataTableHeight" v-model="selected" :headers="headers" fixed-header :items="recdata" sort-by="appdate" hide-default-footer :items-per-page="-1" > <template v-slot:item.actions="{ item }"> <v-icon small class="mr-2" @Click="deleteItem(item)"> mdi-delete </v-icon> <v-icon small class="mr-2" @Click="viewrec(item)"> mdi-circle-edit-outline </v-icon> </template> </v-data-table> </template> </div> |
- 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"を指定する事で、常に全件表示になります。
- あらかじめ、gridareaに対してheightを指定しておいてしまうとリサイズ時の値変更が何故かできなくなるので、高さ指定は外しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//divの高さを自動補正する関数 function setGridHeight(){ var layoutHeight = $(window).height() - 10; $('#gridarea').css('height', layoutHeight + 'px'); } //リサイズ時にウィンドウにフィットさせる window.onresize = function(){ setGridHeight(); } //gridのサイズを自動でウィンドウにフィットする $(document).ready(function () { setGridHeight(); }); |
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//Vue.jsを初期化 vm = new Vue({ el: '#app', vuetify: new Vuetify(), name: "table", data: () => ({ recdata: recdata, dataTableHeight: 0, selected: [], headers: [ { text: '申請日', value: 'appdate', width: '150' }, { text: '社員番号', value: 'uid', width: '130' }, { text: '申請者名', value: 'uname', width: '150' }, { text: '内容', value: 'description' }, { text: 'アクション', value: 'actions', sortable: false, width: '150' }, ], }), methods: { updateDataTableHeight () { //高さを動的変更 var layoutHeight = $(window).height() - 98; this.dataTableHeight= layoutHeight; }, }, mounted : function(){ //管理者宛通知一覧をGAS側から取得する google.script.run.withSuccessHandler(onSuccess).getAdminNotif(); //サイズ調整 setGridHeight(); }, }); |
- データ取得して返り値をrecdataに入れるonSuccess関数については記載を省略しています。
- v-resizeでupdateDataTableHeightが実行されて、リサイズした際のフィットするサイズをdataTableHeightへ入力しています。
- mountedにてマウント完了後にGAS側へデータを要求するようにしています。
図:通知一覧などの全件を表示したい場合に利用する。
テーブルの値をその場で修正したい
テーブルに表示されてる内容のうち、1個だけをその場で修正したいみたいな事例は結構あります。レコード全部を修正可能にしたいのであれば、別途Dialogを用意して値を呼び出して保存する仕組み等を用意すれば良いですが、インスタントにそこだけちょっと編集したいといった場合には、ちょっとこれでは大掛かりです。
そこで用意されてるのがDataTableに用意されてるv-edit-dialog。これを既存のv-data-tableに加えてあげれば、その場でサクっと修正する機能をつけることが可能です。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<v-data-table :height="300" v-model="selected" :headers="headers" fixed-header :items="recdata" sort-by="id" hide-default-footer :items-per-page="-1" > <template v-slot:item.timeval="props"> <v-edit-dialog :return-value.sync="props.item.timeval" large persistent @save="save" @cancel="cancel" @open="open" @close="close" cancel-text="キャンセル" save-text="保存" > <div>{{ props.item.timeval }}</div> <template v-slot:input> <div class="mt-4 title"> 時間の修正 </div> <v-text-field v-model="props.item.timeval" label="Edit" single-line autofocus :rules="[rules.required,rules.timeman2]" ></v-text-field> </template> </v-edit-dialog> </template> </v-data-table> |
- 今回は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の値を直接入れずに以下のようなスタイルで入れるようにしましょう。
1 2 3 4 5 |
viewrec(item){ //itemをダイアログに追加 var json = JSON.stringify(item); this.temppj = JSON.parse(json); }, |
- itemはJSON.stringifyにてjsonという変数に格納する
- このjsonの値を一時的な変数であるにJSON.parseしてから入れてあげる
これだけです。これでVBAで言うところの値渡しの状態になるので、ダイアログ上で編集をしても大元のデータが書き換わったりしなくなります。直接書き換わるほうが便利なケースもありますけれどね。ちなみに、ダイアログ側は以下のような形にしてあります。
1 2 3 4 5 |
<v-row> <v-col cols="12" md="6"> <v-text-field label="PJタイトル*" v-model="temppj.pjtitle" class="input-items"></v-text-field> </v-col> </v-row> |
- 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の一番上の位置までスクロールバーを戻してくれます。
1 2 |
//v-data-tableのスクロールバー位置を上に戻す $('.v-data-table__wrapper').scrollTop(0) |
地味に結構遭遇するパターンなので、jQueryもまだまだ手放せないですね。
列を固定化する
v-data-tableには現在、列を固定化して横スクロール時にそのまま表示しつづける為のオプションが存在しません。2018年から要望は出ているものの現在まで搭載されず、フォークしたデモでは実装してる方もいるのですが、本プロジェクトにはマージされていません。
v-data-tableの実体はtableそのものなので、CSSで固定化するのが現実的な方法なのですが、1列の場合と複数列の場合で結構苦労しました。以下のようなコード体系で実現が可能です。
CSS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* datatableの列を固定化する(1列目) */ #gridarea > div > div > table > tbody > tr > td:nth-child(1), #gridarea > div > div > table > thead > tr > th:nth-child(1) { position: sticky !important; position: -webkit-sticky !important; left: 0px; z-index: 9980; background:white; } #gridarea > div > div > table > thead > tr > th:nth-child(1) { z-index: 9981; } /* datatableの列を固定化する(2列目) */ /* 1列目の幅の分だけ、leftの値を増やして固定化する */ #gridarea > div > div > table > tbody > tr > td:nth-child(2), #gridarea > div > div > table > thead > tr > th:nth-child(2) { position: sticky !important; position: -webkit-sticky !important; left: 200px; z-index: 9980; background:white; } #gridarea > div > div > table > thead > tr > th:nth-child(2) { z-index: 9981; } |
- 2列目の固定は別に用意が必要。違いは、leftの値。1列目の幅(headersで1列目のヘッダの幅を指定しておく)の分だけ、指定しておく必要がある
- ヘッダ部分だけでなく、セルの部分も固定化しないと、ヘッダだけが固定化してスクロールしてしまうので注意
HTMLコード
1 2 3 4 5 6 |
<div id="gridarea" style="width: 100%; height: 300px;"> <template> <v-data-table :height="dataTableHeight" fixed-header :headers="headers" :items="recdata"> </v-data-table> </template> </div> |
- fixed-headerはつけておいたほうが良いでしょう(これは縦スクロールでヘッダを固定化するオプション)
- 外側のIDがgridareaのDIVで今回は括っています。このIDを使ってCSSで指定しています。
図:2列固定化しました
多数のレコードを表示すると激重になる
Vuetifyのv-data-tableにて、非常に多数のデータを一度に表示してみるとわかるのですが、アプリ全体の動きが激遅になります。だいたい500件表示を超えた辺りから、だんだんモッサリしてきて、10000件表示だと表示されるまでも相当待たされた挙げ句、スクロールがもたついて正直使えるレベルではありません。
もともとページネーションで数十件ずつ表示などの運用が前提のコンポーネントなので、このような使い方をしたい場合には、素直にページネーションを利用するか?Cheetah Gridのやag-grid, slickGridような高速なグリッドコンポーネントに乗り換えたほうが、小細工をするより遥かに楽になれます。
他にもこちらのサイトにあるように、様々なGridコンポーネントが存在します。
特定の値に対してハイパーリンクを貼る
datatableの特定の値がURLやmailtoなどで、それに対応する文字列などがある場合(ない場合はURL文字列にリンクを貼る)、その文字をクリックすると、Electron内で新規ウィンドウを開いてURLを開くような動作を付け加える事が可能です。
以下の場合、reporturlがURL文字列の入ってるものになりこれに対してハイパーリンクを貼る仕組みにしてみました。
1 2 3 4 5 6 7 8 9 10 11 12 |
<div id="gridarea" style="width: 100%; height: 300px;"> <template> <v-data-table :height="dataTableHeight" v-model="selected" :headers="headers" fixed-header :items="compdata" sort-by="compid" hide-default-footer :items-per-page="-1"> <template #item.reporturl="{ item }"> <a target="_blank" :href="item.reporturl"> {{ item.reporturl }} </a> </template> </v-data-table> </template> </div> |
v-data-table内に、templateタグを入れて、item.reporturlを対象にaタグを加工して入れる仕組みです。尚、{{ item.reporturl }}に関しては別のフィールドの値を指定すれば、その文字列に対してリンクを貼れるので、そちらのほうがスマートです。
特定の値を元にフォント色を変更する
特定の値のみ色を変更する
意外と需要があると思われる「特定の値を元にフォントの色を変更する」という方法が結構面倒だったのでメモがてら残しておこうと思います。これには2パターンあり、その特定の列の値のフォント色だけ変更する場合と、その値の含まれてるレコード全体を変更する場合がある。今回はitemの中のtstimeの値を元に判定させています。
それぞれ少しずつ異なります。まず、特定の値だけフォントカラー変更する場合は
1 2 3 4 5 |
<v-data-table v-model="selected" :headers="headers" fixed-header :items="recdata"> <template v-slot:item.tstime="{ item }"> <span v-bind:class='isRed(item)'>{{item.tstime}}</span> </template> </v-data-table> |
として、v-bindのclassを動的に判定して変更する関数としてmethodに以下の関数を用意する。style-1とstyle-2はそれぞれCSSにてcolorをclass名として定義しておく。
1 2 3 |
isRed: function (item) { return item.tstime > 0 ? 'style-1' : 'style-2' }, |
図:遅刻早退の時間が0以上で赤くなった
特定の値を元に行全体のフォント色を変更する
行の場合は、:item-classを追加して、関数としてisRedを指定しておく
1 2 |
<v-data-table v-model="selected" :headers="headers" fixed-header :items="recdata" :item-class="isRed"> </v-data-table> |
methodにはisRed関数を用意する。style-1とstyle-2はそれぞれCSSにてcolorをclass名として定義しておく。codePenでこの事例が紹介されています。
1 2 3 |
isRed: function (item) { return item.tstime > 0 ? 'style-1' : 'style-2' } |
テキストの表示制限をしたい
DataTableのカラムに表示するテキストが長すぎて、テーブルの枠の幅を超えて表示されたり、縦に長くなったりと、非常に見づらくなるケースがあります。特に元データの備考欄などのような長文を書いてる場合、ダイアログで表示する分には良いですが、一覧表示のDatatableでやられると使い勝手が非常に落ちます。
ということで、例えば100文字を超えたら省略といったような文字数表示制限をしたい場合には、1つ関数をつくって対処します。vueのmethodに以下の関数をまず追加します。
1 2 3 4 5 6 7 8 9 10 |
//文字数削る supercut(text){ let cuttext = text.substr(0, 100); if(text.length >= 100){ cuttext = cuttext + "…"; } return cuttext; }, |
次にdatatableの該当のカラム指定にて、関数に食わせる形で表示をさせてやります。備考欄はdescriptionというキーで格納されています。
1 2 3 4 5 6 7 8 9 10 |
<div id="gridarea9""> <template> <v-data-table :height="dataTableHeight" v-model="selected" :headers="headers8" fixed-header :items="templist" sort-by="ID" hide-default-footer :items-per-page="-1"> <template v-slot:item.description="{ item }"> <span>{{ supercut(item.description) }}</span> </template> </v-data-table> </template> </div> |
{{ supercut(item.description) }}の部分がそれで、templateのv-slotにてitem.descriptionを指定し、表示箇所で同じく関数の引数に記述。これでデータのうち、descriptionに該当する値に関しては、100文字制限が掛けられて表示がされます。あくまで表示だけなので、元データに変更は加えていません。
図:100文字でカットされて(…)が表示された
行クリックとアクション
v-data-tableの行をクリックしてその内容に応じて処理を分けたい場合があります。この場合には以下のようにしてあげます。なお、classとしてrow-pointerを加えていますが、マウスカーソルをリンクの場合のカーソルに変更する為のCSSも入れてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<template> <v-data-table :height="dataTableHeight" :headers="headman2" fixed-header :items="nowdirlist" sort-by="update" :sort-desc="true" hide-default-footer :items-per-page="-1" @click:row="clickRow" class="row-pointer" > <template v-slot:item.actions="{ item }"> <v-icon small class="mr-2" @click.stop="deleteItem(item)" color="red"> mdi-delete </v-icon> </template> </v-data-table> </template> |
- @click:rowを加えることで、行クリック時イベントとなり、行のデータが関数のitem引数で取得できます。
- また、この場合、アクション列のボタンをクリックすると、行クリック=>アクションのボタンという順番で実行されてしまうので、アクションボタンの側は、@click.stopで関数を指定することでバッティングを防ぐことが可能です。
- row-pointerのCSSは以下のようなものを指定します。
123.row-pointer:hover {cursor: pointer;}
Validation
フォームの入力内容がきちんとボックスの内容に適合してるものなのかチェックする為の機能がvalidation。例えば数値しか受け付けたくない場所に文字や、メールアドレス欄なのにRFCの規則に従っていないメアドとか、半角全角、入力必須などがValidationの対象です。この機能を使わないとなると、サーバに送信時などで全部の入力欄について自分で一個ずつJavaScriptで検査してNGなら止めるみたいなコードを書かなければなりません
少数ならそれでも良いのですが、業務用アプリケーションのように入力欄の多い場合、非常にメンテナンス性が落ちます。これを簡単にしてくれるのがValidation機能です。Vuetify標準で搭載されてるものの他に、Vue.jsのプラグインとして、VeeValidateやVueValidateといったValidation専用のプラグインもリリースされています。
ソースコード
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<v-app> <v-card> <v-toolbar dark color="primary" class="fixed-bar"> <v-toolbar-items> <v-btn dark text @click="saverecord()"> 保存 </v-btn> <v-toolbar-items> </v-toolbar> <v-form ref="formman"> <v-card class="mx-auto" elevation="2"> <v-card-text> <v-container> <v-row> <v-col cols="12" sm="6" md="4"> <v-text-field label="社員番号*" required v-model="selerec.emp_id" :rules="[rules.required,rules.numchk]"></v-text-field> </v-col> <v-col cols="12" sm="6" md="4"> <v-text-field label="氏名*" hint="社内呼称の名称" required v-model="selerec.empname" :rules="[rules.required]"></v-text-field> </v-col> </v-row> </v-container> </v-card-text> </v-card> </v-form> </v-card> </v-app> |
- 保存ボタンにはsaverecord()を呼び出すように割り当てて起きます。(保存時に一括Validationチェックもやらせる為)
- 各入力欄のv-text-field等やv-select等には、:rules=[rules.required]などを割り当てます。これがValidationチェックの項目で、JS側で定義しておく必要があります。複数割り当てる場合は、配列内でカンマ区切りで、[rules.required, rules.numchk]といた具合に割り当てることが可能
- rulesが定義されていない入力欄はValidationの対象外になります。
- 全入力欄は必ずv-formでくくり、ref="formman"のように名前をつける事が必要です
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
vm4 = new Vue({ el: '#app4', vuetify: new Vuetify(), data: () => ({ ・・・・中略・・・・ 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 true }else{ return pattern.test(value) || 'メアドの形式がオカシイ' } }, //日付形式のvalidation dateman: value => { var pattern = /^(\d\d\d\d)\/(\d\d)\/(\d\d)$/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || '日付の形式がオカシイ' } }, //日付形式のvalidation timeman: value => { var pattern = /^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || '時刻の形式がオカシイ' } }, //数字のvalidation numchk: value => { var pattern = /^[0-9]*$/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || '数字の形式がオカシイ' } }, //全角カナチェック kanachk:value => { var pattern = /^[ァ-ンヴー]*$/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || 'フリガナの形式がオカシイ' } }, //ローマ字チェック romachk:value => { var pattern = /^[a-zA-Z]*$/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || 'フリガナの形式がオカシイ' } }, //URLチェック url:value => { var pattern = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; if(value == "" || value == undefined){ return true }else{ return pattern.test(value) || 'URLの形式がオカシイ' } }, //インボイス番号のvalidation invchk : value => { //頭の文字がTであること let tstring = value.substr(0, 1); if(tstring == "T"){ //数値部分を取り出す let tnum = value.replace('T', ''); //13桁かどうかをチェック if(tnum.length == 13){ //数値部分が全部数値かどうかチェック var pattern = /^[0-9]*$/; if (tnum == "" || tnum == undefined) { return true } else { return pattern.test(tnum) || '数字の形式がオカシイ' } }else{ //数値部分が13桁じゃないよ return false || '数値部分が13桁じゃないよ'; } }else{ //頭の文字がオカシイパターン return false || '頭の文字がTじゃないよ'; } } }, }), methods: { saverecord(){ //書き込みするレコードのrecidを取得する var recid = this.selerec.recid; if (this.$refs.formman.validate()) { // すべてのバリデーションが通過したときのみ //GAS側へデータを送る側へデータを送る google.script.run.withSuccessHandler(onSuccess).oninsert(this.selerec,fields,recid,lastedit) } else { console.log("NG") } } } }) |
- 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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<header> <style> /* パネルのサイズをスマートにする */ .v-expansion-panel-header{ padding:8px 8px; min-height:30px; } .v-expansion-panel--active>.v-expansion-panel-header{ min-height:30px; } </style> </header> <body> <!-- multipleを外すとシングル表示になります --> <v-app id="app"> <v-expansion-panels accordion multiple v-model="panel"> <v-expansion-panel v-for="(item,i) in 5" :key="i" > <v-expansion-panel-header>パネル名</v-expansion-panel-header> <v-expansion-panel-content> コンテンツをここに記述 </v-expansion-panel-content> </v-expansion-panel> </v-expansion-panels> </v-app> </body> |
- multiple指定で複数パネルを同時に開く事が可能です。
- v-forで配列データからループでパネルを構成しています。
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 |
<script> var vm; //Body読み込み時に初期化する vm = new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ panel: [0, 1], //パネルの0番、1番目をデフォルトで開いておく }) }); </script> |
- v-expansion-panelsでv-modelにてpanelを指定しておいて、panelにて配列にて数値を指定すると、指定番目のパネルをデフォルトで開いた状態にしてくれます。
- 今回はv-forで動的にv-expansion-panelを生成してるだけなので、0番目と1番目が開いた状態に。
図:こんな感じのパネルを生成可能です。
List
前述のアコーディオンメニューに似ていますが、これも非常に良く利用する所謂ドロップダウンメニューの発展版であるリストです。クリックすると、広がりサブメニューが出てくる仕様です。自分の場合は前述のDrawer等と組み合わせて利用しています。また、スプレッドシートとの組み合わせでメニューを構築するように仕込むと非常に便利です。
Expantion Panelはスマフォ対応では扱いにくいですが、こちらはスマフォ対応する上では非常に扱いやすいです。
図:メニューがこんな感じで広がる
図:データ自体は結構単純
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<v-list-group no-action v-for="(header,i) in headers" :key="i" v-model="item.active"> <template v-slot:activator> <v-list-item-content> <v-list-item-title> <span>{{ header.text }}</span> </v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="element in header.elements" :key="element.id" v-bind:id="element.id" > <v-list-item-title>{{element.text}}</v-list-item-title> <v-list-item-icon> <v-icon color="red">{{element.icon}}</v-icon> </v-list-item-icon> </v-list-item> </v-list-group> |
- v-list-groupでメインメニューをループで構築しています。headersにはGAS側からのデータを入れてあります。
- メインメニューに属するサブメニューをv-list-itemでこちらもループで構築します。
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//GAS側からのプロパティ値をpropdataに入れる function onSuccess(data){ //データを取得する vm.headers = JSON.parse(data); } var vm; vm = new Vue({ el: '#app', vuetify: new Vuetify(), data: { headers : null, }, mounted : function(){ //GAS側から一括でプロパティ値を取得する google.script.run.withSuccessHandler(onSuccess).getvlist(); }, }) |
- GAS側からデータを取得してそのままheadersに返り値のJSONを入れるだけ
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
//vlistの元データを取得する function getvlist(){ //スプレッドシートを取得 var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName("vlist"); //データを取得 var header = sheet.getRange("A2:B").getValues(); var listman = sheet.getRange("D2:G").getValues(); //返却用の配列を用意 let array = []; //headerを回してbukaのelementsを構成する for(var i = 0;i<header.length;i++){ //レコードを一個取り出す let rec = header[i]; //IDが空ならば処理終了 if(rec[0] == "" || rec[0] == undefined){ break; } //一時配列を用意してデータを追加 let temparr = { "text": rec[1], "elements":[] } //部門コードを取得する let recid = rec[0]; //listmanを回してelementsを構築する let templist = []; for(var j = 0;j<listman.length;j++){ //レコードを一個取り出す let list = listman[j]; //listコードを取得する let templistcode = list[1]; //listcodeと一致するものだけを取得する if(recid == templistcode){ //element要素を作成 let bukaarray = { id:list[0], text:list[2], parent:list[1], icon:list[3] } //tempbukaに追加する templist.push(bukaarray); } } //tempbukaをelementsに追加する temparr.elements = templist; //temparrをarrayに追加 array.push(temparr); } //作成結果を表示 console.log(JSON.stringify(array,null,"\t")) //値を返す return JSON.stringify(array); } |
- メインとサブのデータをそれぞれ取得し、HTML側のheadersに入れるデータを生成。扱いやすい用に予め加工しておきます。
- textがメインメニュー、elementsがそれにぶら下がるサブメニュー項目一式を入れています。
- メインメニューとそれにぶら下がるサブメニューをIDとPIDで連結して以下のようなJSONで構築して返します。
12345678910111213141516171819202122232425[{"text": "くだもの","elements": [{"id": 1,"text": "チェリー","parent": 1,"icon": "mdi-fruit-cherries"},{"id": 2,"text": "レモン","parent": 1,"icon": "mdi-fruit-citrus"},{"id": 3,"text": "ぶどう","parent": 1,"icon": "mdi-fruit-grapes"}]}]
Chips
ウェブアプリケーション等でよく見かける「タグ」であったり、カテゴリ分けを付けたり、場合によっては添付ファイルの表示として使われるのがこの「チップ」。地味な機能ではあるものの、結構奥深くて、実際にこれを使って添付ファイルの有無や操作から削除などを実装してみましたが、結構色々とノウハウがあります。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<v-card class="mx-auto" outlined> <v-chip color="#17204d" text-color="yellow" v-for="(attach, index) in attachment" :key="index" close @click="chipclick(attach)" @click:close="remove(attach)" > {{ attach.name }} </v-chip> </v-card> |
- あらかじめ、vueのattachment変数にデータの塊をいれておきます(JSON形式)
- v-forでデータを回して、チップを複数生成していきます。この時、nameをチップの表示文字列として利用します。
- closeプロパティを付けると閉じる事が出来るようになります。またこのcloseにアクションを付ける場合は、@click:closeで関数を指定します。
- チップ自体のクリック時に関しては通常通り、@clickで関数を指定すれば、attachの中身を渡してアクションを実行可能です。
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//チップクリック時は直接ダウンロード chipclick (item) { //fileidを取得 var fileid = item.fileid; //URLを構築 var aturl = "https://drive.google.com/uc?export=download&id=" + fileid; //閲覧ダイアログの時のみダウンロードを実行 if(this.viewDialog == true){ //新規ウィンドウで開かせてダウンロード window.open(aturl, "_blank"); } }, remove(item){ //uuidを取得する var uuid = item.uuid; //attachmentから除外する for(var i = 0;i<this.attachment.length;i++){ //レコードを一個取り出す let rec = this.attachment[i]; //uuidが一致する値を除外する if(uuid == rec.uuid){ this.attachment.splice(i, 1); break; } } }, |
- 通常のチップクリック時処理と、クローズクリック時処理の2つを記述します(Closeが不要の場合はremoveはいらない)
- remove時はattachに入ってる値を元に、attachment変数から対象のチップデータを除外します。
図:jpgファイルをchipで表現してみた
組み合わせの妙技
Expantion Panel内のリストをv-forで作る
アコーディオンメニューであるExpantion Panelですが、そのヘッダ部分はv-forで普通に作ることが可能です。しかし、そのヘッダに属するリストをv-expansion-panel-contentの中に作り込もうとした場合、v-expansion-panelの名前に紐付くものをデータからフィルタして・・・とやると非常に厄介です。
そこではじめからデータ構造を工夫しておけば、computedなどを駆使してなんてせずとも、簡単にそのヘッダ名に属するサブ項目をリストの形で構築する事が可能です。こちらのサイトの内容を参考にしました。
HTML部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<v-expansion-panels accordion multiple v-model="panel"> <v-expansion-panel v-for="(header,i) in headers" :key="i" > <v-expansion-panel-header> <span>{{ header.text }}</span> <template v-slot:actions> <v-icon color="blue">mdi-arrow-down</v-icon> </template> </v-expansion-panel-header> <v-expansion-panel-content v-for="element in header.elements" :key="element.id" > {{element.text}} </v-expansion-panel-content> </v-expansion-panel> </v-expansion-panels> |
- Expantionのアイコンと色も変えておきました。
- headersにデータが部分が入っており、ここのtext項目を元にアコーディオンメニューをv-forで構成します。
- v-expansion-panel-contentでのv-forはheadersの中のelements項目の中身を回して、その中のtext項目を生成する仕組みです。
データ部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
headers: [ { text: '営業部', elements: [ {id:1, text:"ホールセールス"}, {id:2, text:"リテールセールス"}, ] }, { text: '管理部', elements: [ {id:1, text:"人事課"}, {id:2, text:"総務課"}, {id:3, text:"管理課"}, ] } ] |
- データ構造は上記のようなスタイル
- 1レコードの連想配列の中に入れ子でelementsという配列を入れておき、これがtextの部署に属する課といった具合に2つ目のv-forでループを回す対象になります。
- データ構造の工夫だけで複雑なロジックを構築する事なく目的のものを作ることが可能です。
図:こんな感じのをデータから生成する
テキストボックスとカレンダー連動+α
非常に便利でリッチなUIを提供してくれるVuetifyですが、公式ドキュメントだけでは実務では困るケースが結構あります。特に今回社内向けアプリを作ってて困ったのが、テキストボックスとカレンダーの連携。困った点は以下の通り
- テキストボックスクリックでカレンダー表示だと手修正時に面倒(ボタンを別途用意する必要性)
- カレンダーを要するものが1個ならばVue.js内に用意した変数と連動で良いが、たくさんある場合にはこの手は非常に面倒(カレンダーの数だけ用意が必要)
- Vuetifyのカレンダーは、yyyy-mm-ddの形式でなければならないが、テキストボックス内に表示されるのはyyyy/mm/dd(このまま渡すとエラーになる)
- 値を読み書きする対象は、単一の値ではなくJSONの塊であるため、ロジックが必要(テキストボックスへの連結はv-modelで連結で良いのだが・・・)
これらを解消するにはちょっと複雑な仕組みが必要です。
ソースコード
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<style> /* DatePickerの土日の色 */ .v-date-picker-table.v-date-picker-table--date > table > tbody tr td:nth-child(7) .v-btn__content { color:blue } .v-date-picker-table.v-date-picker-table--date > table > tbody tr td:nth-child(1) .v-btn__content { color:red } </style> <v-form ref="form"> ・・・前略・・・ <v-menu :close-on-content-click="false" :nudge-right="40" transition="scale-transition" offset-y min-width="290px"> <template v-slot:activator="{ on, attrs }"> <v-text-field v-model="selerec.joindate" label="入社日付" type="text" hint="入社した日付を選択" persistent-hint required> <template v-slot:append-outer> <v-btn icon color="primary" dark elevation="0" @click="onBindName('joindate')" v-on="on"> <v-icon>mdi-calendar</v-icon> </v-btn> </template> </v-text-field> </template> <v-date-picker v-model="picker" @click="menu = false" locale="ja-jp" :day-format="date => new Date(date).getDate()" ></v-date-picker> </v-menu> ・・・後略・・・ </v-form> |
- 入社日付テキストボックスとは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名を入れておき、読み書き対象の場所を特定できるように指定しておく。
- v-menuのclose-on-content-clickをtrueにするとピッカーで日付をクリックすると閉じるようになる
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//グローバル変数 var binder = ""; //ダイアログを初期化する vm3 = new Vue({ el: '#app3', vuetify: new Vuetify(), data() { return { selerec: {}, date: null, } }, methods: { onBindName(args){ //対象の日付テキストのバインド先を格納する binder = args; //バインド先データの日付を取得し、dateに格納しておく try{ var dateman = this.selerec[binder]; this.date = dateman.replace(/\//g, '-'); }catch(e){ } }, }, computed: { picker: { get() { //変数dateから取得変換済みの日付をカレンダーに対して返す return this.date; }, set(val) { //カレンダーの選択日付をセットする this.date = val; var dateman = val.replace(/-/g, '/') this.selerec[binder] = dateman; } } } }) |
- グローバル変数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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<v-form ref="form"> ・・・前略・・・ <v-menu :close-on-content-click="false" ref="menu" :nudge-right="40" transition="scale-transition" :return-value.sync="time" offset-y min-width="290px"> <template v-slot:activator="{ on, attrs }"> <v-text-field v-model="selerec.workman" label="作業時間" type="text" hint="作業した時間数" persistent-hint class="input-items"> <template v-slot:append-outer> <v-btn icon color="primary" dark elevation="0" @click="onBindName2('workman')" v-on="on"> <v-icon>mdi-timer-outline</v-icon> </v-btn> </template> </v-text-field> </template> <v-time-picker v-model="picker2" ref="picker" full-width format="24hr" @click="menu2 = false"></v-time-picker> </v-menu> ・・・後略・・・ </v-form> |
- 入社日付テキストボックスとは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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//グローバル変数 var binder = ""; //ダイアログを初期化する vm3 = new Vue({ el: '#app3', vuetify: new Vuetify(), data() { return { selerec: {}, time: null, menu2: false, } }, methods: { onBindName2(args){ //対象の時刻テキストのバインド先を格納する binder = args; //バインド先の日付を取得 try{ var timeman = this.selerec[binder]; this.time = timeman; }catch(e){ } }, }, computed: { picker2: { get() { //バインド先から取得変換済みの日付をカレンダーに対して返す return this.time; }, set(val) {val //カレンダーの選択日付をセットする this.time = val; this.selerec[binder] = this.time; //時刻選択の状態に戻す this.$refs.picker.selectingHour = true; } } } }) |
- グローバル変数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
1 2 3 4 5 6 7 |
/* DialogのToolbarを上部に固定化 */ .fixed-bar { position: sticky; position: -webkit-sticky; /* for Safari */ top: 0em; z-index: 9; } |
- ツールバー部分はposition:stickyにて上部に固定化する
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<v-app> <v-dialog v-model="dialog" fullscreen hide-overlay transition="dialog-bottom-transition"> <v-card> <v-toolbar dark color="primary" class="fixed-bar"> <v-btn icon dark @click="dialog = false"> <v-icon>mdi-close</v-icon> </v-btn> <v-toolbar-title v-text="selerec.emp_name+'の情報詳細'"></v-toolbar-title> <v-spacer></v-spacer> <v-toolbar-items> <v-btn dark text @click="dialog = false"> 保存 </v-btn> </v-toolbar-items> <template v-slot:extension> <v-tabs v-model="model"> <v-tabs-slider color="yellow"></v-tabs-slider> <v-tab href="#tab-1"> 共通項目 </v-tab> <v-tab href="#tab-2"> 人事 </v-tab> <v-tab href="#tab-3"> 通勤費 </v-tab> <v-tab href="#tab-4"> 厚生 </v-tab> <v-tab href="#tab-5"> 勤怠 </v-tab> </v-tabs> </template> </v-toolbar> <!-- 各タブ内のコンテンツ --> <v-form ref="form"> <v-tabs-items v-model="model"> <v-tab-item value="tab-1"> ここにコンテンツを記述 </v-tab-item> <v-tab-item value="tab-2"> 人事U </v-tab-item> <v-tab-item value="tab-3"> 通勤費関係 </v-tab-item> <v-tab-item value="tab-4"> 福利厚生 </v-tab-item> <v-tab-item value="tab-5"> 勤怠の入力欄 </v-tab-item> </v-tabs-items> </v-form> </v-dialog> </v-app> |
- 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での調整を利用しています。こちらのほうがスッキリ決まります。
また、サイドバーでのパネル変更時にpanel1ではTabを表示し、panel2ではTabを非表示にした場合には、古典的なテクニックですが、以下のような仕組みを利用します。
- v-tabsに対して、v-show="tabman"としてプロパティを追加。tabmanの値をfalseにすればタブだけが非表示になる
- v-tabsのv-modelの値を変更することで、選択してるタブを変更することが出来ます
1this.model = "tab-0";
上記のコードでtab-0のタブに変更することが可能です。 - さらにパネル切り替えのコマンドにて、jQueryでheaderの高さを変更すればタブを完全に消すことが可能
1$('.v-toolbar').css('height' ,'96px'); - 追加でそのpanelに表示してるdivのエリアの高さも調整すればキレイにタブを非表示にすることが可能です。
- このイベントをサイドバーのパネル変更に加えておく
図:標準のタグよりも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で判定させてレンダリングさせる。
この三段構えが必要になります。コード的には
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<親コンポーネント v-for="([id,folderid], i) in master" > <!-- n個目のデータを取り出す --> <親の子コンポーネント v-if="id"> <!-- cidとidが一致するものだけレンダリング--> <template v-for="([subid,fileid,filename], i) in submaster"> <div v-if="id==subid"> <レンダリング用コンポーネント> <!-- 描画するサブマスタ内容をここに記述 --> </レンダリング用コンポーネント> </div> </template> </親の子コンポーネント> </親コンポーネント> |
となります。これでv-forでマスタ、サブマスタを回しつつ、両方に共通するIDで一致するデータをレンダリング用コンポーネントに描画するといった流れを構築可能です。コードだけで実現するよりも、逆に複雑に見えますが、Vue.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をグローバルから扱えるようにします。
1 2 3 4 |
mounted : function(){ //グローバルからAPI操作できるようにする calapi = this.$refs.fullCalendar.getApi(); } |
calapiはグローバル変数です。これを他のメソッドより扱い、データの再取得時の処理で以下のようにしました。
1 2 3 4 5 6 7 8 |
function onSuccess(data){ ・・・・データの追加処理・・・・ //一旦再描画(これをしないと全件表示されないことがある) calapi.render(); } |
地味に嵌ってました。1件ずつだとこの処理をせずともきちんと、データの再取得で表示されるのですが・・・・
VuetifyのCSSが邪魔してしまうシーン
これらプラグインとVuetifyをあわせて使ってる場合、時としてVuetifyのCSSが効いてしまい、自分がカスタマイズをする際に困るシーンがあります。特にv-appで指定される.v-applicationのCSSは面倒で、この場合、CSSを別途定義して一度設定をクリアしてから再度、別のclass指定でCSSを定義するようにすると機能する事があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* vuetifyのcolor設定を消去 */ .v-application a { color: unset !important; } .fc-username-button { all: unset; font-size: 18px; display: inline-block; padding: 0.5em 1em 0.3em; color: #ff3399; border: double 4px #ff3399; background-color: #ffffff; cursor: pointer; margin-right: 20px; } |
- 上記はあるボタンの文字色に関する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の読み込み後に以下のコードで追加で読み込むようにします。
1 2 |
<script src="https://unpkg.com/cheetah-grid@0.22.9/dist/cheetahGrid.es5.min.js"></script> <script src="https://unpkg.com/vue-cheetah-grid@0.22.3/dist/vueCheetahGrid.js"></script> |
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//シートからGrid用のデータを生成する function geneperson(){ //スプレッドシートのデータを取得 var rows = SpreadsheetApp.openById(sheetid).getSheetByName("grid").getRange("A1:D").getValues(); //1行目をJSONのkeyとして設定 var keys = rows.splice(0, 1)[0]; //keyに対応するデータを元にJSONを構築 return rows.map(function(row) { var obj = {}; row.map(function(item, index) { obj[String(keys[index])] = String(item); }); //JSONを返す console.log(obj) return obj; }); } |
- スプレッドシートのデータをタイトル列をkeyに、連想配列データにして返す関数です
- このデータをCheetah Gridのrecordsにそのまま、当て込みます
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//cheetah Gridプラグインを読み込む Vue.use(vueCheetahGrid) //スプレッドシートからグリッドデータを生成する google.script.run.withSuccessHandler(onSuccess).geneperson(); //Vue.jsを初期化する function onSuccess(data){ records = data; new Vue({ el: '#app', vuetify: new Vuetify(), data:()=> ({ records:data }), methods: { //ボタンクリック時に呼び出されてレコード内容を表示する onClickRecord(rec) { alert(JSON.stringify(rec)); } } }); } |
- Vue.useで今回の拡張プラグインを読み込まないとエラーになります
- google.script.run.withSuccessHandlerにてGAS側からデータを受け取る
- 受け取ったデータを元にnew Vueでrecordsに当て込みます。
- 今回はボタンをクリックするとレコード内容をalertで表示するメソッドを追加
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<div id="app" style="width:100%; height: 500px; border: solid 1px #ddd;"> <!-- 2列目まで固定 --> <c-grid :data="records" :frozen-col-count="2"> <!-- チェックボックス表示 --> <c-grid-check-column field="check" width="50"> 終了フラグ </c-grid-check-column> <!-- ID列を表示 --> <c-grid-column field="personid" width= "85"> ID </c-grid-column> <!-- マルチプルカラムで表示 --> <c-grid-column-group caption="Name"> <c-grid-input-column field="fname" width="20%" min-width="150"> 名前 </c-grid-input-column> <c-grid-input-column field="lname" width= "20%" min-width="150"> 名字 </c-grid-input-column> </c-grid-column-group> <!-- Mail列を表示 --> <c-grid-column field="email" width= "85"> Mail </c-grid-column> <!-- 編集ボタン --> <c-grid-button-column caption="編集" width="120" @click="onClickRecord"> 編集 </c-grid-button-column> </c-grid> </div> |
- frozen-col-countにて固定する列の列数を指定します。2で初めの2列を固定化
- データと連結するにはfieldプロパティを使ってrecordsのkeyと一致させる。personidならrecordsデータのpersonidのkeyの値がここに入ってきます。
- 2段表示のタイトル列を作る場合は、c-grid-column-groupにて1段目を定義し、その中に2段目を個別に定義する形になります。
- onClickRecordにて定義した関数が呼び出されますが、特に引数を指定せずともrecにレコードの全ての値が入ってくるので、これを元にデータを加工します。
Grid用データ
1 2 3 4 5 6 7 8 9 10 11 12 |
var records = [ {'personid': 1, 'fname': 'Sophia', 'lname': 'Hill', 'email': 'sophia_hill@example.com'}, {'personid': 2, 'fname': 'Aubrey', 'lname': 'Martin', 'email': 'aubrey_martin@example.com'}, {'personid': 3, 'fname': 'Avery', 'lname': 'Jones', 'email': 'avery_jones@example.com'}, {'personid': 4, 'fname': 'Joseph', 'lname': 'Rodriguez', 'email': 'joseph_rodriguez@example.com'}, {'personid': 5, 'fname': 'Samuel', 'lname': 'Campbell', 'email': 'samuel_campbell@example.com'}, {'personid': 6, 'fname': 'Joshua', 'lname': 'Ortiz', 'email': 'joshua_ortiz@example.com'}, {'personid': 7, 'fname': 'Mia', 'lname': 'Foster', 'email': 'mia_foster@example.com'}, {'personid': 8, 'fname': 'Landon', 'lname': 'Lopez', 'email': 'landon_lopez@example.com'}, {'personid': 9, 'fname': 'Audrey', 'lname': 'Cox', 'email': 'audrey_cox@example.com'}, {'personid': 10, 'fname': 'Anna', 'lname': 'Ramirez', 'email': 'anna_ramirez@example.com'} ]; |
- カラム名:値という形でのシンプルな連想配列になっています。
- データ入れ替えを考えるならば、これをJSファイルとして切り出し、動的に入れ替えるようにすれば、あとはVue.jsが良しなにやってくれます。
サンプル表示
サンプル用データは疑似個人情報生成サービスを利用しています
こういうプラグインが豊富にあるのも、Vue.jsの良いところ
ソートできるようにする
Cheetah Gridはちょっとドキュメントが十分に整備されてるとは言えない部分がありますが(Googleもいい加減なドキュメントの部分があったりするのでアレですが)、Vue.jsでソートを利用できるようにするには、以下のように加工するだけでOKです。
HTML側コード
1 2 |
<!-- グリッドのカラム --> <c-grid-column field="recid" width= "85" sort="true">ID</c-grid-column> |
- c-grid-columnにsort="true"を追加するだけです。追加したカラムだけクリックで昇順・降順が有効になります
ソート用矢印の色を変更する
ソート用の色は、Theme機能に以下のコードを追加するだけです。
1 2 3 4 5 6 7 8 9 10 11 |
data: () => ({ userTheme: { frozenRowsBorderColor: '#dbdbdb', header: { sortArrowColor: 'red' }, }, }, |
- vueの初期化のdataの中のuserThemeにて、headerセクションを追加する
- headerセクション内にsortArrowColorを定義して、16進数もしくはredなどのカラーコードを指定する
図:ソート状態がわかりやすくなりました
特定の列の値に応じてレコードの背景色を変更する
現場でこの手のGridを使ったケースでよく遭遇するのが、「特定の列の値がTrueの場合には、背景色を変更する」といったような事例。自分が現在手掛けてるアプリケーションで言えば、終了済みタスクのフラグが立ってるレコードは背景色を変更し、それが一目で終わってるタスクとわかるようにしてるといった事例です。
この実装をするには、ちょっと複雑な装備をする必要があります。
HTML側コード
1 2 |
<!-- 2列目まで固定 --> <c-grid :data="records" :frozen-col-count="4" :theme="userTheme"> |
- こちらは、c-gridタグに対して、:theme="userTheme"を加えておくだけ
- 今回はタイトル列を2行固定させています。JS側ではこの2行が重要になります。
JS側コード(index.html)
vueを初期化するコードに、Cheetah GridのuserThemeに関するオプションを、dataの中に記述する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
vm = new Vue({ el: '#grid', vuetify: new Vuetify(), data: () => ({ records : recdata, userTheme: materialDesignTheme.extends({ defaultBgColor({row, grid}) { //固定されてる行より低いrowの場合は何もしない if (row < grid.frozenRowCount) { return null; } //固定されてる行数分、rowから引いた値が配列の値と一致する var tomato = row - 1; //このvm内のrecdataにアクセスしてrestflgの値を見る if(vm.recdata[tomato].restflg==-1){ return '#DDF'; } return null; }, }) }), |
- vm.recdataにはGAS側から取得したレコードデータの塊が入っています。
- グローバル変数でmaterialDesignThemeに対して、Cheetahgridのthemeを読み込みます。
- userThemeをdataオプションの中に記述しますが、materialDesignTheme.extendsを利用します。
- defaltBgColorの関数をこの中に記述します。Cheetah Gridフォーマット時にこれらが自動で読み込まれ、rowは1行ずつ順番にdefaltBgColorが処理していきます。
- 冒頭2行は固定行なので、処理対象外とします。
- rowは行番号なのですが、検索する対象であるrecdataは0番目から精査する必要があるため、-1で0からスタートするようにしています。これを行わないと、実際のデータ数と表示するrowの数が一致しなくなり、エラーとなります。ヘッダーの固定行が2行の場合は、-2となります。
- recdata[rowの番号].restflgで、recdataの◯番目のrestflgの値を引き出せます。この値が-1のものが今回の色を変更する基準となります
- returnで直接HTML Colorの16進数の値を返せばrestflgが-1の値のレコードの背景色だけが変更されます。
- それ以外のケースでは、何もせずにreturnを返します。
図:endflgが-1のレコードだけ背景色が薄紫になった
検索機能をつける
Vue.jsでCheetah Gridを利用する際に、検索機能がやはり欲しくなります。しかし、APIらしきものが見当たらなかったのですが、よくよく考えればデータの変数と連結してるので、変数内のデータをフィルタして再度変数に格納すれば、それだけで検索絞り込みは実装出来ます。
検索窓ダイアログ用のHTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<v-app> <v-form ref="form"> <v-row justify="center"> <v-dialog v-model="dialogman" persistent max-width="700px"> <v-card> <v-container> <v-row> <v-col cols="12"> <v-text-field v-model="search" label="検索ワード" type="text" v-if="dialogman" autofocus clearable></v-text-field> </v-col> </v-row> <v-card-actions> <v-spacer></v-spacer> <v-btn color="green darken-1" text @click="onSearchman"> 検索 </v-btn> <v-btn color="red darken-1" text @click="closeman()"> 閉じる </v-btn> </v-card-actions> </v-container> </v-card> </v-dialog> </v-row> </v-form> </v-app> |
- 検索窓のテキストボックスは、v-modelにてsearch変数にバインドしておく
- 検索ボタンはonSearchman関数を実行する仕組みになっています
検索窓用ダイアログの初期化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
vm5 = new Vue({ el: '#app5', vuetify: new Vuetify(), data: () => ({ dialogman: false, search: word, }), methods: { closeman(){ //ダイアログを閉じる this.dialogman = false; }, onSearchman(){ //検索ワードでCheetah Gridのデータをフィルタしてつっこむ var temprec = []; //searchが空の場合はrecdataをそのまま戻す if(this.search == "" || this.search == undefined){ temprec = recdata; }else{ //recdataの件数を調べる var length = recdata.length //レコードデータに検索ワードが含まれていたら、temprecにpush for(var i = 0; i<length; i++){ //レコードデータにワードが含まれているか? var checkrec = JSON.stringify(recdata[i]); var tomato = checkrec.indexOf(this.search); console.log(tomato); if(tomato > 0){ temprec.push(recdata[i]); } } } //フィルタしたレコードデータをcheetah gridに突っ込む vm2.records = temprec; //ダイアログを閉じる this.dialogman = false; }, }, }) |
グリッドの高さ自動調整
Cheetah Gridを利用したウィンドウのリサイズ時に、このグリッド自身もリサイズしてくれないと非常に不格好です。自分が利用してるパターンの場合以下のようなHTML構造になっています。この状態でウィンドウのリサイズを行ったときに、ピッタリフィットするように調整します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<div id="gridarea" style="width: 100%; height: 200px;""> <!-- 5列目まで固定 --> <c-grid :data="recdata" :frozen-col-count="5" :theme="userTheme"> <!-- 編集ボタン --> <c-grid-button-column caption="💎" width="80" @click="onClickRecord"> 編集 </c-grid-button-column> <!-- TSログデータ --> <c-grid-column field="ID" width= "85" sort="true">ID</c-grid-column> <c-grid-column field="compid" width= "100" sort="true">会社ID</c-grid-column> <c-grid-column field="empid" width= "120" sort="true">社員ID</c-grid-column> <c-grid-column field="empname" width= "120" sort="true">社員名</c-grid-column> </c-grid> </div> |
gridareaのdivでc-gridの本体が括られてるので、gridareaとc-gridをリサイズ時にサイズ変更するスクリプトを走らせる必要性があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Gridの高さを自動補正する関数 function setGridHeight() { var layoutHeight = $(window).height() - 100; //ヘッダ領域分だけ減らす $('#gridarea').css('height', layoutHeight + 'px'); $('.c-grid').css('height', layoutHeight + 'px'); } //リサイズ時にウィンドウにフィットさせる window.onresize = function(){ setGridHeight(); } //gridのサイズを自動でウィンドウにフィットする $(document).ready(function () { setGridHeight(); }); |
このアプリは上部に固定する形でtoolbarエリアが存在し、その分だけgridエリアのサイズから減らしてあげています。
複数列ソートが出来ないので
Cheetah Gridは残念な事に、複数列ソートが出来ません。単一の列でしかソートが出来ない為、元のデータのIDが複数列を考慮して連番にでもなっていない限り、ケースによっては綺麗にソートが成しえません。そこで、取得したデータに対して、JavaScriptにてソートを掛けたものをレコードソースとして使う事で、表示時に目的の順番にソートされたデータとして表示するようにしています。
Cheetah Gridは連想配列のJSONオブジェクトをデータソースとして使っている為、反映する前にソートを掛けてあげます。元のコードはstackoverflowに掲載されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//複数キーでソートを掛ける関数 function sortByMultipleKey(keys) { return function(a, b) { if (keys.length == 0) return 0; // force to equal if keys run out key = keys[0]; // take out the first key if (a[key] < b[key]) return -1; // will be 1 if DESC else if (a[key] > b[key]) return 1; // will be -1 if DESC else return sortByMultipleKey(keys.slice(1))(a, b); } } //データを取得してGridに反映する ipc.on("gettslogdata", (arg) => { //データを取り出す var data = arg.data; //JSONデータを2つのキーでソートする(昇順) data.sort(sortByMultipleKey(['empid', 'workdate'])); //dataを反映する vm.recdata = data; }); |
vm.recdataがCheetah Gridと連結してる変数なので、ここにソート後のデータを入れてあげれば、上記の例で言えば、empidおよびworkdateの2つの列を持ってして昇順でソートを掛けています。
データが表示されない場合
アプリ起動時のみCheetah Gridのレコードソースに対して、メインプロセスからのデータを受け取り格納しても、なぜかスクショのようにデータは表示されず、特にエラー表示も出ず。といった現象に遭遇する事があります。リサイズを行うと正常に描画されるので、きちんとデータは入っているのも確認済みで、以降はデータを入れ替えてもきちんと表示はなされます。
ということで、この現象の対策は、起動時のデータリクエストの返り値を受け取った関数内で、Windowをリサイズするコードを追加する事で対応しました。HTML側に記述します。
1 2 3 |
//起動時にリサイズ var layoutHeight = $(window).height(); window.resizeTo(1200, layoutHeight); |
Cheetah GridはDOMではなくCanvasを使ってデータを描画しているので、この辺りが原因なのかもしれません。
図:おかしな表示になってデータが出てこない
リサイズ時やv-tabでの切替時にオカシナ表示
v-tabで複数のタブ表示をし、それぞれに別々のCheetah Gridを表示させてみたのですが、タブ切り替え時や最大化した時にタブの変更を行うと、前述と同様にそこにデータはあるはずなのに、Cheetah Gridで適切な表示がされない現象があります。これは最新版でも変わらず。style.displayの属性でblockやnoneを指定しても同様の症状が出ます。そこで、これをなんとか出来ないかなと色々トライしてみた結果、親のgridareaというdiv属性にv-show="grid1 == 0"といったように、ディレクティブを追加し、grid1の値で判定し、表示非表示するようにした所、タブ切り替えや最大化表示でもオカシナ表示にならずに大丈夫でした。
Vue.js版のCheetah Gridにグリッドのリペイントイベントなどがあれば良いのですが、用意されていないようなので、このようなバッドノウハウを使う必要があります。
※Cheetah Grid 1.6までは利用できますが、1.7にすると利用できませんでした。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 |
<template v-slot:extension> <v-tabs v-model="model"> <v-tabs-slider color="yellow"></v-tabs-slider> <v-tab @Click="resizeman(1)"> 元データ </v-tab> <v-tab @Click="resizeman(2)"> 補正判定 </v-tab> </v-tabs> </template> |
- HTMLタグとしては、v-tabs-itemsなどは使用しません。
- tabをクリックしたら、VueのMethod内に用意したresizemanを実行するように、クリックイベントを追加しています。
Cheetah Grid部分
1 2 3 4 5 6 7 8 |
<div id="gridarea" style="width: 100%; height: 300px;" v-show="grid1 == 0"> <c-grid :data="recdata" :frozen-col-count="5" :theme="userTheme"> <!-- TSログデータ --> <c-grid-column field="ID" width= "85">ID</c-grid-column> </c-grid> </div> |
- divにv-showディレクティブとgrid1の変数参照を追加しておく。
- vue側にgrid1の変数を用意しておくことを忘れずに
JS側コード
1 2 3 4 5 6 7 8 9 10 |
methods: { resizeman(num){ this.grid1 = num; //Cheetah Gridのバグ対策 var layoutHeight = $(window).height(); var layoutWidth = $(window).width(); window.resizeTo(layoutWidth, layoutHeight + 29); window.resizeTo(layoutWidth, layoutHeight + 28); }, |
- タブクリックで、v-showの中の値を切り替えると、cheetah Gridのパネルも切り替えられる。
- 強制的にウィンドウサイズをわずかだけ伸縮させると再描画される。+29や+28はこのアプリの場合の枠内の高さを調整しています。
Auto Paging機能を使ってみた(Node.js)
※本項目だけNode.js/Electronで取り扱っています。Google Spreadsheetだと大容量のデータを扱うには向いていない為(最大500万セルだと、20カラムで25万件が限界であるため)
Cheetah Gridはデータさえ取得できれば、何十万件のデータを表示してもスクロールでもたついたりしない為、大容量のデータを扱うElectronアプリ等でも非常に有用です(試しに20カラム48万件のデータを表示してみましたが、表示後は非常にスムーズ)。
しかし、問題はVueおよびVuetify、ElectronのIPC通信が、例えばSQLiteなどからゴッソリ40万件のデータを取ってきても、すぐにCheetah Gridに渡してあげられない(非常に遅い)のが難点。表示まで分単位で待つのは実用に耐えません。そこで、Cheetah GridのAutopaging機能を利用して、1万件ずつ遅延ロードで取得してCheetah Grid用の変数にあとからpushするような仕組みを構築しました。但しこの機能はCheetah GridのVue.jsコードでは使えないので、JavaScript APIにて実装しています(Cheetah GridのAPIはVue用とJavaScript用とで使える機能に大きく差がある)。
以下のエントリー内で、Auto Pagingを利用時にSQLiteから順次1万件取得してCheetah Gridに表示する方法を記述しています。
Cheetah Grid用の変数はrecdataというグローバル変数にIPC通信でメイン⇒レンダラ側に格納しています。
HTML側コード
1 |
<div class="sample demo-grid middle"></div> |
今回はJSでの実装なので、特にVueのような特殊なタグを使わず、サンプルのままです。
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
//グローバル変数 var recdata = []; //Cheetah Grid用のデータ格納場所 var rectotal = 500000; //レコード件数合計 const GET_RECORDS_SIZE = 10000; //遅延ロードで呼び出すレコードの数 const BUFFER_RECORDS_SIZE = (Math.floor(GET_RECORDS_SIZE / 2) || 1); const loadedData = {}; let isAllLoaded = false; //Cheetah Gridに連結するレコードソース const dataSource = new cheetahGrid.data.CachedDataSource({ get(index) { const loadStartIndex = Math.floor(index / GET_RECORDS_SIZE) * GET_RECORDS_SIZE; if (!loadedData[loadStartIndex]) { const promiseObject = getRecordsWithAjax(loadStartIndex, GET_RECORDS_SIZE).// return Promise Object then((data) => { if (isAllLoaded) { return data; } // length update? const length = loadStartIndex + data.length; if (data.length < GET_RECORDS_SIZE) { // all loaded!! dataSource.length = length; isAllLoaded = true; } else if (dataSource.length <= length) { // append length!! dataSource.length = length + BUFFER_RECORDS_SIZE; } return data; }); loadedData[loadStartIndex] = promiseObject; } return loadedData[loadStartIndex]. then((data) => data[index - loadStartIndex]); }, length: BUFFER_RECORDS_SIZE, }); //Cheetah Gridを初期化する const grid = new cheetahGrid.ListGrid({ //Grid表示要素の指定 parentElement: document.querySelector('.sample'), //グリッドのタイトル行と値の指定 header: [ {field: 'ID', caption: '社員番号', width: 100}, {field: 'fname', caption: '姓', width: 200}, {field: 'lname', caption: '名', width: 200}, {field: 'email', caption: 'メアド', width: 250}, ], //列固定の何列目まで固定するか frozenColCount: 1, }); grid.configure('fadeinWhenCallbackInPromise', true); //データをセットする grid.dataSource = dataSource; //遅延ロードでデータを送り出す関数 function getRecordsWithAjax (startIndex, num) { return new Promise((resolve) => { const loadedCount = startIndex + num; let last = false; if (loadedCount >= rectotal) { num = rectotal - startIndex; last = true; } setTimeout(() => { const records = []; for (let i = 0; i < num; i++) { //recdataからデータを取り出してrecordsにpushする records.push(recdata(startIndex + i)); } resolve(records); }, 500); }); } |
- Cheetah Gridに直結するのは取得したデータを格納するrecdata変数ではなく、そこからGET_RECORDS_SIZEで指定し、getRecordsWithAjax関数にて取り出したレコードを格納するdatasource変数です
- 予めrecdataに格納するデータの件数をrectotal変数に入れておく必要があるので、IPC通信でデータを取得時にでもlengthを図って、入れておくと良いでしょう(今回は50万件キッカリで暫定的に指定)
- JavaScript APIで構築してるので、これまでのようなVueでの初期化ではない方法でCheetah Gridを初期化しています。
- getRecordsWithAjax関数は初期化時およびデータの表示件数が終端まで到達した時に、自動で呼び出されて追加のデータをGET_RECORDS_SIZEで指定した文だけ、datasourceにpushしていまs(一度pushした後、重複してpushされたりしない)
- 再取得と追加表示にラグが生じるので、GET_RECORDS_SIZEの値はなるべく小さめのほうが良い。
- Electron等で1万件ずつ、メインプロセスから追加で取得するには、データの総合計件数を取得⇒offsetとLimit句でデータの件数と取得開始場所を指定⇒recdataに追加格納⇒getRecordsWithAjax関数にてrecordsにpushという手順を踏む必要があります。コレを行う事で大量のデータであっても遅延ロードで表示し切る事が可能(getRecordsWithAjax関数を改造が必要)
- IPC通信で送って値を取得して返す場合は、ipc.sendではなくipc.sendSync(もしくはinvoke)を使い、メインプロセス側はevent.sender.sendではなくevent.returnValue(もしくはipcMain.handleで受けて、returnで返してあげる(以下のような構文を使う)。同期的処理が必要なので、レンダラ側は前述のようにPromiseで処理が受け待処理が必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//レンダラプロセス側 var ipc = window.api; let msg = "トマトの値段" let data = ipc.sendSync('dataget', 'msg'); //dataにメイン側からの値が返ってくる //メインプロセス側 ipcMain.on('dataget', (event, msg) => { //msgでレンダラ側から引数を受け取る console.log(msg) //レンダラ側に直で返す event.returnValue = '1000円' return }) |
関連リンク
- 2020年秋にVue.jsのアプリケーションを作るなら、押さえておきたい5つのポイント
- GAS + Vue.js + Vuetify でスプレッドシートの編集履歴を表示する
- 【GAS x Vue.js】JavaScript のみで今、家計簿をつくるとしたら【ハンズオン付き!】
- GASでWEBアプリケーションを作るときはちゃんと考えよう
- Vuetifyを高速で学ぶための32本の無料動画講座を紹介します
- Vuetify.js を使ってマテリアルデザインに挑戦しよう!
- 【入門】Vuetifyで手軽に綺麗なWebページを作る
- VuetifyのDate Pickerを日本語化したら"日"ってついてくる、やだキモい
- Vuetifyで画面デザインをサボってみた
- 【vue.js】Vuefityをマスターする(1)
- Vue.jsのUIフレームワーク「Vuetify」の使い方
- Vuetify.js のスニペット集Vuetify.js のスニペット集
- Vue + Vuetify data table first column image
- [Vuetify] Unable to locate target [data-app]
- Event fired when bottom sheet is active or hidden - Vuetify
- Vuetify.js を使ってマテリアルデザインに挑戦しよう!
- Material Design Icons
- CSS/jQueryでoverflow-y:auto/scrollなエリアのスクロールバーを非表示にする
- Vue.jsメモ:属性、ループ内呼び出し、など
- vue.js reference div id on v-on:click
- Vue.jsで最速に始めるCheetah Grid
- 【JavaScript】最速のOSS Data Table/Grid/SpreadSheetを探して、
- Show 1,000,000 records without stress
- vue-cheetah-grid example
- Vue.js おすすめライブラリ 21選(おまけ+1)
- add custom part to v-autocomplete dropdown
- vuetify v-select multiple text values
- vuetify.jsのautocompleteを利用して、よみがな検索した結果を表示する方法
- v-autocompleteとユーザー入力を値として設定
- [JavaScript] 正規表現パターンサンプル集
- 「v-tooltip」でvue.js対応のツールチップを簡単に実装する
- In vuetify, radio is not shown
- 英数字、-、_、およびスペースを許可するための正規表現
- Vue Scheduler Component| Vue Event Calendar
- Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)
- 【忘却のJavaScript #3】Vue.jsでカレンダーを表示してみる
- カレンダーページを「fullcalendar-vue」で実装する
- コンポーネントのカスタムイベントでキャメルケース名のイベントが動かない時の対処法
- FullCalendar Document
- FullCalendar Codepen sample
- Vue Full Calendar (next, prev) buttons trigger
- FullCalendarをそこそこカスタマイズしてみる。
- 【jQuery】FullCarendar
- FullCalendar Vue - CodeSandbox
- FullCalendar Vue - CodeSandbox 2
- FullCalendarの日付を操作するサンプル
- FullCalendar Holiday Hightlight
- vue Fullcalendar eventRender, add vue component
- FullCalendarで特定の日を色付けする方法はありますか?
- fullcalendarで、イベントの背景色をさまざまな色に変更するにはどうすればよいですか?
- Fullcalendarでイベント作成するも、なぜか月表示した時に、9時以降にならないとその日に表示されない問題
- Is there a way to freeze a column in a vuetify data table?
- vuetify/v-data-tableの列固定
- CodePen Home A fixed columns table in vuetify
- sticky(CSS)を使って列を2列分固定する方法を教えてください
- Vuetify data table slow for large number of objects
- JavaScript sort array by multiple (number) fields
- Vuetify - How to keep the first expansion panel open by default and if I open another panel, the others should be close?
- [Electron] IPC には新しい ipcRenderer.invoke() メソッドを使ったほうが便利 (v7+)
- add hyperlink in v-data-table Vuetify
- filterでv-forの出力を制御してみよう
- Vuetifyのv-checkboxやv-switchなどは:valueではなくて:input-valueでコンポーネントの値を決めないといけなかった