Vue.js + GAS スプレッドシートを編集するサイドバーを作成する

# スクリプトファイルの作成
サイドバーはドキュメントやスプレッドシートに作成することができます
スクリプトファイルはコンテナバインドである必要があります。
（スタンドアローンでは作成できません）

# createMenu()とaddMenuでメニューを作成
``````
function onOpen() {
 var ui=SpreadsheetApp.getUi()
 var userMenu=ui.createMenu("案件登録")
 userMenu.addItem('登録', 'openOfferMenu');
 userMenu.addItem("更新", 'openNegoMenu');
 userMenu.addItem('申請', 'openSettleMenu');
 userMenu.addToUi();
};

function openOfferMenu(){openMenu("new")}
function openNegoMenu(){openMenu("edit")}
function openSettleMenu(){openMenu("submit")}
``````
onOpenはスプレッドシートを開いた時に実行される関数
uiクラスのcreateMenuメソッドでメニューを作成する
メニューを追加するにはaddItemメソッドを使う
最後にaddToUiメソッドでメニューが完成

# サイドバーのHTMLを返す関数を作成
サイドバーの中身はHTMLで記述しています。
uiクラスのshowSidebarメソッドを使って、作成したHTMLテンプレートをサイドバーとして表示します。
include()は後に解説しますひとつのhtmlファイルに他のhtmlファイルを埋め込む時に使う関数です。

main.gs

