５．入力した内容をスプレッドシートに保存できるようにする

# 今日の目標：入力した内容をスプレッドシートに反映できる

## 0．前回の振り返り
https://qiita.com/LoAMiat/items/b7414692e214ac2a64ac

画面遷移できるようになりました！

https://qiita.com/LoAMiat/items/30a9becddd2a97c98904

ちょっとした障害にドはまりしてました。

## 1．前回の課題「初期表示が遅い」への試行錯誤
各画面の初期表示が本当に重いのですが、スプレッドシートの検索は１回しかしていないので、検索回数を減らす以外の方法で高速化を目指します。
とりあえず、検索結果をキャッシュすれば早くなるのでは・・・？と思ったのでキャッシュしてみます。
``` javascript:List.gs
// キャッシュ有効化
const cache = makeCache();

function initList(e) {

  let htmlOutput;
  let cacheVal = cache.get('List');
  // キャッシュに値が残っているか確認
  if( !cacheVal && cacheVal !== null){
    htmlOutput = cacheVal;
  } else {
    // スプレッドシート検索
    const query = '=QUERY(🐈中略🐈)';
    const cats = getAllRecords(query);

    // 表示画面、変数定義
    const template = HtmlService.createTemplateFromFile('CatList');
    template.deployURL = ScriptApp.getService().getUrl();
    template.formHTML = setListHTML(e, cats, 'List');
    template.rows = rowNumCat;
    // html生成
    htmlOutput = template.evaluate();
    htmlOutput.setTitle('保護猫一覧');
    htmlOutput.setFaviconUrl(🐈中略🐈);
    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    // 検索結果をキャッシュに残す
    cache.put( 'List', htmlOutput );
  }
  return htmlOutput;
}

/**
 * キャッシュの保存・取得をする関数
 * 参考元：https://qiita.com/Terasan_Koshigaya/items/1144bc2f833f433416e0
 */
function makeCache() {
  const cache = CacheService.getScriptCache();
  return {
    get: function(key) {
      return JSON.parse(cache.get(key));
    },
    put: function(key, value) {
      //デフォルトでは10分間（600秒）保存される。最大値は6時間（21600秒）
      cache.put(key, JSON.stringify(value), 21600);
      return value;
    }
  };
}
```

スプレッドシートの検索結果のみをキャッシュするパターンと、検索結果をもって生成したhtmlをキャッシュするパターンで試したところ、htmlをキャッシュするほうが早さを実感できましたので、この形で行こうと思います。

## 2．選択リストをスプレッドシートの値から生成
入力画面で利用する一部選択肢の中身はスプレッドシートにあるので、まずはこれまでと同様に対象一覧を取得します。

ラベルは名前、ValueはIDになるようにしておきます。
``` javascript:CatDetailsEdit.gs
/**
 * 預かりさんの選択リスト一覧を作成する
 */
function setSelectOptions(e, template) {
  // 預かりさん一覧を取得
  const query = '=QUERY(' + CatHouseLines + ',"select * where A IS NOT NULL AND H = TRUE order by G, A", 1)';
  const person = getAllRecords(query);

  let html = ``;
  for(const man of person) {
    if( man['預かりさんID'] == template.personId ){
      html += `<option value="` + man['預かりさんID'] + `" selected>` + man['通称'] + `(` + man['預かり頭数'] + `)</option>`;
    } else {
      html += `<option value="` + man['預かりさんID'] + `">` + man['通称'] + `(` + man['預かり頭数'] + `)</option>`;
    }
  }
  template.selectOptions = html;

  return template;
}
```
``` edit.html
<select class="detailvalueInput detailvalueSelect" value="<?= catHouse ?>">
  <? output._ = selectOptions ?>
</select>
```

## 3．保存したらスプレッドシート更新処理が起動するように修正
ひとまずボタンをクリックしたら、.gs内のメソッドが起動するようにします。
保存後もアプリにとどまるので、素直にPOSTメソッドを利用します。
``` edit.html
  <form method="post" action="<?= deployURL ?>">
    <input type="hidden" name="type" value="edit"/>
    <input type="hidden" name="catId" value="<?= catId ?>"/>
    <h2 class="text-center m-4">保護猫詳細
      <button type="submit" class="catBtn btn btn-outline-primary">保存</button>
    </h2>
```
``` javascript:CatDetailsEdit.gs
function doPost(e) {

  // パラメータ取得
  const type = e.parameter.type;
  const catId = e.parameter.catId;
  const personId = e.parameter.personId;

  let htmlOutput;

  // 猫追加
  if( type === 'New' ){
    htmlOutput = doNew(e);
  }
  // 猫編集
  else if( type === 'Edit' ){
    htmlOutput = doEdit(e);
  }
  // 猫体重追加 OR 猫写真追加
  else if( type === 'Add-Weight' ||  type === 'Add-Photo' ){
    htmlOutput = doAdd(e, type);
  }

  return htmlOutput;
}
```

