スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その４[完結編]～～

バグってたので修正しました。ごめんなさい。(2020/05/22 10:00頃
コード.gs内のgetNewDataListです。
□データ件数が少ないandデータがない場合にエラーとなる
・データの取得の仕方を勘違い修正
・初期表示数設定を追加
・初期表示件数よりデータが少ない場合に抜ける処理を追加
・データが1件もない場合に抜ける処理を追加


###本稿でいよいよ完結します。
（はやくコロナも終わってくれないかなぁ）

###今までのはこちら↓
[スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その１～～](https://qiita.com/yamamow/items/6be4fa99cd3cb45ecffa)
[スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その２～～](https://qiita.com/yamamow/items/38ad08c3cf85ec9df0e7)
[スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その３～～](https://qiita.com/yamamow/items/c7c2c50cd26eed48988a)

###ハマったところもありました。。。

[スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところinputの再描画編~~](https://qiita.com/yamamow/items/ad967a1dbfda6f9e4aa8)

[スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところcheckboxの再描画編~~](https://qiita.com/yamamow/items/d5c353e6d52861007278)

ソースを公開する前に、データシートとマスターシートの説明をしておこうかなと思います。（忘れてましたすみません）

##現状のイケてないところ
1. モーダルで詳細画面を開いたときに前回の内容が現れ、１パクおいてから表示される。
　⇒データが切り替わったのを確認できるので、今はこのまま。描画判定するプロパティの変更位置を後にすれば解決するかも・・・

2. モーダルの詳細画面で更新を掛けた後、一覧をリフレッシュしていない
　⇒ちょっと試したんですが、実現できず・・・

##データシートの構成（シート名：database）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207568/645cf98e-6328-1526-fa79-dabebe194d20.png)

|列|A列|B列|C列|D列|E列|F列|G列|H列|I列|J列|
|:-----:|:-----:|:-----------:|:----:|:-----:|:-:|:-:|:----:|:------:|:-:|:-:|
|項目の見出し|リンク行|タイムスタンプ|店舗名|郵便番号|県|住所|電話番号|営業時間|設備|感想|
|タグのid,name|rownum|timestamp|tempo|postnum|ken|address|telnum|activetime|setsubi|note
|表示形式|read|read|text|text|select|textarea|text|radio|checkbox|textarea
|設定値|    |    |30  |8   |2     |5,30    |15  |3    |4       |3,30|
|データ|5|2020/05/15 16:09:34|吉野家 有楽町２号店|100-0006|東京都|千代田区有楽町2丁目|100-0006123|９－２１時かも|駐車場,ドライブスルーかも,駐輪場|とってもいい|      
|データ|6|2020/05/15 14:24:24|吉野家 永田町１号店|100-0014|東京都|千代田区永田町1丁目|100-0014123|９－１８時かも|ドライブスルーかも,駐輪場|うまーい

例えば、「住所」の場合、「address」という文言でhtml内のid,nameに設定します。
そして、表示形式は「textarea」となり、「設定値」が「5,30」なので、５行３０文字の大きさで表示します。

「県」の場合は、「ken」という文言でhtml内のid,nameに設定し
表示形式は「select」で設定値が「2」なので、マスターシート（シート名：itemmaster）の２列目を参照という意味になります。

##マスターシートの構成（シート名：itemmaster）
A列はサポートしている表示形式になります。（固定）
B列以降はカスタマイズできます。
１行目は、何のデータなのかわかりやすい文言を入れてください。表示やシステムには関係ありません。
そして、２行目から下がその項目のデータになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207568/99278758-b7b0-10c4-7449-791e83d1a98f.png)
例えば、「県」や「営業時間」、「設備」は以下のように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207568/3b7ee0ea-ecc9-8e01-db26-fb00cd08f2b2.png)

#ソース公開
お恥ずかしいソースですが公開します。

###スプレッドシート（シート名：database）
とりあえずcsvで。データは２０件。

```csv
リンク行,タイムスタンプ,店舗名,郵便番号,県,住所,電話番号,営業時間,設備,感想,来店回数,好きなメニュー
rownum,timestamp,tempo,postnum,ken,address,telnum,activetime,setsubi,note,repeat,menu
read,read,text,text,select,textarea,text,radio,checkbox,textarea,radio,checkbox
,,30,8,2,"5,30",15,3,4,"3,30",5,6
5,2020/05/15 16:09:34,吉野家 有楽町２号店,100-0006,東京都,千代田区有楽町2丁目,100-0006123,９－２１時かも,"駐車場,ドライブスルーかも,駐輪場",とってもいい      ,,
6,2020/05/15 14:24:24,吉野家 永田町１号店,100-0014,東京都,千代田区永田町1丁目,100-0014123,９－１８時かも,"ドライブスルーかも,駐輪場",うまーい,,
7,2020/05/15 13:52:57,吉野家 永田町１号店,100-0014,東京都,千代田区永田町1丁目,,９－２１時かも,ドライブスルーかも,うまーい,,
8,2020/05/15 17:16:39,吉野家 水道橋１号店,101-0061,東京都,千代田区神田三崎町2丁目,1234566,９－１８時かも,"駐車場,駐輪場",とってもいい      ,,
9,2020/05/15 17:38:07,吉野家 小川町１号店A,101-0052,東京都,千代田区神田小川町2丁目,101-0052,24時間かも,駐車場,うまい,,
10,2020/02/12 14:57:55,吉野家 秋葉原１号店,101-0023,東京都,千代田区神田松永町,,９－２１時かも,"駐車場,駐輪場",うまーい,,
11,2020/02/13 14:57:55,吉野家 神保町１号店,101-0051,東京都,千代田区神田神保町2丁目,,９－２１時かも,"駐車場,駐輪場",,,
12,2020/05/18 9:37:11,吉野家 淡路町１号店,101-0041,東京都,千代田区神田須田町1丁目,101-00411111,９－２１時かも,"駐車場,駐輪場",うまいよ～,１５回まで,スタミナ超特盛丼
13,2020/02/15 14:57:55,吉野家 お茶の水１号店,101-0062,東京都,千代田区神田駿河台2丁目,,９－２１時かも,"駐車場,駐輪場",,,
14,2020/02/16 14:57:55,吉野家 神田１号店,101-0044,東京都,千代田区鍛冶町2丁目,,９－２１時かも,"駐車場,駐輪場",,,
15,2020/02/17 14:57:55,吉野家 霞ヶ関１号店,100-8918,東京都,千代田区霞が関,,９－２１時かも,ドライブスルーかも,,,
16,2020/02/18 14:57:55,吉野家 霞が関２号店,100-0013,東京都,千代田区霞が関,,９－２１時かも,ドライブスルーかも,,,
17,2020/02/19 14:57:55,吉野家 帯広１号店,080-0011,北海道,帯広市西1条南,,９－２１時かも,ドライブスルーかも,,,
18,2020/02/20 14:57:55,吉野家 旭川１号店,078-8231,北海道,旭川市豊岡1条,,９－２１時かも,,,,
19,2020/02/21 14:57:55,吉野家 旭川２号店,070-0034,北海道,旭川市4条通2丁目,,９－２１時かも,,,,
20,2020/02/22 14:57:55,吉野家 岩見沢１号店,068-0825,北海道,岩見沢市日の出町,,９－２１時かも,,,,
21,2020/02/23 14:57:55,吉野家 新千歳１号店,006-0012,北海道,千歳市美々 新千歳空港,,９－２１時かも,,,,
22,2020/02/24 14:57:55,吉野家 千歳２号店,066-0036,北海道,千歳市北栄2丁目,,９－２１時かも,,,,
23,2020/02/25 14:57:55,吉野家 1厚別東１号店,004-0004,北海道,札幌市厚別区厚別東4条3丁目,,９－２１時かも,,,,
24,2020/02/26 14:57:55,吉野家 苫小牧新開町店,053-0052,北海道,苫小牧市新開町2丁目,,９－２１時かも,,,,
```
###同じスプレッドシート内の別シート（シート名：itemmaster）
とりあえずcsvで。

```csv
disptype,県,営業時間,設備,リピート数,メニュー
read,北海道,24時間かも,駐車場,初回,牛丼
text,青森県,９－２１時かも,駐輪場,５回まで,スタミナ超特盛丼
textarea,岩手県,９－１８時かも,ドライブスルーかも,１０回まで,肉だく牛丼
select,宮城県,PMのみかも,,１５回まで,ねぎだく牛丼
radio,秋田県,,,,ねぎたま牛丼
checkbox,山形県,,,,ライザップ牛サラダエビアボカド
,福島県,,,,
,茨城県,,,,
,栃木県,,,,
,群馬県,,,,
,埼玉県,,,,
,千葉県,,,,
,東京都,,,,
,神奈川県,,,,
,新潟県,,,,
,富山県,,,,
,石川県,,,,
,福井県,,,,
,山梨県,,,,
,長野県,,,,
,岐阜県,,,,
,静岡県,,,,
,愛知県,,,,
,三重県,,,,
,滋賀県,,,,
,京都府,,,,
,大阪府,,,,
,兵庫県,,,,
,奈良県,,,,
,和歌山県,,,,
,鳥取県,,,,
,島根県,,,,
,岡山県,,,,
,広島県,,,,
,山口県,,,,
,徳島県,,,,
,香川県,,,,
,愛媛県,,,,
,高知県,,,,
,福岡県,,,,
,佐賀県,,,,
,長崎県,,,,
,熊本県,,,,
,大分県,,,,
,宮崎県,,,,
,鹿児島県,,,,
,沖縄県,,,,
```
###コード.gs
```javascript
// GASでお決まりなので必要
function doGet(e) {
  var template = HtmlService.createTemplateFromFile('vue_index');
  return template.evaluate();
}

//インクルードするためのもの
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

//マスターデータを取得
function getMasterData(col) {
  var spread = SpreadsheetApp.getActiveSpreadsheet() ;
  var sheet = spread.getSheetByName('itemmaster');
  var idname = sheet.getRange(1, col, 1, 1).getValues();
  var values = sheet.getRange(2, col, sheet.getRange(2, col).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()-1, 1).getValues();
  //連想配列にする
  var res = {};
  for (i=0; i < values.length; i++) {
      res[i]={ id:idname + i, item:values[i][0]};
  }
  return res;
}

//更新や新規登録を行う
//排他制御を掛けるので更新処理を一か所にまとめた
function dataSave(sheet, row1, col1, row2, col2, data){
  var msg = "";
  
  //ドキュメントロックを使用する
  var lock = LockService.getDocumentLock();
 
  //30秒間のロックを取得
  try {
    //ロックを実施する
    lock.waitLock(30000);
    
    //ここにメインルーチンを記述する
    sheet.getRange(row1, col1, row2, col2).setValues(data);
    
    //メッセージを格納
    msg = "保存完了";
    
  } catch (e) {
    //ロック取得できなかった時の処理等を記述する
    var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。";
    
    //通常のエラーとロックエラーを区別する
    if(e.message == checkword){
      //ロックエラーの場合
      msg = "更新処理中でした";
    }else{
      //ソレ以外のエラーの場合
      msg = e.message;
    }  　 
    
  }　finally　{
    //ロックを開放する
    lock.releaseLock();
    
    //メッセージを表示する
    //ui.alert(msg);
  }
}

//項目名称を取得する
//-1が指定された場合は、オブジェクトの設定値も返す
//0以外の整数が設定された場合、その数の設定項目名を返す
function getItemNameList(col){
  var res = [];
  var spread = SpreadsheetApp.getActiveSpreadsheet() ;
  var sheet = spread.getSheetByName('database');
  if (col == -1) {
    var values = sheet.getRange(1, 1, 4, sheet.getLastColumn()).getValues();
  } else {
    var values = sheet.getRange(1, 1, 1, col).getValues();
  }
  return values;
}

//タイムスタンプの新しい方から取得する。dispRowにて件数調整可能
//[行番号,日付]の二次元配列で時間で降順
function getNewDataList(){
    var dataStartRow = 5;
    var dispRow = 15;       // 初期表示で何件表示するか　追加
    var res = [];
    var spread = SpreadsheetApp.getActiveSpreadsheet() ;
    var sheet = spread.getSheetByName('database');
    
    // データがないときは返す　追加
    if (sheet.getLastRow()-dataStartRow+1 < 1) {return true;}
    
    //2行目にシステム的な項目、3,4行目にHTMLのタグのタイプ、設定値を入れたので5行目からの取得とする　修正　３つ目のパラメータ
    var values = sheet.getRange(dataStartRow, 1, sheet.getLastRow()-dataStartRow+1, 2).getValues();
    
    //ソート　sorting_asc　sorting_desc
    console.info(values);
    values.sort(sorting_desc);
    for (var i = 0; i < dispRow + 1; i++){
        res.push(values[i]);
        // データがdispRowよりも少ない場合は抜ける　追加
        if (i == values.length - 1) {break;}
    }
    return res;
}

//スプレッドシート内を文言で検索し行番号を返す（同じ行内に複数出てくると抽出結果も重複する
function rowSearch(str){
  var res = [];
  var spread = SpreadsheetApp.getActiveSpreadsheet() ;
  var sheet = spread.getSheetByName('database');
  var textFinder = sheet.createTextFinder(str);
  var ranges = textFinder.findAll();
  for(var i = 0; i < ranges.length; i++){
    var range = sheet.getRange(ranges[i].getA1Notation());
    res.push(range.getRow());
  }
  var res2 = uniqueArray(res);
  return res2;
}

//項目名を付けてデータを１行返す。
function getItemNameAndData (itemCount,dataNo,dataCol) {
  var res = [];
  res = getItemNameList(itemCount);
  
  if (dataNo > 0) {
    res.push(getCellValue(dataNo,dataCol));
    
    // checkボックスは２つ以上選択項目があるものとする
    // checkボックスのデータは配列に置き換える（テキストをカンマ区切りで配列に置き換える
    for (var i = 0; i < res[2].length; i++) {
        if (res[2][i] == "checkbox"){
          if (res[4][i] == '' || res[4][i] == null) {
            res[4][i] = [];    // データがなかったらカラの配列
          } else {
            if ( String(res[4][i]).indexOf(',') != -1) {
              res[4][i] = res[4][i].split(',');  // データがあってカンマ区切りなら配列にする
            } else {
              res[4][i] = [res[4][i]];           // データが１つしかない場合はそのまま配列にする
            }
          }
        }
    }
  } else {
    // 新規作成用
    var tmp2 = Array(res[0].length);
    
    //　select radio のデータはカラにする
    // checkbox のデータはカラの配列にする
    for (var i = 0; i < res[2].length; i++) {
        if (res[2][i] == "select" || res[2][i] == "radio" ){
            tmp2[i] = '';
        } else if (res[2][i] == "checkbox") {
            tmp2[i] = [];
        } else {
            tmp2[i] = "未設定";
        }
    }
    res.push(tmp2);
  }
  
  //行列を入れ替える
  res = arrayTranspose(res);
  return res;
}

//行番号からセル値を取得
function getCellValue(row, col){
  // 現在アクティブなスプレッドシートを取得
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('database');
  // そのシートにある (1, 1) のセルから3行目までのセル範囲を取得
  if (col == -1 ) {
    var range = sheet.getRange(row, 1, 1, sheet.getLastColumn());
  } else {
    var range = sheet.getRange(row, 1, 1, col);
  }
  // そのセル範囲の値を取得
  var values = range.getValues();
  values[0][0] = row;
  values[0][1] = Utilities.formatDate( values[0][1], 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
  return values[0];
}

//一次配列から重複を排除する
function uniqueArray(ary){
  var res = [];
  res = ary.filter(function(value, index, self){ 
                       return self.indexOf(value) === index; });
  return res;
}

//ソート昇順
function sorting_asc(a, b){
  if(a[1] < b[1]){
    return -1;
  }else if(a[1] > b[1] ){
    return 1;
  }else{
   return 0;
  }
}

//ソート降順
function sorting_desc(a, b){
  if(a[1] > b[1]){
    return -1;
  }else if(a[1] < b[1] ){
    return 1;
  }else{
   return 0;
  }
}

// 配列の行列入れ替え
// 参考：https://qiita.com/kznr_luk/items/790f1b154d1b6d4de398
// const transpose = a => a[0].map((_, c) => a.map(r => r[c]));
function arrayTranspose(a) {
  return a[0].map((_, c) => a.map(r => r[c]));
}

// splitの結果を返す
function getSplit(array, _order) {
  var tmp = array.split(",");
  if (_order == -1) {
    return tmp;
  } else {
    return tmp[_order];
  }
}

//itemmasterシートのB列以降のデータを配列に格納する
function getItemMasterData() {
  var spread = SpreadsheetApp.getActiveSpreadsheet() ;
  var sheet = spread.getSheetByName('itemmaster');
  var res = [];
  for (var i = 1; i < sheet.getLastColumn()+1; i++) {
    var tmp = getMasterData(i);
    res.push(tmp);
  }
  return res;
}

//一覧取得（検索にも対応）
function getSheetData(_str) {
  var spread = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spread.getSheetByName('database');
  
  if (_str == '' || _str == null) {
    // 検索なしの一覧取得
    var tmp1 = getNewDataList();
    var res = [];
    for(var i = 0; i < tmp1.length; i++){
      res.push(getCellValue(tmp1[i][0], 6));
    }
  } else {
    // 検索時
    var tmp1 = rowSearch(_str);
    var res = [];
    for(var i = 0; i < tmp1.length; i++){
      res.push(getCellValue(tmp1[i], 6));
    }
  }
  for (var i = 0; i < res.length; i++) {
    for (var j = 0; j < res[0].length; j++) {
      if (Object.prototype.toString.call(res[i][j]) == '[Object Date]') {
        res[i][j] = formateDate(res[i][j]);
      }
    }
  }
  return res;
}

//データ更新(新規も対応）
function setUpdData(ary) {
  var spread = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spread.getSheetByName('database');
  var arrayUpd = [ary.length];
  var typeIdx = 2;                // inputなどのタイプの要素番号
  var dataIdx = 4;                // データの要素番号
  
  if (ary[0][dataIdx] == "未設定") {
    // 新規用
    arrayUpd[0] = sheet.getLastRow()+1;
  } else {
    arrayUpd[0] = ary[0][dataIdx];  // 行番号を格納
  }
  arrayUpd[1] = getDateOfTokyo(); // タイムスタンプをセット
  
  // 更新データだけの配列を作成しながら、checkboxのデータは配列から文字列に変換する
  for (i=2; i < ary.length; i++) {
    if (ary[i][typeIdx] == "checkbox") {
      arrayUpd[i] = ary[i][dataIdx].join(',');
    } else {
        arrayUpd[i] = ary[i][dataIdx];
    }
  }
  var tmp = [];
  tmp.push(arrayUpd);
  dataSave(sheet,arrayUpd[0], 1, 1, arrayUpd.length,tmp);
  return true;
}

// 今日の日付
var getDateOfTokyo = function() {
  var date = new Date();
  return Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
}

//日付フォーマット
var formateDate = function(d) {
  var addZero = function(n) {
    return (n < 10)? "0" + String(n):String(n);
  }
  var year = String(d.getFullYear());
  var month = addZero(d.getMonth() + 1);
  var date = addZero(d.getDate());
  var hour = addZero(d.getHours());
  var minite = addZero(d.getMinutes());
  var second = addZero(d.getSeconds());
  return year + "/" + month + "/" + date + " " + hour + ":" + minite + ":" + second;
};

```
###vue_js.html
ファイル名↑が間違がっていましたので修正しました。すみません。（2020/05/21）

```html
<script>
var vue_example = new Vue({
    el: '#vue_example',

    data:{
        listtitles:[['初', '期', '表', '示', '中', 'です']],
        listitems: [['初', '期', '表', '示', '中', 'です']],
        dispDetails: false,
        showContent: false,
        searchword: '',
        refitem:[],
        initOptions:[['初期値']],             //詳細画面のデータ
    },

    methods:{
        // データプロパティ
        initData: function(ary){ 
                        this.listitems = ary; 
                  },
        // 詳細データの更新
        refData:  function(ary){ 
                        this.refitem = [];
                        this.refitem = ary;
                        this.refitem.push('dummy');
                        this.refitem.pop();
                        //データ更新
                        vue_example.$forceUpdate();
                        this.$nextTick();
                        Vue.nextTick();
                  },
        initTitles: function(ary){ 
                        this.listtitles = ary; 
                  },
        // 検索文字列からデータ検索しデータ表示
        search_word: function(){ 
                        //リスト取得
                        google.script.run.withSuccessHandler(this.initData)
                                         .withFailureHandler(function(arg){
                                             alert("データの初期取得に失敗しました。");
                                          }).getSheetData(this.searchword);
                        //リストの項目名称6個を取得    
                        google.script.run.withSuccessHandler(this.initTitles)
                                         .withFailureHandler(function(arg){
                                             alert("リスト項目名の初期取得に失敗しました。");
                                          }).getItemNameList(6);
                  },
        // 検索文字列のクリア
        search_word_clea: function(){ 
                          this.searchword = '';
                  },
        // モーダル表示のプロパティ変更
        openModal: function(){
                        this.dispDetails = true;
                        this.showContent = true; 
        },
        // モーダル表示のプロパティ変更
        closeModal: function(){
                        this.dispDetails = false;
                        this.showContent = false; 
        },
        // プロパティinitOptionsのデータ更新
        initOption: function(ary){ 
                        this.initOptions = [];
                        this.initOptions = ary;
                        this.initOptions.push('dummy');
                        this.initOptions.pop();
                        //データ更新
                        vue_example.$forceUpdate();
                        this.$nextTick();
                  },
        // モーダル画面を表示し、データ取得（上記メソッドの呼び出し、引数になる）
        ref_item: function(_idx){
                      this.dispDetails = true;
                      this.showContent = true;
        
                      google.script.run.withSuccessHandler(this.initOption)
                                       .withFailureHandler(function(arg){
                                           alert("リスト項目名の取得に失敗しました。");
                                        }).getItemMasterData();

                      google.script.run.withSuccessHandler(this.refData)
                                       .withFailureHandler(function(arg){
                                           alert("データの取得に失敗しました。");
                                       }).getItemNameAndData(-1, this.listitems[_idx][0], -1);
                  },
        // 更新画面（モーダル）でのデータ更新後、モーダルOFF
        upd_item: function(){
                      google.script.run.withSuccessHandler(this.refitem)
                                       .withFailureHandler(function(arg){
                                           alert("データの更新に失敗しました。");
                                       }).setUpdData(this.refitem);
                      // モーダル画面を閉じる
                      this.dispDetails = false;
                      this.showContent = false;                      
                  },
        // 追加画面表示
        add_disp: function(){
                      // モーダル表示ON
                      this.dispDetails = true;
                      this.showContent = true;
        
                      google.script.run.withSuccessHandler(this.initOption)
                                       .withFailureHandler(function(arg){
                                           alert("リスト項目名の取得に失敗しました。");
                                        }).getItemMasterData();

                      google.script.run.withSuccessHandler(this.refData)
                                       .withFailureHandler(function(arg){
                                           alert("データの取得に失敗しました。");
                                       }).getItemNameAndData(-1, 0, -1);
                  },
        // データ一覧取得
        list_items: function(){
                      google.script.run.withSuccessHandler(this.initData)
                                       .withFailureHandler(function(arg){
                                           alert("データの取得に失敗しました。");
                                       }).getSheetData();
                  },
    },
    // 初期表示
    created: function() {//リスト取得
                         google.script.run.withSuccessHandler(this.initData)
                                          .withFailureHandler(function(arg){
                                              alert("データの初期取得に失敗しました。");
                                          }).getSheetData();
                         //リストの項目名称6個を取得    
                         google.script.run.withSuccessHandler(this.initTitles)
                                          .withFailureHandler(function(arg){
                                              alert("リスト項目名の初期取得に失敗しました。");
                                          }).getItemNameList(6);
    } 
});
</script>
```
###vue_index.html
```html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <base target="_top">
    <?!= include('css'); ?>
    <script src="https://unpkg.com/vue"></script>
  </head>
<body>
Vueで作成したページです
  <div id="vue_example">
    <input type=text name=searchword v-model="searchword" />
    <button v-on:click="search_word()" class='btn-radius-blue'>　検索　</button>&nbsp;
    <button v-on:click="search_word_clea()" class='btn-radius-blue'>　検索クリア　</button>&nbsp;
    <button v-on:click="add_disp()" class='btn-radius-blue'>　新規登録　</button><br><br>
    <table>
      <tr v-for="(titles,idx) in listtitles">
        <th v-for="title in titles">
        {{ title }}
        </th>
        <th>ボタン</th>
      </tr>
      <tr v-for="(it,idx) in listitems">
        <td v-for="tmp in it">{{ tmp }}</td>
        <td><button v-on:click="ref_item( idx )" class='btn-radius-blue'>詳細表示</button></td>
      </tr>
    </table>
    
    <!-- ----以下はモーダル表示-------------------------------------------------------------------- -->
    
    <div id="modal" v-show="showContent" class="overlay">
    <div id="modal_content" v-if="dispDetails" class="content">
    <table class="modal_table">
      <tr v-for="(refits,edit_Number) in refitem">
        <th>{{ refits[0] }}</th>
        <!-- いったんすべてを表示したいならここをインする
             <th>{{ refits[0] }}:{{ refits[1] }}:{{ refits[2] }}:{{ refits[3] }}:</th>
             <td><input v-model="refits[4]" size=refits[3] maxlength=refits[3] /></td>
        -->
        <td v-if="refits[2] == 'read'">{{ refits[4] }}</td>
        <td v-if="refits[2] == 'text'"><input :id="refits[1]" :name="refits[1]" type=text v-model="refits[4]" size=”refits[3]” maxlength=”refits[3]” /></td>
        <td v-if="refits[2] == 'textarea'">
            <textarea :id="refits[1]" v-model="refits[4]" rows=getSplit(refits[5],0) cols=getSplit(refits[5],1) placeholder="入力して下さい">
            </textarea>
        </td>
        <td v-if="refits[2] == 'select'">
            <select :id="refits[1]" :name="refits[1]" v-model="refits[4]" >
                <option v-for="(items,idx) in initOptions[refits[3]-1]" v-bind:value="items.item">{{ items.item }}</option>
            </select>
        </td>
        
        <td v-if="refits[2] == 'radio'">
            <div v-for="(items,idx) in initOptions[refits[3]-1]" >
                <input :id="refits[1]" 
                       :name="refits[1]"                                        
                       :key="items.id" 
                       type=radio 
                       v-model="refits[4]"
                       v-bind:value="items.item"
                       :checked="refits[4] == items.item"
                       />{{ items.item }}<br>
                       <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する -->
            </div>
        </td>
        
        <td v-if="refits[2] == 'checkbox'">
          <div v-for="(items,idx) in initOptions[refits[3]-1]">
                <input :id="refits[1]" 
                       :name="refits[1]" 
                       :key="items.id" 
                       type=checkbox
                       v-model="refits[4]"
                       v-bind:value="items.item"
                       :checked="refits[4].indexOf(items.item) > -1" />{{ items.item }}<br>
                       <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する -->
          </div>
        </td>
      </tr> <!-- trのfor文終了 -->
      <tr>
        <td></td>
        <td><button v-on:click="upd_item" class='btn-radius-orange'>　更新　</button>&nbsp;&nbsp;&nbsp;
            <button v-on:click="closeModal" class='btn-radius-blue'>閉じる</button></td>
      </tr>
    </table>
    
    </div>
    </div>
    <!-- モーダル表示終了 ----------------------------------------------------->
  </div>

<?!= include('vue_js'); ?>
</body>
</html>
```
###css.html
```html
<style>
body{
    font-family:Verdana,Arial;
    font-size:14px;
    text-align: center; 
}

div{
    text-align:center;
    margin-left:auto;
    margin-right:auto;
    text-align:left;
    width: 90%;
}

h2{
    font-size:14px;
    border-left:5px solid #ccc;
    padding:3px 0 3px 10px;
    margin-bottom:10px;
}

h3{
    border-bottom:1px solid #ccc;
    padding:3px 0;
    margin-bottom:10px;
}

table{
  width: 100%;
  <!-- border-collspase: collapse;     -->
  border-collapse:separate;
  border-spacing: 0;
}

.modal_table{
  width: 80%;
  <!-- border-collspase: collapse;     -->
  border-collapse:separate;
  border-spacing: 0;
}

table th{
  text-align: center;
  color:white;
  background: linear-gradient(#829ebc,#225588);
  border-left: 1px solid #3c6690;
  border-top: 1px solid #3c6690;
  border-bottom: 1px solid #3c6690;
  box-shadow: 0px 1px 1px rgba(255,255,255,0.3) inset;
  padding: 10px 10px;
}

table td{
  text-align: center;
  border-left: 1px solid #a8b7c5;
  border-bottom: 1px solid #a8b7c5;
  border-top:none;
  padding: 5px 10px;
}

table tr:nth-child(odd){
  background-color: #eee
}

table td:last-child{
  border-right: 1px solid #a8b7c5;
}

.btn-radius-orange {
  display: inline-block;
  padding: 7px 20px;
  border-radius: 10px;
  text-decoration: none;
  color: #FFF;
  background-image: linear-gradient(45deg, #FFC107 0%, #ff8b5f 100%);
  transition: .4s;
}

.btn-radius-orange:hover {
  background-image: linear-gradient(45deg, #FFC107 0%, #f76a35 100%);
}

.btn-radius-blue {
  display: inline-block;
  padding: 7px 20px;
  border-radius: 10px;
  text-decoration: none;
  color: #FFF;
  background-image: linear-gradient(45deg, #1F436E 0%, #1C6ECD 100%);
  transition: .4s;
}

.btn-radius-blue:hover {
  background-image: linear-gradient(45deg, #1F436E 0%, #0000BB 100%);
}

input[type="text"] ,input[type="email"] {
  width: 30em; 
}

select { 
  //width:250px; 
}

textarea {
  width: 30em;
  height: 100px;
}

/* #overlay{ */
.overlay{
  /*　要素を重ねた時の順番　*/
  z-index:1;

  /*　画面全体を覆う設定　*/
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);

  /*　画面の中央に要素を表示させる設定　*/
  display: flex;
  align-items: center;
  justify-content: center;

}

/* #content{ */
.content{
  z-index:2;
  overflow-y: scroll;
  width:80%;
  height:80%;
  padding: 1em;
  background:#fff;
  /*　画面の中央に要素を表示させる設定　*/
  display: flex;
  justify-content: center;
}

</style>
```

#感想
ここまで読んで下さりありがとうございました。

表示に使用するHTMLは１ファイルで１００行未満。更新も追加もこれだけ。
Vueファイルも冗長っぽい感じがあるものの１３５行。
コード.gsは、doPostがなくなったものの、必要な関数を追加し変わらずの３２０行。

変にゴリゴリ書く感じもそんなにないし、これだけのコードで済むのだからやはりスゴイのかなVue。

なんだかんだで当初目標にしていた、**「表示項目を増やしてもコードの編集が無いように」**を達成できたので大変満足です。
項目を増やすたびにコードいじってたら手離れよくないですもんねぇ。

ほんと、誰かの助けにでもなれば幸いです。
ありがとうございました。m(\_'\_)m