GoogleAppsScript(GAS)で入力フォームを作る

# 概要

- Google Apps Scriptsのウェブアプリケーション
  - スプレッドシートの参照、更新、メール送信ができるため、現場でちょっとしたツールを作るのに向いてる
- gasでウェブアプリケーションとして導入するまでの手順は省略
- 使いまわせるような雛形を作っておき、後でコピペで作れるようにしておくことを目的としてます

## リンク

- 公式ガイド：https://developers.google.com/apps-script/guides/web
- gasサンプル集：https://github.com/google/google-apps-script-samples

# サンプルプログラムの内容

![スクリーンショット 2016-12-11 21.06.19.png](https://qiita-image-store.s3.amazonaws.com/0/152830/d19f2bed-b633-1280-4054-3000d6d6e7e9.png)

- 入力フォームの内容をスプレッドシートに追記する
- 入力フォームのセレクトボックス(動的)は、マスタデータを記載しているスプレッドシートから動的に生成する

# 使用ライブラリ

- jquery(あまり使いたくないけど、以下のライブラリ使うのに必要なため)
- bootstrap(デザイン)
- fontawesome(フォント)
- toastr(トースト通知)
- bootstrap-select(高機能セレクトボックス)
- bootstrap-validator(入力チェック)

# ソース

- `コード.js` :GETリクエストきたときにindex.html表示。スプレッドシート参照・更新処理。
- `index.html`:入力フォーム画面のhtmlを記述。
- `javascript.html`：javascriptを記述。index.htmlから外だししておく。
- `stylesheet.html`：スタイルシートを記述。index.htmlから外だししておく。

## `コード.js`

- doGet():GETリクエストがきたら、index.htmlを返す。javascriptとスタイルシートを外だしするために、`HtmlService.createTemplateFromFile()`を使用する。
- registerSSByFormData():入力フォームデータを受け取って、スプレッドシートを更新する。
- getSelectListFromMasterSS():スプレッドシートを参照し、入力フォームのセレクトボックスを動的に作成する。

```
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setSandboxMode(HtmlService.SandboxMode.IFRAME)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setTitle('gas入力フォームサンプル');
}

function registerSSByFormData(data) {
  
  Logger.log("data = %s", data);
  
  var datasheet = SpreadsheetApp.openById('送信データを登録するスプレッドシートのID').getSheetByName('シート名');
  var now = new Date();

  var i = datasheet.getLastRow() + 1;
  datasheet.getRange(i,  1).setValue(data[ 1]);
  datasheet.getRange(i,  2).setValue(data[ 2]);
  datasheet.getRange(i,  3).setValue(data[ 3]);
  datasheet.getRange(i,  4).setValue(data[ 4]);
  datasheet.getRange(i,  5).setValue(data[ 5]);
  datasheet.getRange(i,  6).setValue(data[ 6]);
  datasheet.getRange(i,  7).setValue(data[ 7]);
  datasheet.getRange(i,  8).setValue(data[ 8]);
  datasheet.getRange(i,  9).setValue(data[ 9]);
  datasheet.getRange(i, 10).setValue(data[10]);
  datasheet.getRange(i, 11).setValue(data[11]);
  datasheet.getRange(i,  12).setValue(Utilities.formatDate(now, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'));
  result = true;

  return {data: true};
}

function getSelectListFromMasterSS() {
  var selectList = [];
  
  // マスタデータシートを取得
  var datasheet = SpreadsheetApp.openById('マスタデータのスプレッドシートのID').getSheetByName('シート名');
  // B列2行目のデータからB列の最終行までを取得 
  var lastRow = datasheet.getRange("B:B").getValues().filter(String).length - 1;
  Logger.log("lastRow = %s", lastRow);
  // B列2行目のデータからB列の最終行までを1列だけ取得 
  selectList = datasheet.getRange(2, 2, lastRow, 1).getValues();
  Logger.log("selectList = %s", selectList); 

  return {data: selectList};
}
```

## `index.html`

- `HtmlService.createHtmlOutputFromFile()`を使い、外だししてあるjavascript.htmlと、stylesheet.htmlをincludeする。
- デザインにはbootstrapを使用

```
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <?!= HtmlService.createHtmlOutputFromFile('stylesheet').getContent(); ?>
</head>
<body>

  <nav class="navbar navbar-inverse">
    <div class="navbar-header"><a class="navbar-brand" href="#">入力フォーム</a></div>
  </nav>
  
<div class="container">
    <form class="form-horizontal" id="myForm" onsubmit="return false;">
      
        <div class="form-group">
            <label class="control-label col-xs-3" for="input_id1">氏名</label>
            <div class="col-xs-9">
                <input type="text" class="form-control" id="input_id1" required>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-xs-3" for="input_id2">パスワード</label>
            <div class="col-xs-9">
                <input type="password" class="form-control" id="input_id2" required>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-xs-3">チェックボックス</label>
            <div class="col-xs-9">
                <div class="checkbox" id="checkbox1">
                    <label><input type="checkbox" id="input_id3" >check1</label>
                    <label><input type="checkbox" id="input_id4" >check2</label>
                    <label><input type="checkbox" id="input_id5" >check3</label>
                </div>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-xs-3">ラジオボタン</label>
            <div class="col-xs-9">
                <div class="radio" id="radiobutton1">
                    <label><input type="radio" name="radio1" id="input_id6" required>radio11</label>
                    <label><input type="radio" name="radio1" id="input_id7" required>radio12</label>
                    <label><input type="radio" name="radio1" id="input_id8" required>radio13</label>
                </div>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-xs-3">セレクトボックス(search)</label>
            <div class="col-xs-9">
                <select id="select_id1" class="selectpicker form-control" data-live-search="true" width="auto">
                  <option selected>選択肢１</option>
                  <option>選択肢２</option>
                  <option>選択肢３</option>
                  <option>テストA</option>
                  <option>テストB</option>
                </select>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        
        <div class="form-group">
            <label class="control-label col-xs-3">セレクトボックス(multiple)</label>
            <div class="col-xs-9">
                <select id="select_id2" class="selectpicker form-control" multiple >
                  <option selected>ガリガリ君（ソーダ味）</option>
                  <option>ハーゲンダッツ</option>
                  <option>ピノ</option>
                  <option>ガリガリ君（コーラ味）</option>
                </select>
                <div class="help-block with-errors"></div>
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-xs-3">セレクトボックス(動的）</label>
            <div class="col-xs-9">
                <select class="form-control" id="select_id3"></select>
                <div class="help-block with-errors"></div>
            </div>
        </div>
        
        <div class="form-group">
            <div class="col-xs-offset-3 col-xs-9">
                <button id="send_button" type="submit" class="btn btn-primary btn-large">
                  <i class="fa fa-send fa-lg"></i>送信
                </button>
            </div>
        </div>
    </form>
<i id="processing" class="fa fa-refresh fa-spin fa-3x fa-fw"></i>
</div>
<?!= HtmlService.createHtmlOutputFromFile('javascript').getContent(); ?>
</body>
</html>
```

## `stylesheet.html`

- CDNのスタイルシートを参照

```
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.1/css/bootstrap-select.min.css">
```

## `javascript.html`

- 処理内容はコメントに記載の通り。
- コード.gs側のfunctionを呼び出しするのに`google.script.run.withSuccessHandler`を使用します。
- bootstrap-validatorのバリデーションはsubmitしないとかからないため小細工してます
  - フォームsubmitで、submit用のボタンのスタイルがdisabledだったら、バリデーションエラーなので、送信処理はしないようにする 
  - フォームsubmitで、画面遷移させないように、`onsubmit="return false;"`にしておく


```
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://use.fontawesome.com/7bcbed1321.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.1/js/bootstrap-select.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js"></script>
<script>
/**
 コード.jsのgetSelectListFromSS()を読んで、スプレッドシートのデータをもとに、セレクトボックスを構築する
*/
function getSelectList() {
    try {
        google.script.run
            .withSuccessHandler(function(result) {

                console.log("result", result);

                var select = document.getElementById('select_id3');
                var option = document.createElement('option');
                option.setAttribute('value', '');
                option.setAttribute('selected', true);
                option.innerHTML = '未選択';
                select.appendChild(option);

                for (var i = 0; i < result.data.length; i++) {
                    option = document.createElement('option');
                    option.setAttribute('value', i + 1);
                    option.innerHTML = result.data[i];
                    select.appendChild(option);
                }


            })
            .withFailureHandler(function(result) {
                toastr.error('リストの取得に失敗しました。', result);
            })
            .getSelectListFromMasterSS();

    } catch (e) {
        toastr.error(e);
    }
}

/**
 コード.jsのregisterSSByFormData()を呼んで、フォームの内容をスプレッドシートに登録する
*/
function sendData() {

    try {

        processing(true);

        var elemntIFrame = window.parent.document.getElementById('userHtmlFrame');
        elemntIFrame.setAttribute('scrolling', 'no');
        elemntIFrame.setAttribute('frameborder', '0');

        console.log("document.getElementById('select_id2').selectedIndex)", document.getElementById('select_id2').selectedIndex);
        
        var data = [];
        data[0] = 'dummy';
        data[1] = document.getElementById('input_id1').value;
        data[2] = document.getElementById('input_id2').value;
        data[3] = document.getElementById('input_id3').checked;
        data[4] = document.getElementById('input_id4').checked;
        data[5] = document.getElementById('input_id5').checked;
        data[6] = document.getElementById('input_id6').checked;
        data[7] = document.getElementById('input_id7').checked;
        data[8] = document.getElementById('input_id8').checked;
        data[9] = document.getElementById('select_id1').options[document.getElementById('select_id1').selectedIndex].text;
        data[10] = getMultipleSelectedText(document.getElementById('select_id2').options);
        data[11] = document.getElementById('select_id3').options[document.getElementById('select_id3').selectedIndex].text;

        console.log("data", data);
        
        
        google.script.run
            .withSuccessHandler(function(result) {
                processing(false);
                if (result.data) {
                    toastr.info('送信完了');
                } else {
                    toastr.error('送信失敗しました');
                }
            })
            .withFailureHandler(function(result) {
                processing(false);
                toastr.error('データ送信に失敗しました。', result);
            })
            .registerSSByFormData(data);
        
    } catch (e) {
        processing(false);
        toastr.error(e);
    }
    
}

// multipeセレクトをカンマ区切りのテキストにして取得
function getMultipleSelectedText(selectOptions){
  console.log(selectOptions, selectOptions.length);
  var arr = [];
  for(var i = 0; i < selectOptions.length; i++) {
    if(selectOptions[i].selected){
      arr.push(selectOptions[i].value);
    }
  }
  return arr.join(',');
}

// 処理中アイコン表示・非表示
function processing(processing) {
    if (processing) {
        document.getElementById('processing').style.visibility = "visible";
        document.getElementById('send_button').setAttribute("disabled", true);
    } else {
        document.getElementById('processing').style.visibility = "hidden";
        document.getElementById('send_button').removeAttribute("disabled");
    }
}

// ロード完了後の処理
$(function() {
    // トースト通知の位置指定
    toastr.options.positionClass = "toast-bottom-left";
    // バリデーションチェック有効化
    $('#myForm').validator();
    // 処理中アイコン非表示
    processing(false);
    // セレクトボックスのリストを動的に取得する
    getSelectList();

    // submit時のイベント登録
    $('#myForm').validator().on('submit', function(e) {
        if (document.getElementById('send_button').className.indexOf("disabled") != -1) {
            // submit用のボタンのスタイルにdisabledが設定されていればバリデーションエラー
        } else {
            // バリデーションエラーでなければ送信
            sendData();
        }
    })
});
</script>
```