## 4．入力された内容でスプレッドシートの値を更新する
画面表示している内容と同じ行を更新していきます。
htmlの情値は、name要素をラベルとしてパラメータから取得するため、各種input,selectにはすべてname属性を付与していきます。
``` CatDetailsEdit.html
<div class="detail">
  <div class="detailLine">
    <span class="detailLabel">なまえ</span>
    <input type="text" name="catName" class="detailvalueInput" value="<?= catName ?>"/>
  </div>
</div>
<div class="detail">
  <div class="detailLine">
    <span class="detailLabel">性別</span>
    <span class="detailvalueRadio">
      <label><input type="radio" name="sexInput" value="♂" <?= catSexMale ?> > ♂ </label>
      <label><input type="radio" name="sexInput" value="♀" <?= catSexFemale ?> > ♀ </label>
    </span>
  </div>
</div>
<div class="detail">
  <div class="detailLine">
    <span class="detailLabel">預かりさん氏名</span>
    <select class="detailvalueInput detailvalueSelect" name="catHouse">
      <? output._ = selectOptions ?>
    </select>
  </div>
</div>
<div class="detail">
  <div class="detailLine">
    <span class="detailLabel">トライアル開始日</span>
    <input type="date" name="catTrialDate" class="detailvalueInput" value="<?= catTrialDate ?>"/>
  </div>
</div>
```

スプレッドシートにアクセスする回数が多ければ多いほど処理が遅くなるので、できるだけアクセス回数を減らすようにしています。
`sheet.getRange(rangeArea).setValues([editCat]);`を使って、指定範囲のセルを値で完全に上書きしていきます。
``` javascript:CatDetailsEdit.gs
/**
 * 入力された値を更新する
 */
function doEdit(e, catId) {

  // キャッシュのキーを生成
  EditCat = 'Edit' + catId;
  EditTemplateCat = 'EditTemplate' + catId;
  const DetailCat = 'Details' + catId;
  
  // スプレッドシート準備
  const ss = SpreadsheetApp.openByUrl(catMasterURL);
  const sheet = ss.getSheetByName('猫マスタ');
  // 猫IDは行番号と相対値になっているので、行を算出
  const rowNumber = parseInt(catId.replace('C','')) + 1;
  // CatMasterLines = "'猫マスタ'!A:AA";をもとに更新範囲を算出
  const rangeArea = "'猫マスタ'!A" + rowNumber + ":AA" + rowNumber;

  // １行分の猫情報を作成し、更新する
  const editCat = setCatAllLines(e, rowNumber, catId, e.parameter.catURL);
  sheet.getRange(rangeArea).setValues([editCat]);

  // 変更前の値を取得
  beforeVal =  cache.get(EditTemplateCat);

  // 更新された値に応じてcacheクリア
  const keys = ['List', DetailCat, EditCat, EditTemplateCat];
  relatedCacheClear(keys);

  e.parameter.type = 'detail';
  e.parameter.catId = catId;
  const htmlOutput = initDetail(e, catId);
  return htmlOutput;
}

/**
 * 猫マスタ情報1行分全部設定
 */
function setCatAllLines(e, rowNumber, catId, url){
  cat = [
    catId,
    e.parameter.catName,
    e.parameter.sexInput,
    e.parameter.catBirthday,
    '=DATEDIF(D'+rowNumber+',TODAY(),"Y")&"歳"&DATEDIF(D'+rowNumber+',TODAY(),"YM")&"か月"&DATEDIF(D'+rowNumber+',TODAY(),"MD")&"日"',
    e.parameter.catProtectionDate,
    '=DATEDIF(F'+rowNumber+',TODAY(),"Y")&"年"&DATEDIF(F'+rowNumber+',TODAY(),"YM")&"か月"&DATEDIF(F'+rowNumber+',TODAY(),"MD")&"日"',
    e.parameter.catProtectionArea,
    e.parameter.catTrialDate,
    e.parameter.catTransferDate,
    e.parameter.catHouse,
    '=VLOOKUP(K'+rowNumber+",'預かりさん'!A:C,3,FALSE)",
    e.parameter.catVaccine1,
    e.parameter.catVaccine1Done,
    e.parameter.catVaccine2,
    e.parameter.catVaccine2Done,
    e.parameter.catSurgery,
    e.parameter.catSurgeryDone,
    e.parameter.catVirusCheckDate,
    e.parameter.catVirusCheckResultsInput,
    url,
    e.parameter.weight,
    e.parameter.trial,
    e.parameter.note,
    e.parameter.profile,
    e.parameter.catColor,
    '=VLOOKUP(Z'+rowNumber+",'猫柄リンク集'!A:B,2,FALSE)"
  ];
  return cat;
}
```
更新対象の1行の中には、数式で計算しておきたい列もあるので、数式を含めています。