``````JavaScript
function openMenu(menu) {  //menuには"offer","nego","settle"のいずれか
  var ui = SpreadsheetApp.getUi();
  var html = HtmlService.createTemplateFromFile("00application")
  html.menu=menu  //これの解説は後ほど
  ui.showSidebar(html.evaluate().setTitle(menu));
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
``````
## 解説
二行目は、indexという名前のHtmlOutputオブジェクトを生成している。
getUiメソッドはバインドしているドキュメント（コンテナといいます）のUIを操作するためのUiオブジェクトを取得する
showSidebarメソッドは、htmlOutputオブジェクトをサイドバーに反映させて表示するメソッド

# 枠組みを作成（html側の全体像）
vue.jsを活用します。
メニューによって異なるページを表示しているように見えますが、用意しているページはapplication.htmlただ一つで、中に配置するコンポーネントを変えているだけです。

こういう作りにしている理由は、ページを開いた時のアクション（他のスプレッドシートからデータを読み込むなど）やページのレイアウトは共通しているため、別個にページを作ってしまうと一つ修正する時に全て修正する必要があるからです。

application.html

``````javascript
<script src="https://jp.vuejs.org/js/vue.js"></script> <!--vueのインストール-->

<?!= include("コンポーネント記述ファイル1") ?>
<?!= include("コンポーネント記述ファイル2") ?>
・・・

<div id="user_menu">
  <component1></component1>
  <component2></component2>
</div>

<script>
var vm=new Vue({
  data: {
  },
  ・・・
}.$mount("#user_menu"),
``````

component.html

``````javascript
<script type="text/x-template" id="form">
  <form name="form" class="box">  //formにしているのはvueを使っていなかった時の名残なので、divでいいです
  </form>
</script>

<script>
Vue.component("component1",{
  template: '#form',
  props:{
  },
  methods:{
  }
})
</script>
``````

# 今のページが何なのか区別する

application.htmはひとつしかありませんので、今のページがnewなのかeditなのかsubmitなのかの状態をvueのデータに保持して、コンポーネントの表示有無を判断します。

それには、application.htmlのどこかに <?= menu ?>をつけてやり、gsでhtmlテンプレートを作成した時にmenuに値を代入し、ページ作成後にvueでmenuの値を読み込むという方法をとります。

application.html

``````javascript
<div id="user_menu">
  <span ref="menu"> <?= menu ?></span>
</div>

<script>
var vm=new Vue({
  data: {
    menu:"",
  },
  mounted() {
    this.menu=this.$refs.menu.innerHTML //今の画面がofferかnegoかsettleが判別
  },
・・・
``````

gs

``````JavaScript
function openMenu() {
  var ui = SpreadsheetApp.getUi();
  var html = HtmlService.createTemplateFromFile("00application")
  html.menu=""  //ここで"new","edit","submit"を入れる
  ui.showSidebar(html.evaluate().setTitle(menu));
}
``````

# サイトを開いた時に他からデータを引っ張ってくる
ページを開いた時に他サイトの情報（API、スクレイピングなど）を取ってくる必要があると思います。
ここでは複数のスプレッドシートの情報を引っ張ってきて、vueのdataに放り込みます

流れとしては、
1. html側からgoogle.script.runを使用してgs側のファイル取得関数を実行
2. gs側から返ってきた値をdataに挿入
3. 以上の動作をPromiseで行い、全てのデータ取得が終われば次のアクションに移る

gs側

``````javascript
②function doFunction(funcName){ //html側から送られてきた関数名を実行する
  return eval(funcName)  
}

③function get_funcName(){
//スプレッドシートをJSON形式で返す。シートは1枚目を取得し、1行目をヘッドに設定します。
  const contents=SpreadsheetApp.openById(fileId).getSheets()[0].getDataRange().getValues()
  const head=contents.shift()
  const values = contents.reduce((acc,cur,idx)=>{
    var value = cur.reduce((a,c,i)=>{a[head[i]]=c;return a;},{})
    acc.push(value);return acc;
  },[])
  return JSON.stringify(values)
}
``````

html側（application.html）

``````javascript
  created() {
    this.fetchDatas();
  },
  methods:{
 ④   fetchDatas: async function(){
      var self=this;
      const b=["file1","file2","file3"].map(e=>{return this.fetchData(e)});
      await Promise.all( b )
    },
 ① fetchData(dataName){ //ひとつのスプレッドシートから返される配列は一つだけの前提
      return new Promise((resolve,reject)=>{
      google.script.run
      .withSuccessHandler(res=>{
        var values=JSON.parse(res);
        this[dataName]=values;
        resolve(dataName+"はOK")})
②    .doFunction( "get_"+dataName+"()");
      })
    },
  }
``````

## 解説
①google.script.runでgs側のfileを返す関数を実行するのはfetchData関数です。
google.script.runはそのままでは非同期を実行しにくいので、Promiseを返すようにしています。

②google.script.runの末尾にはgs側で実行する関数名を記述する必要がありますが、今回はgs側の関数が複数あり、それら全てのために別々のgoogle.script.runを記述していては、いかにも冗長です。
そこで、スクリプトを文字列で書いて実行するevalを用いて、関数名を渡せばgs側でその関数が実行できるようにしています
（evalの使用はセキュリティ上の危険を伴いますが、googleの強固さを信じます）

③そして、指定のスプレッドシートのデータを取得し（ここではスプレッドシートの1行目をヘッドとしてオブジェクト形式に変換しています）返します。
gs側からhtml側にデータを渡す時はJSONにしましょう。（嵌った経験あり）

④上記一連の作業は並行処理で行います。
全てが終わってから、次のアクションに移るにはPromise.allを用います。
promise.allの中身は配列ですので、mapを使えばその配列全てが作成された後にthenに移るといった行動が可能になります
（ここではthen以降を省略しています）


# サイトを開いた時にログインっぽくする

先ほどのデータ取得は非同期で並行して進めているとはいえ、取得に時間がかかるデータがある時は少し時間がかかります。
そこで、ユーザー一覧の情報だけは別個で取得し、ユーザーが従業員番号を入力している間に他のデータを取得するようにしました。
ここでは、サイドバーを開いたらモーダルウィンドウが開き、そこにユーザーが従業員番号を入力したら、誰々がログインしているという状態を保持する機能を実装します。

``````javascript
var vm=new Vue({
  data: {
    staffs:[],
    staff:"",
  },
  created() {
    var self=this;
    this.fetchData("staffs").then(function(){
        var staff_code = prompt("従業員番号を入力してください", "");
        self.staff=self.staffs.find(staff=>staff["従業員番号"]==staff_code)
    })
  （"fetchDataの内容は先ほどと同じのため省略"）
``````

# 列の表示、非表示を切り替える

先ほどのログインで、vueのstaffデータには誰々がログインしているという状態が保持されます。
（ちなみにstaffは{code:1234,name:"アンリー",division:"第一購買部"}といった情報が入っています）

スプレッドシートで、この人が入力した行だけを表示するような機能を実装します。

|コード|名前    |部署     |
|------|--------|---------|
|1111  |スピッツ|第一購買部|
|2222  |YOASOBI |第二購買部|
|1234  |アンリー|第一購買部|


html側（コンポーネントで作成しています）

``````javascript
<script type="text/x-template" id="simple-filter">
  <form name="simpleFilter" class="box">
    <h2>簡単絞り込み</h2>
    <button type="button" @click="hideExcept(`コード`,staff.group)">担当</button>
    <button type="button" @click="hideExcept(`部署`,staff.code)">自部署</button>
    <button type="button" @click="showAllRows()">全案件表示</button>
  </form>
</script>

<script>
Vue.component('simple-filter', {
  template: '#simple-filter',
  props:{
    staff:"",
  },
  methods:{
    hideExcept(column,key){
      google.script.run.hideRows(column,key)
    },
    showAllRows(){
      google.script.run.showAllRows()
    }
  }
});
</script>
```````

gs側

``````JavaScript
//列の非表示。条件を満たす値以外を非表示にする
function hideRows(column,key){
  var row_values=loc(column)
  var row_indexes=row_values.reduce((acc, cur, idx) => cur!==key  ? [...acc,idx+3]: acc, [])
  row_indexes.forEach(e=>sheet.hideRows(e))
}
function showAllRows(){
  sheet.showRows(1,lastRow+3)
}

//指定したキーの列データを配列で返す
function loc(column){
  const index=head.findIndex(key=>key==column) //headはスプレッドシートの1行目（グローバル変数として定義）
  const rows=values.map(row=>row[index-1])
  return rows
}
``````

## 解説
行の表示、非表示を切り替えるメソッドは、sheetクラスのhideRow（hideRows）、showRow（showRows）です。
hideRows()の引数には隠す行を番号で指定します。