【デモページ付】GoogleAppsScriptでGoogleフォームの申し込み状況をウェブページに反映してみた

## はじめに
予約ページやイベントの申込ページによくある（？）申込者数を表示するやつを作ってみたので、公開までの過程を書いてみました。
また、個人情報などを扱う場合は自己責任でお願いします。
間違いなどがありましたら編集リクエストまたは、コメントで教えていただけると助かります。

なお、申込者数が定員に達した際に応募を締め切る処理などは実装していません。

* 対象者
  * GitHubPagesなどに公開するサイトに、フォームの内容を反映したい人
  * イベントページなどに申込者数などを表示したい人

* 対応ブラウザ
__XMLHttpRequest__対応ブラウザに関しては[こちら](https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest#Browser_compatibility)で確認してください。

### デモ・ソースコード
デモは下記のリンクから見ることができます。
※デモページはPC向けに作成したので、スマホの方は見づらいかもしれません。
[デモページ](https://takahiro55555.github.io/GoogleFormIndicatorDemo/)
[ソースコード](https://github.com/Takahiro55555/GoogleFormIndicatorDemo/)
![ex03_captured.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/e98bae67-558c-1097-d13e-a2c056a1f57c.png "表示例")

### 今回使用したもの
* GAS(GoogleAppsScript)
* Googleフォーム
* Googleスプレッドシート
* JavaScript
* GitHub Pages(デモページ公開用)

## Googleフォームの作成とスプレッドシートとの連携
Googleフォームとスプレッドシートとの連携方法が分かる方は、[GAS(Google Apps Script)の作成](#gasgoogle-apps-scriptの作成)まで読み飛ばしていただいて大丈夫です。

### Googleフォームの作成
まずは、Googleフォームの作成です。Googleフォームを作成してください。
一応、作成方法のリンクを載せておきます。
[Googleフォームの基本的な作り方](https://boxil.jp/mag/a1839/#1839-7)

### Googleフォームとスプレッドシートを連携させる
つぎに、GoogleフォームとGoogleスプレッドシートを連携させます。
連携方法が分からない方は、下記の記事が参考になると思います。
[スプレッドシートで回答を確認](https://boxil.jp/mag/a1839/#1839-16)

## GAS(Google Apps Script)の作成
### Googleフォームの集計
連携させたスプレッドシートを開き、集計用のシートを作成します。
今回の例では、申込者数と各陣営の数を集計しました。また、各派閥の定員数もついでに書いておきました。今回使用したスプレッドシートの関数は、__COUNTA__と__COUNTIF__の２つです。
![gform11.5_cap.png](https://qiita-image-store.s3.amazonaws.com/0/202199/aeee3a19-2912-0655-7552-ed40531331b1.png)


### GASの準備
まず初めに、GASのエディタを開きます。
Googleスプレッドシートをブラウザで開いて、__ツール__から__スクリプトエディタ__を選択します。
![gform12_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/7503c101-0f45-0c72-365d-f587960bcfa4.png)

__スクリプトエディタ__を選択すると、以下のようなページが表示されます。
![gform13_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/c55fd938-88a0-f8f4-9b13-13ec5b070d9b.png)
画像の赤枠で囲われたところに、__GAS__のコードを書いていきます。

### コード
GASのコードは以下のようになりました。下記のコードは、JSON文字列を返す処理とただの文字列を返す処理があります。
なお、ファイル名が__コード.gs__となっていますがGitHubのほうでは、__code.gs__というファイル名に変更しています。

```js:コード.gs
/**
 * @OnlyCurrentDoc
 */

// GETリクエストに対する処理
function doGet(e) {
  var ss = SpreadsheetApp.getActive();          // スプレッドシートオブジェクトを取得
  var sheet = ss.getSheetByName("summary"); // シートを指定

  // パラメータに応じた処理をする
  if(e.parameter.q == "respondents"){                         // 回答者の数をリクエストされた場合
     var result = sheet.getRange("A2").getValue();              // データを取得するセルを指定(回答者の数)
  }else{                                                      // 特に指定がなかった場合
    var respondents = sheet.getRange("A2").getValue();          // データを取得するセルを指定(回答者数)
    var takenoko    = sheet.getRange("B2").getValue();          // (タケノコ派の数)
    var kinoko      = sheet.getRange("C2").getValue();          // (キノコ派の数)
    var spectator   = sheet.getRange("D2").getValue();          // (こだわりはない人の数)
    var takenoko_prescribed  = sheet.getRange("B4").getValue(); // (タケノコ派の募集定員)
    var kinoko_prescribed    = sheet.getRange("C4").getValue(); // (キノコ派の募集定員)
    var spectator_prescribed = sheet.getRange("D4").getValue(); // (こだわりはない人の募集定員)

    var dict = {"respondents":respondents, "takenoko":takenoko, "kinoko": kinoko, "spectator":spectator,
                "takenoko_prescribed":takenoko_prescribed, "kinoko_prescribed":kinoko_prescribed, "spectator_prescribed":spectator_prescribed}; // 連想配列にする
    return ContentService.createTextOutput(JSON.stringify(dict)).setMimeType(ContentService.MimeType.JSON); // JSONを返す
  }

  return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.TEXT); // 文字列として返す
}

}
```

#### 補足

最初の部分はスクリプトを作成したドキュメント（今回はスプレッドシート）以外にアクセスできないようにするための宣言です。[解説記事](https://sleepygamersmemo.blogspot.com/2018/04/google-apps-script-onlycurrentdoc.html)

```js:コード.gs
/**
 * @OnlyCurrentDoc
 */
```

これを書かないと、公開する際に以下のような警告が出てきてきます。（詳細を押すと先に進むことはできます）
![gform18_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/9abb4a97-a4cd-82f1-b40e-9023adda3b1a.png)

また、関数名は必ず__doGet__にしてください。これは、GETリクエストが飛んできた際に呼ばれる関数です。

```js:コード.gs
// GETリクエストに対する処理
function doGet(e) {
  var ss = SpreadsheetApp.getActive();          // スプレッドシートオブエクトを取得
  (以下略)
```



### 公開する
コードが完成したら実際に公開していきます。
GASのツールバーの__公開__から__ウェブアプリケーションとして導入__を選択します。
![gform14_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/1b2dbf4f-31d3-c22b-6b3f-6ded875e9fc0.png)

実際に選択すると、以下のようなダイアログが出てきます。
画像の通りに設定したら、__導入__を選択してください。
![gform15.0_cap.png](https://qiita-image-store.s3.amazonaws.com/0/202199/64413e3e-3af1-7c2f-7407-9413119b659e.png)

一番最初に公開する際は、以下のように承認が求められます。__許可を確認__を選択して下さい。
その後、画面の指示に従って進めてください。
![gform16_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/1c9361d5-ab71-1e6f-c0f1-849a485d537d.png)

最終的に以下のような表示になります。
表示されたURLはメモに残しておいてください。
![gform22_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/6216d72c-fd45-ab83-772c-68100cd2d70b.png)

上記のURLにアクセスし、以下のように表示されたら成功です。
![gform23_cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/202199/9da3bffa-0c7f-f608-0b7b-3ffc8c243f1d.png)

## JavaScriptを使用したデータの取得、反映
次にJavaScript側の実装です。データの取得に__XMLHttpRequest__を使用しています。
また、__JSON__として受け取るものと__文字列__として受け取るもの、２種類の処理を書いてみました。
### コード
今回は、スクリプトの部分だけ載せました。HTMLも含めすべてのコードを見たい方は、[こちら](https://github.com/Takahiro55555/GoogleFormIndicatorDemo/blob/master/index.html)からアクセスしてください。
__XMLHttpRequest__のURLはGASを公開した際にメモに残しておいたURLを使ってください。

```html:index.html
  <script type="text/javascript">
    // json文字列で受け取る場合（各陣営の申込者数と定員数）
    var xmlHttpRequestJson = new XMLHttpRequest();
    xmlHttpRequestJson.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            if (this.response) {
                let takenoko    = document.getElementById("takenoko");
                let kinoko      = document.getElementById("kinoko");
                let spectator   = document.getElementById("spectator");

                // 読み込んだ人数と定員を表示する
                takenoko.innerHTML    = this.response.takenoko + "/" + this.response.takenoko_prescribed;
                kinoko.innerHTML      = this.response.kinoko + "/" + this.response.kinoko_prescribed;
                spectator.innerHTML   = this.response.spectator + "/" + this.response.spectator_prescribed;

                // 定員を超えている場合は、背景を赤にする
                if(this.response.takenoko > this.response.takenoko_prescribed){
                  takenoko.classList.add("table-danger"); //"table-danger" はbootstrapの背景色を指定するクラス
                }
                if(this.response.kinoko > this.response.kinoko_prescribed){
                  kinoko.classList.add("table-danger");
                }
                if(this.response.spectator > this.response.spectator_prescribed){
                  spectator.classList.add("table-danger");
                }
            } else {
                console.log(this.response);
            }
        }
    }
    // GASを公開した際にメモに残しておいたURL
    xmlHttpRequestJson.open("GET", "https://script.google.com/macros/s/AKfycbwNJ-8U1-rljLvTEPWuqFj_elcKkRsBELYFict_ng/exec", true);
    xmlHttpRequestJson.responseType = "json"; // JSONとして受け取る
    xmlHttpRequestJson.send(null);

    // 文字列で受け取る場合（申込者数）
    var xmlHttpRequestText = new XMLHttpRequest();
    xmlHttpRequestText.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            if (this.response) {
                let respondents = document.getElementById("respondents");
                respondents.innerHTML = this.response; // 読み込んだ人数を表示する
            } else {
                console.log(this.response);
            }
        }
    }
    // "GASを公開した際にメモに残しておいたURL" + "クエリ文字列（URLパラメータ）"
    xmlHttpRequestText.open("GET", "https://script.google.com/macros/s/AKfycbwNJ-8U1-rljLvTEPWuqFj_elcKkRsBELYFict_ng/exec?q=respondents", true);
    xmlHttpRequestText.responseType = "text"; // テキストで受け取る(DOMString)
    xmlHttpRequestText.send(null);

  </script>
```

## おわりに
Qiita初投稿で、このような文章を書くのは初めてです。分かりにくいところもあったと思いますが、最後まで読んでいただいてありがとうございます。
間違いや、改善点があれば編集リクエスト又は、コメント欄までお願いします。
特にセキュリティー系の問題がある場合は早めに教えていただけると幸いです。
今後も、気が向けば記事を投稿していこうと思います。

### 参考文献
* [XMLHttpRequest](https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest)
* [javascriptでサーバーからJSON取得](https://qiita.com/Uyutaka/items/53d2b4c96ef7715d4457)
* [ライブラリを使わない素のJavaScriptでDOM操作](https://qiita.com/kouh/items/dfc14d25ccb4e50afe89)
* [GASでJSONを返すAPIを作る](https://qiita.com/tfuruya/items/3c306ee03d1ac290bcef)
* [Google Apps Script セキュリティ診断にひっかかったときは](https://sleepygamersmemo.blogspot.com/2018/04/google-apps-script-onlycurrentdoc.html)