## 5．入力された内容でスプレッドシートに行を追加
一部情報は更新履歴として別のスプレッドシートに行を追加していきます。
（最終的に体重のグラフを表示したいので、その準備🐈）
`sheet.appendRow(catLog);`を利用して、スプレッドシート内の最終行に新規追加していきます。
``` javascript:CatDetailsEdit.gs
/**
 * 体重、備考を追加する
 * 体重：weight：D列に保存
 * 備考：note：E列に保存
 */
function doAdd(e, type, catId) {

  EditCat = 'Edit' + catId;
  EditTemplateCat = 'EditTemplate' + catId;
  const DetailCat = 'Details' + catId;

  const ss = SpreadsheetApp.openByUrl(catLogURL);
  let sheet = ss.getSheetByName('猫備考系履歴');
  const now = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm"); // 24h表記
  let catLog = [];
  // 体重
  if( type === 'Add-Weight' ){
    sheet = ss.getSheetByName('猫体重履歴');
    catLog = [catId, e.parameter.catName, now, e.parameter.weight];
  }
  // 備考
  else if( type === 'Add-Note' ){
    catLog = [catId, e.parameter.catName, now, '', e.parameter.note, ''];
  }
  // 一番最後の行に値を追加する
  sheet.appendRow(catLog);

  // 更新された値に応じてcacheクリア
  const keys = ['List', DetailCat, EditCat, EditTemplateCat];
  relatedCacheClear(keys);

  e.parameter.type = 'detail';
  e.parameter.catId = catId;
  const htmlOutput = initDetail(e, catId);
  return htmlOutput;
}
```

## 6. 保存時のローディングアニメーションを用意する
``` edit.html
(前略)
    <script>
      // loadingアニメーション表示
      function onClickBtn() {
        let loadingDiv = document.getElementById('loading');
        loadingDiv.style.display = 'block';
      }
    </script>
  </head>
  <body>
    <div id="loading" class="loading">
      <div class="ring">
        loading
        <span></span>
      </div>
    </div>
(後略)
```
```css:css.html
  /* ローディングアニメーションのスタイル */
  .loading {
    display: none; /* 初期状態では非表示 */
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(255, 255, 255, 0.8);
    z-index: 9999;
    text-align: center;
    padding-top: 20%;
  }
  .ring {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    width:  150px;
    height: 150px;
    border: 3px solid #bd7ff8;
    border-radius: 50%;
    text-align: center;
    line-height: 150px;
    font-family: sans-serif;
    font-size: 20px;
    color: #8300ff;
    letter-spacing: 2px;
    text-transform: uppercase;
    background: #dedede;
    box-shadow: 0 0 0 4px #dedede;
  }
  .ring:before {
    content: '';
    position:absolute;
    top: 0px;
    left: 0px;
    height: 100%;
    width: 100%;
    border: 3px solid transparent;
    border-top: 3px solid #8300ff;
    border-right: 3px solid #8300ff;
    border-radius: 50%;
    animation: animateCircle 2s linear infinite;
  }
  .ring span {
    display: block;
    position: absolute;
    top: calc(50% - 2px);
    left: 50%;
    width: 50%;
    background: transparent;
    height: 4px;
    transform-origin: left;
    animation: animate 2s linear infinite;
  }
  .ring span:before {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #8300ff;
    top: -6px;
    right: -8px;
    box-shadow: 0 0 20px #8300ff;
  }
  @keyframes animateCircle {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }

  @keyframes animate {
    0% {
      transform: rotate(45deg);
    }
    100% {
      transform: rotate(405deg);
    }
```
こんな感じの見た目になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2782982/091f74a8-1c2f-85e6-991c-5dba57683b5e.png)

---
やっと、保存までこぎつけられました。
今日はここまで。