GASで送信中画面のあるwebフォームを作る

# 背景

GAS初心者がWebフォームをGASで自作し、入力後の送信後画面を出そうとしただけで、それは大変な時間を費やしたのでした。
たくさんサンプルはあるんですけど、いい感じに組み合わせた例は無い気がしたので、まとめてみました。

## その1. Googleフォームじゃちょっと足りないんだよね...
Googleフォームを使えば、簡単にアンケートや集計をとれます。
でも、ちょっと複雑なバリデーションチェック(入力チェック)はできなかったりと、壁を感じていました。

それでGASでwebフォームを作ったんですが、フォームデータ送信時に2点大きな問題が発生してしまいました。

* ボタンを連打すると複数回データが送信されてしまう
* 入力直後のページのままブラウザを閉じたあとに、再度ブラウザを開くと送信後の画面を開いてしまい、データを再送してしまう

## その2. そんなにJavaScript得意じゃないんだよね...
必要に迫られてその都度調べながら、切り貼りして「これでいいんだよね？」的な感じでGAS, JavaScriptのプログラミングをしていると、複数ページのWebサイトを作るのは結構大変でした。
そんなに人の参考になればいいな、と思います。

# 最初の状態：GASで複数ページを作る
こちらのページを参考に、複数ページからなるwebフォームを作りました。
この時点で最初はかなり満足していたんですけど。

[GAS（HtmlService）で複数のページを遷移（SPA）](https://web-breeze.net/htmlservice-spa-pagechange/)

# 改善後の状態：GASで複数ページ + 送信完了ページへ自動遷移
ごく簡単なWebフォームです。
名前を入力し、送信ボタンを押すと、送信中画面、送信完了画面と自動で画面が遷移します。

![webフォーム.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65847/033274ba-cb56-988e-1369-ac10f51b379c.png)

![送信中.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65847/0f2fdd37-9cf4-7a09-86e4-af0532df8e87.png)

![送信完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65847/b1e189ff-13df-b9c8-17a1-6fe454c58566.png)

このフォームを入力すると、こんな感じにスプレッドシートに保存されていきます。

![入力後スプレッドシート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65847/c6efed7a-c82d-e3d6-cae8-c4a4025b565d.png)

# プログラム
GASエディタで以下を入力し、デプロイしてください。

```javascript:code.gs
// コード.gs
function doGet() {
  return HtmlService.createTemplateFromFile("index").evaluate();
}

function saveName(name) {
  const sheetId = 'スプレッドシートID'; // ここにスプレッドシートのIDを入力
  const sheet = SpreadsheetApp.openById(sheetId).getActiveSheet();
  sheet.appendRow([name]); // 名前をスプレッドシートの最終行に追加
}
```

```html:index.html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    .loading {
      display: none;
      text-align: center; /* 中央揃え */
      margin-top: 50px;
    }
    
    .loader {
      border: 16px solid #f3f3f3; /* 薄い色のボーダー */
      border-top: 16px solid #3498db; /* 上部のボーダーの色 */
      border-radius: 50%; /* 円形にする */
      width: 50px; /* 幅 */
      height: 50px; /* 高さ */
      animation: spin 2s linear infinite; /* アニメーションを設定 */
      margin: 0 auto; /* 中央揃え */
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .page:not(.isActive) {
      display: none;
    }
  </style>
  <script>
    function submitForm() {
      const nameInput = document.querySelector('input[type="text"]').value; // 名前を取得
      document.getElementById("loader").style.display = "block"; // 処理中のグルグルを表示

      google.script.run.withSuccessHandler(function() {
        document.getElementById("loader").style.display = "none"; // ②処理中画面を非表示
        switchPage('success'); // ③送信完了画面に遷移
      }).saveName(nameInput); // ①名前を保存する関数を呼び出す
    }

    function switchPage(pageName) {
      const pages = Array.from(document.getElementsByClassName("page"));
      for (const page of pages) {
        if (page.id === pageName) {
          page.classList.add("isActive");
        } else {
          page.classList.remove("isActive");
        }
      }
    }

    document.addEventListener("DOMContentLoaded", (event) => {
      // 初期表示
      switchPage("home");
    });
  </script>
</head>
<body>
  <div id="loader" class="loading">
    <div class="loader"></div> <!-- グルグル回る要素 -->
    <div>処理中...</div> <!-- 処理中のテキスト -->
  </div>
  
  <!-- ページ切り替え -->
  <div id="home" class="page isActive">
    <h1>ホームページ</h1>
    <form onsubmit="event.preventDefault(); submitForm();">
      <input type="text" placeholder="名前" required>
      <button type="submit">送信</button>
    </form>
    <a href="#" onclick="switchPage('contact');">お問い合わせ</a>
  </div>

  <div id="success" class="page">
    <h1>送信完了</h1>
    <p>フォームが正常に送信されました！</p>
    <a href="#" onclick="switchPage('home');">戻る</a>
  </div>

  <div id="contact" class="page">
    <h1>お問い合わせ</h1>
    <p>お問い合わせ内容はこちらに入力してください。</p>
    <a href="#" onclick="switchPage('home');">戻る</a>
  </div>
</body>
</html>
```

# まとめ
ここで紹介した例が最適とはとても思えないんですが、誰かの参考になれば嬉しいです。

なお、この記事は、[【札幌現地+オンライン開催】力強くブログを108記事アウトプットする日の 20241229](https://forceoutput.connpass.com/event/339151/)のブログの一つとして書きました。
参加した皆さん、お疲れ様です！