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も利用する必要があります。
事前準備
ウェブ上で公開されてる事前準備では、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を縦に並べてみた
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値が取得できない)
注意点
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
アプリケーションの顔とも言える、アプリ上部にあるヘッダに該当するナビゲーションバーです。この他にもファイルやヘルプなどのツールバーなどもあったりします(もちろん、モバイル向けの下部ナビゲーションも)。ただし、サンプルのままで動作しようとすると、このナビゲーションバーにコンテンツ部分が被ったりと色々と面倒な部分もありましたが、以下のコードで動作させることが可能です。
ソースコード
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を使わず別の確認メッセージを表示する手段を利用するようにしましょう。
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として割り当ててます。
- itemsのiconはMaterial iconの文字列を指定、textはリストのタイトル。それぞれをVue.jsで割当
サンプル表示
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 |
<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" :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ページ毎のレコード数に今回置き換えています。
- 移動用のアイコンも同様に標準のものから、マテリアルアイコンを指定して置き換える事が可能です。
- マルチソート、グルーピング、行選択などなど、幅広いオプションが用意されているので、気にいる形式に仕立て上げる事が可能です。社員マスタなどのマスタ系を弄るのに向いていると思います。
図:ダミーデータを10件ずつ表示してみた
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 |
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) || 'フリガナの形式がオカシイ' } }, }, }), 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側へデータを送って書き込ませるようにします。
図:入力欄チェックはデータの整合性を保つのに必要
組み合わせの妙技
テキストボックスとカレンダー連動+α
非常に便利でリッチな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名を入れておき、読み書き対象の場所を特定できるように指定しておく。
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での調整を利用しています。こちらのほうがスッキリ決まります。
図:標準のタグよりもCSS調整のほうが綺麗に決まることもある
Vue.jsのプラグイン
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側コード
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 |
<style> .tooltip { display: block !important; z-index: 10000; } .tooltip .tooltip-inner { background: black; color: white; border-radius: 16px; padding: 5px 10px 4px; } .tooltip .tooltip-arrow { width: 0; height: 0; border-style: solid; position: absolute; margin: 5px; border-color: black; z-index: 1; } .tooltip[x-placement^="top"] { margin-bottom: 5px; } .tooltip[x-placement^="top"] .tooltip-arrow { border-width: 5px 5px 0 5px; border-left-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; bottom: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .tooltip[x-placement^="bottom"] { margin-top: 5px; } .tooltip[x-placement^="bottom"] .tooltip-arrow { border-width: 0 5px 5px 5px; border-left-color: transparent !important; border-right-color: transparent !important; border-top-color: transparent !important; top: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .tooltip[x-placement^="right"] { margin-left: 5px; } .tooltip[x-placement^="right"] .tooltip-arrow { border-width: 5px 5px 5px 0; border-left-color: transparent !important; border-top-color: transparent !important; border-bottom-color: transparent !important; left: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .tooltip[x-placement^="left"] { margin-right: 5px; } .tooltip[x-placement^="left"] .tooltip-arrow { border-width: 5px 0 5px 5px; border-top-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; right: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .tooltip.popover .popover-inner { background: #f9f9f9; color: black; padding: 24px; border-radius: 5px; box-shadow: 0 5px 30px rgba(black, .1); } .tooltip.popover .popover-arrow { border-color: #f9f9f9; } .tooltip[aria-hidden='true'] { visibility: hidden; opacity: 0; transition: opacity .15s, visibility .15s; } .tooltip[aria-hidden='false'] { visibility: visible; opacity: 1; transition: opacity .15s; } </style> <v-btn icon v-tooltip.bottom="'レコードのエクスポート'"> <v-icon dark text>mdi-export</v-icon> </v-btn> |
- CSSはツールチップのデザインを担当しています
- ツールチップを表示したい項目にv-tooltip.bottom=でコメントを入れればOK。bottomの他にもtopとすれば上部に表示されます。今回のケースではアイコンの下に表示がなされます。
JS側コード
1 2 3 4 5 6 |
<script src="js/v-tooltip"></script> <script> //v-tooltipプラグインを読み込む Vue.use(VTooltip) </script> |
- CDNでライブラリは提供されているので、vuetifyやvue.jsのJSファイルとともに追加すればOK
- 追加したら、Vue.use(VTooltip)を実行すればそのHTML内でツールチップが有効になります。
- 別途用意したjsフォルダ内に、v-tooltip.min.jsを格納してから呼び出しています。CDNは利用していません(CDN側はVue.js v3.0対応となっているため、互換性がありません)
図:アイコンにマウスカーソルで表示されます
FullCalendar
現在、別件で工数管理をしたいという要望が出てきたので、同じくVue.js + Vuetifyを利用して従業員の人に工数登録をしてもらうアプリケーションを構築中です。手際よく登録できるよう可能な限り手数を削って登録できるように構築しているのですが、そこでやはり欠かせないのが「カレンダー」。といっても、VuetifyのDate Pickerなカレンダーではなく、Googleカレンダーのようなフル規格のカレンダーが必要です(これがないと入力漏れや入力手間の削減が難しい)
そこで今回利用してみてるのが「FullCalenderライブラリ」です。Vue.jsのプラグインとして実装されているものなのですが、ちょっと他のプラグインとは違うコーディング方法が必要な箇所があるので備忘記録として残しておきます。
※今回はv4.x系を使っています。v5.0はまた大分コードが異なるので暇があったら実装したい。
ソースコード
Vue.jsの拡張プラグインとして、CDN配布されているので、vue.jsの読み込み後に以下のコードで追加で読み込むようにします。まずは、JSライブラリ
1 2 3 4 5 |
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@4.2.0/main.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/interaction@4.3.0/main.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@4.2.0/main.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/vue@4.2.2/main.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@4.2.0/locales/ja.js"></script> |
次にCSSをHEADセクションに追記しておきます。
1 2 3 |
<link href="https://cdn.jsdelivr.net/npm/@fullcalendar/core@4.2.0/main.css" rel='stylesheet'> <link href="https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@4.2.0/main.css" rel='stylesheet'> <link href="https://unpkg.com/@fullcalendar/timegrid@4.3.0/main.min.css" rel='stylesheet'> |
追加のCSSとしては以下のようなものを表記のカスタマイズとして使っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<style> .fixed-bar { /* toolbarを固定化 */ position: sticky; position: -webkit-sticky; /* for Safari */ top: 0em; z-index: 9; } html::-webkit-scrollbar{ /* メイン画面だけスクロールバーを消す */ display: none; } .fc-sun { /* 日曜日の日付の文字色 */ color:red } .fc-sat { /* 土曜日の日付の文字色 */ color:blue } .fc-widget-header { /* カレンダーのタイトル行の背景色 */ background-color:#d1ffd1; } </style> |
HTML部分の記述
今回は、Vuetifyのタブの1つの中にカレンダーを表示しようと思っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<v-tab-item value="tab-1" color="white"> <div id="calspace"> <script type="text/x-template" id="calender"> <fullCalendar @dateClick="handleDateClick" ref="fullCalendar" default-view="dayGridMonth" :plugins="calendarPlugins" :locale="'ja'" :header="header" :buttonText="buttonText" :height=height :events="events" :custom-buttons="customButtons" :eventLimit="eventLimit" /> </script> <calender /> </div> </v-tab-item> |
- Vue.jsではHTMLに色々イベント名を直書きしても、それらはすべて小文字として扱われてしまいます。そこで、scriptタグにてidがcalendarという名前でfullCalendarタグの中身をラッピングしています。
- これは特に@dateClickのイベント名対策で、このラッピングを行わないと、HTML上に記述してもイベントが認識されません。
- dateClickでhandleDateClickという関数が、カレンダーの1マスをクリック時のイベント発火が行われ明日。
- localeは日本語を指定したいので、jaを指定しています。
- custom-buttonsは後で出てきますが、既存の月移動のボタンをオーバーライドでちょっと上書きする形で指定してます。
- カレンダーの高さはアプリのタブ内の大きさによって自動でフィットするように変数heightを指定してあり、リサイズ時にはサイズを正確に算出してこの変数に値を書き込ませています。
- タグ側でeventLimitの設定を記述しておき、変数eventLimitがtrueの場合、マス目に入り切らないイベントがあった場合、「+2 event」といった表示でまとめてくれます。
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 |
var vm; var calman; var events = [ { title: 'event 1', date: '2021-01-01' }, { title: 'event 2', date: '2021-01-02' } ] var materialDesignTheme; const dayGridPlugin = window.FullCalendarDayGrid.default; const FullCalendarInteraction = window.FullCalendarInteraction.default; //Body読み込み時に初期化する function initial() { materialDesignTheme = vueCheetahGrid.cheetahGrid.themes.MATERIAL_DESIGN; //FullCalendarプラグインを読み込む Vue.use(FullCalendar) //ケバブだと動かないので独自コンポーネント化しておく calman = Vue.component('calender', { template: '#calender', data: function() { return { eventLimit: true, calendarPlugins: [ dayGridPlugin, FullCalendarInteraction ], calendarWeekends: true, header:{ left: 'title', center: 'addPostButton', right: "prev,next today" }, buttonText: { today: '今日' }, height:function(){ //高さの自動調整 var y = $('#calspace').position().top; var layoutHeight = $(window).height() - y - 110; return layoutHeight; }, events: events, customButtons: { prev: { // this overrides the prev button text: "PREV", click: () => { //前の月へ移動 let calendarApi = this.$refs.fullCalendar.getApi(); calendarApi.prev(); //移動先の1日の日付を取得 console.log(calendarApi.getDate()); } }, next: { // this overrides the next button text: "NEXT", click: () => { //次の月へ移動 let calendarApi = this.$refs.fullCalendar.getApi(); calendarApi.next(); //移動先の1日の日付を取得 var nextmonth = calendarApi.getDate() console.log(nextmonth); } } }, } }, methods: { handleDateClick: function(info) { //クリックされた日付を取得 var seleday = info.dateStr //イベントを追加登録してみる events.push({ title: 'event 3', date: '2021-01-03', tomato:'test' }) alert('click day :' + info.dateStr); } } }); } |
- グローバルの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プロパティと色指定をつけるだけです。
1 |
{ title: 'event 2', date: '2021-01-02', backgroundColor: '#335af5' } |
このマス目ですが、実は通常のHTMLのテーブルで出来ているので、イベントデータをロードした時に、以下のようなコードでtdに対してjQueryでクラスを付与し、対応するクラスのCSS側で色の設定をしておけば、OKです。
CSS
1 2 3 |
.workman { background: #a7f4fa; } |
- workmanという名前の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 |
<v-tabs-items v-model="model"> <v-tab-item value="tab-1" color="white"> <div id="calspace"> <script type="text/x-template" id="calender"> <fullCalendar @dateClick="handleDateClick" ref="fullCalendar" default-view="dayGridMonth" :plugins="calendarPlugins" :locale="'ja'" :header="header" :buttonText="buttonText" :height=height :events="events" :custom-buttons="customButtons" :navLinks="true" :eventLimit="eventLimit" @eventRender="eventRender" /> </script> <calender /> </div> </v-tab-item> <v-tab-item value="tab-2" color="white"> 通知 </v-tab-item> </v-tabs-items> |
- FullCalendarのタグに新たに「@eventRender=”eventRender”」を追加し、レンダリング時のイベントを付与する
JS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
eventRender(info){ //イベントが一個描画されるごとに実行する var eveDate = new Date(info.event.start) var yearval = eveDate.getFullYear(); var monthval = eveDate.getMonth() + 1; var dateval = eveDate.getDate(); //冒頭に0を追加する場合 var paddingZero = function(n) { return (n < 10) ? '0' + n : n; }; //0埋め実行 monthval = paddingZero(monthval); dateval = paddingZero(dateval); //対象の日付を生成 var tempdate = yearval + "-" + monthval + "-" + dateval; //対象の日付の属性を持つ要素にclass追加 $('[data-date=' + tempdate + ']').addClass('workman'); } |
- 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キーを取得します。以下の手順で取得します。
- Google Cloud Consoleへログインする
- 左上のメニューからAPIとサービス⇒ライブラリを開く
- 検索窓よりCalendarを検索し、Google Calendar APIをクリックする
- 追加ボタンをクリック
- 左上のメニューからAPIとサービス⇒認証情報を開く
- 認証情報作成をクリックし、APIキーをクリック
- キーが生成されるのでコピーしておく
- キーを制限をクリックする
- APIの制限にて、キーを制限にチェックを入れて、Select APIsではGoogle Calendar APIを選択し保存をクリック
- アプリケーションの制限はお好みで
図:APIキーは必ず制限をつけましょう
CSS
1 2 3 4 5 6 7 8 9 |
/* Googleカレンダーイベントの休日名の色指定 */ .fc-holiday, .holiday-text { color: #e74c3c; } /* Googleカレンダーイベントの休日イベントは非表示にする */ .holidayman { display:none; } |
- Google Calendarの休日の文字色と、今回はGoogle側イベントはイベントとしては非表示にするので、そのための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 29 30 |
<v-tabs-items v-model="model"> <v-tab-item value="tab-1" color="white"> <div id="calspace"> <script type="text/x-template" id="calender"> <fullCalendar @dateClick="handleDateClick" ref="fullCalendar" default-view="dayGridMonth" :plugins="calendarPlugins" :locale="'ja'" :header="header" :buttonText="buttonText" :height=height :calendarOptions="calendarOptions" :custom-buttons="customButtons" :navLinks="true" :eventLimit="eventLimit" @eventRender="eventRender" :googleCalendarApiKey="googleCalendarApiKey" :eventSources="eventSources" /> </script> <calender /> </div> </v-tab-item> <v-tab-item value="tab-2" color="white"> 通知 </v-tab-item> </v-tabs-items> |
- FullCalendarタグのオプションに、新規に:googleCalendarApiKeyと:eventSourcesを追加し、これまで使ってた:eventsは削除する
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 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/4.2.0/google-calendar/main.min.js"></script> <script> ・・・・前略・・・・ const googleCalendar = window.FullCalendarGoogleCalendar.default; Vue.component('calender', { template: '#calender', data: function() { calendarPlugins: [ dayGridPlugin, FullCalendarInteraction, googleCalendar ], googleCalendarApiKey: 'ここにAPIキーを記述', eventSources: [ { events: events, //ここが通常のイベント }, { // 日本の祝日 googleCalendarId: 'ja.japanese#holiday@group.v.calendar.google.com', //設定class Name className: 'holidayman', }, ], ・・・・・中略・・・・ methods: { eventRender(info){ //イベントが一個描画されるごとに実行する var eveDate = new Date(info.event.start) var yearval = eveDate.getFullYear(); var monthval = eveDate.getMonth() + 1; var dateval = eveDate.getDate(); //冒頭に0を追加する場合 var paddingZero = function(n) { return (n < 10) ? '0' + n : n; }; //0埋め実行 monthval = paddingZero(monthval); dateval = paddingZero(dateval); //対象の日付を生成 var tempdate = yearval + "-" + monthval + "-" + dateval; //要素の情報を取得 var classman = info.el.className; var titleman = info.el.text if(classman.includes("holidayman")){ //休日名を一旦消去 $('.fc-day-top[data-date=' + tempdate + ']').each(function(i, elem) { $('.fc-day-top[data-date=' + tempdate + '] .holiday-text').remove(); }); //休日名を付与 jQuery('.fc-day-top[data-date=' + tempdate + ']').prepend("<span class='holiday-text'>" + titleman + "</span>"); return false; } ・・・・・中略・・・・ } ・・・・・後略・・・・ }, }, }); </script> |
- 追加で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名は分けましょう。
図:休日名がカレンダーに併記出来ました
Cheetah Grid
業務用アプリケーションを構築する上で、特にPC向けで構築する場合にほぼ必須になるコンポーネントが「Grid」。VuetifyにもGridはあるのですが、欲しいのはこういうものじゃない・・・これまでは、w2ui-gridやSlickGridなどを使ってましたが、前者は以前は高速だったのですが、現在は重く切り替えも非常にもたつく、後者は高速なのですが既にメンテされていないし、構築が結構面倒。
ということで見つけたのが、Cheetah Grid。Vuetifyじゃなく、Vue.jsのプラグインとして実装されているものになりますが、マテリアルデザインで表現されます。
列の固定やマルチプルカラム(複数列にまたがるカラムタイトルを作れる)などが可能で、非常に高速に表示が出来るというので、手を出してみました。デモページはこちらになります。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 |
//cheetah Gridプラグインを読み込む Vue.use(vueCheetahGrid); materialDesignTheme = vueCheetahGrid.cheetahGrid.themes.MATERIAL_DESIGN; vm2 = new Vue({ el: '#grid', vuetify: new Vuetify(), data: () => ({ records : recdata, userTheme: materialDesignTheme.extends({ defaultBgColor({row, grid}) { if (row < grid.frozenRowCount) { return null; } var tomato = row - 2; if (recdata[tomato].endflg == -1) { return '#DDF'; } return null; }, }) }), |
- recdataにはGAS側から取得したレコードデータの塊が入っています。ページ表示時に取得してrecordsに入れています。
- グローバル変数でmaterialDesignThemeに対して、Cheetahgridのthemeを読み込みます。
- userThemeをdataオプションの中に記述しますが、materialDesignTheme.extendsを利用します。
- defaltBgColorの関数をこの中に記述します。Cheetah Gridフォーマット時にこれらが自動で読み込まれ、rowは1行ずつ順番にdefaltBgColorが処理していきます。
- 冒頭2行は固定行なので、処理対象外とします。
- rowは行番号なのですが、検索する対象であるrecdataは0番目から精査する必要があるため、-2で0からスタートするようにしています。これを行わないと、実際のデータ数と表示するrowの数が一致しなくなり、エラーとなります。
- recdata[rowの番号].endflgで、recdataの◯番目のendflgの値を引き出せます。この値が-1のものが今回の色を変更する基準となります
- returnで直接HTML Colorの16進数の値を返せばendflgが-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; }, }, }) |
関連リンク
- 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 のスニペット集
- [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で、イベントの背景色をさまざまな色に変更するにはどうすればよいですか?
コメントを残す