【続】GASで超簡単にウェブサイトを作ってみる〜バリデーションを追加する〜

# はじめに
この記事は「[Ateam LifeDesign Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ateam-life-design)」で完走賞を狙って25記事書いているうちの22日目の記事です。今年も完走目指して頑張るぞ！

# 作るもの

[昨日](https://qiita.com/kakakaori830/private/b82cd8273931bfcfa14d)まででGASで簡単なウェブサイトを作って見た目を整えていきました。ウェブサイトを作る際にフォームの部分にはほぼ100％バリデーションが必要になってくるので今日はバリデーション処理を追加していきます。

追加するバリデーションは以下の2種類を実装していきます。
- 「顔文字」の必須バリデーション
- 「くだもの」の重複バリデーション（過去に同じものが投稿されている場合は投稿できない）


# サンプルコード

処理としてはPOSTをされたときにバリデーションの判定を行うためdoPost関数の先頭に処理を追加していきます。

```diff_javascript
function doPost(e) {
+  var kao = e.parameter.kao;
+  var fruit = e.parameter.fruit;
+  var alert = {'kao':'', 'fruit': ''};
  // 以下処理略
}
```
まずこの部分でエラー文言が入る変数を`alert`という名前で用意しておきます。顔文字に関するエラーとくだものに関するエラーが入るように連想配列で準備します。

## 必須バリデーション

```diff_javascript
function doPost(e) {
  var kao = e.parameter.kao;
  var fruit = e.parameter.fruit;
  var alert = {'kao':'', 'fruit': ''};
  var values = {'kao': kao, 'fruit': fruit};

+  if (kao == '') {
+    alert['kao'] = '顔文字は必ず入力してください';
+    return createIndexHtml(alert, values);
+  } 
  // 以下処理略
} 

+function createIndexHtml(alert = {'kao':'', 'fruit': ''}) {
+  const items = sheet.getDataRange().getValues();
+  var template = HtmlService.createTemplateFromFile("index");
+  template.deployURL = ScriptApp.getService().getUrl();
+  template.items = items;
+  template.alert = alert;
+  return template.evaluate();
+}
```

顔文字の必須バリデーションは上述の通りです。存在の判定でよいのでif文で空文字か否かを判定し、空だったときは`alert['kao']`に対してエラー文言をセットします。その後`index.html`を表示するのですがここの関数が以前用意した`doGet`関数の中身とほぼ同じ内容になるため関数として切り出しておきます。`doGet`関数のとの違いは`template.alert`を使ってエラー内容をテンプレート側に渡す処理を追加している点になります。

同様に重複バリデーションも作っていきます。

## 重複バリデーション

重複バリデーションは先程のように空文字との比較みたいに単純にはできないので、まず与えられたくだもの名がすでに存在するか否かを判定する`checkDuplicatedFruit`という関数をつくっておきます。

```javascript
function checkDuplicatedFruit(fruit) {
  var lastRow = sheet.getLastRow();
  var data = sheet.getRange('C1:C' + lastRow).getValues(); 

  for (var i = 0; i < data.length; i++) {
    if (data[i][0] === fruit) {
      return true; // 同じくだものが見つかった場合はtrueを返す
    }
  }

  return false; // 同じくだものが見つからなかった場合はfalseを返す
}
```

この関数ではC列を最終行まで見て行ってそれらのセルに書き込まれている値を`data`変数に保持しておきます。あとはその変数を1つずつ見ていきながら与えられたくだもの名と同じものがあれば重複があったということで`true`を、最後まで同じものがみつからなければ`false`を返すようにしています。
この関数を使って先程の`doPost`関数に処理を追加すると

```diff_javascript
function doPost(e) {
  var kao = e.parameter.kao;
  var fruit = e.parameter.fruit;
  var alert = {'kao':'', 'fruit': ''};

  if (kao == '') {
    alert['kao'] = '顔文字は必ず入力してください';
    return createIndexHtml(alert);
  }  

+  if (checkDuplicatedFruit(fruit)) {
+    alert['fruit'] = 'くだものがすでに登録されているものと重複しています';
+    return createIndexHtml(alert);
+  }
  // 以下処理略
}
```

これによってすでに登録があるくだものの場合は先程同様エラー文言を渡すようにします。

では次にこのエラー文言をサイト側で表示するために`index.html`を編集していきます。

## index.htmlにエラー内容を表示

```diff_html:index.html
          <div class="col">
            <label for="kao" class="form-label">顔文字</label>
-           <input type="text" class="form-control" id="kao" name="kao">            
+           <input type="text" class="form-control <?= alert['kao'] ? 'is-invalid' : '' ?>" id="kao" name="kao">
+           <div class="invalid-feedback">
+             <?= alert['kao'] ?>
+           </div>  
          </div>
          <div class="col">
            <label for="fruit" class="form-label">くだもの</label>
-           <input type="text" class="form-control" id="fruit" name="fruit">
+           <input type="text" class="form-control <?= alert['fruit'] ? 'is-invalid' : '' ?>" id="fruit" name="fruit">
+           <div class="invalid-feedback">
+             <?= alert['fruit'] ?>
+           </div>              
          </div>
```

Bootstrapのバリデーションエラー表示のためのCSSを使っています。エラー内容自体は`invalid-feedback`というクラスを振った`div`内に表示するようにし、同列にある兄弟要素の`input`タグにエラーがある場合は`is-invalid`というクラスを追加します。これによりフォームのinput部分が赤枠で囲われてエラー文言が表示されるようになります。

:::note warn
`is-invalid`クラスを追加せず`invalid-feedback`のみを追加した場合、エラー文言が表示されないので注意してください。
:::

と、これで完成したように思えるのですが、このままだとエラーが発生した場合、前に入力していた内容が保持されません。再度すべて入力するようにさせるのはユーザビリティが悪いので入力内容を保持できるように処理を追加していきます。

## 入力内容の保持

```diff_javascript
function doPost(e) {
  var kao = e.parameter.kao;
  var fruit = e.parameter.fruit;
  var alert = {'kao':'', 'fruit': ''};
+  var values = {'kao': kao, 'fruit': fruit};

  if (kao == '') {
    alert['kao'] = '顔文字は必ず入力してください';
-    return createIndexHtml(alert);    
+    return createIndexHtml(alert, values);
  }  

  if (checkDuplicatedFruit(fruit)) {
    alert['fruit'] = 'くだものがすでに登録されているものと重複しています';
-    return createIndexHtml(alert);
+    return createIndexHtml(alert, values);
  }
  // 以下処理略
}
```

まず`doPost`関数内で受け取った`values`をhtmlを生成する関数に第2引数として渡してやります。

```diff_javascript
-function createIndexHtml(alert = {'kao':'', 'fruit': ''}) {
+function createIndexHtml(alert = {'kao':'', 'fruit': ''}, values = {'kao':'', 'fruit': ''}) {
  const items = sheet.getDataRange().getValues();
  var template = HtmlService.createTemplateFromFile("index");
  template.deployURL = ScriptApp.getService().getUrl();
  template.items = items;
  template.alert = alert;
+  template.values = values;
  return template.evaluate();
}
```

`createIndexHtml`関数側でも第2引数として`values`を受け取れるようにしたあと、テンプレート側にその値を引き渡すようにします。

```diff_html:index.html
          <div class="col">
            <label for="kao" class="form-label">顔文字</label>
-            <input type="text" class="form-control <?= alert['kao'] ? 'is-invalid' : '' ?>" id="kao" name="kao">
+            <input type="text" class="form-control <?= alert['kao'] ? 'is-invalid' : '' ?>" id="kao" name="kao" value="<?= values['kao'] ?>">
            <div class="invalid-feedback">
              <?= alert['kao'] ?>
            </div>  
          </div>
          <div class="col">
            <label for="fruit" class="form-label">くだもの</label>
-            <input type="text" class="form-control <?= alert['fruit'] ? 'is-invalid' : '' ?>" id="fruit" name="fruit">
+            <input type="text" class="form-control <?= alert['fruit'] ? 'is-invalid' : '' ?>" id="fruit" name="fruit" value="<?= values['fruit'] ?>">
            <div class="invalid-feedback">
              <?= alert['fruit'] ?>
            </div>              
          </div>
```

そしてテンプレート側で受け取った値を`input`フォームの`value`属性にしていすることで、受け取った値がページを読み込んだ段階で表示されるようにします。

# 完成品

# コード
全体はこんな感じになりました。

```javascript
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName('Sheet1'); // シート名を入力

function doGet() {
  return createIndexHtml();
}

function createIndexHtml(alert = {'kao':'', 'fruit': ''}, values = {'kao':'', 'fruit': ''}) {
  const items = sheet.getDataRange().getValues();
  var template = HtmlService.createTemplateFromFile("index");
  template.deployURL = ScriptApp.getService().getUrl();
  template.items = items;
  template.alert = alert;
  template.values = values;
  return template.evaluate();
}

function doPost(e) {
  var kao = e.parameter.kao;
  var fruit = e.parameter.fruit;
  var alert = {'kao':'', 'fruit': ''};
  var values = {'kao': kao, 'fruit': fruit};

  if (kao == '') {
    alert['kao'] = '顔文字は必ず入力してください';
    return createIndexHtml(alert, values);
  }  

  if (checkDuplicatedFruit(fruit)) {
    alert['fruit'] = 'くだものがすでに登録されているものと重複しています';
    return createIndexHtml(alert, values);
  }

  var currentDate = new Date();
  const newRow = [
    currentDate.toLocaleDateString(),
    kao, 
    fruit
  ];
  sheet.appendRow(newRow);

  var template = HtmlService.createTemplateFromFile("complete");
  return template.evaluate();
}

function checkDuplicatedFruit(fruit) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var lastRow = sheet.getLastRow();
  var data = sheet.getRange('C1:C' + lastRow).getValues(); 

  for (var i = 0; i < data.length; i++) {
    if (data[i][0] === fruit) {
      return true; // 同じフルーツが見つかった場合はtrueを返す
    }
  }

  return false; // 同じフルーツが見つからなかった場合はfalseを返す
}
```

```index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>
  <body>
    <div class="container" style="max-width: 1000px;">
      <h1>簡単なサイトを作るよ。</h1>
      <table class="table table-striped">
        <thead>
          <tr><th>日付</th><th>顔</th><th>食べ物</th></tr>
        </thead>
        <tbody>
          <? for(let i = 1; i < items.length; i++){ ?>
          <? let item = items[i]; ?>
          <tr><td><?= item[0] ?></td><td><?= item[1] ?></td><td><?= item[2] ?></td></tr>
          <? } ?>
        </tbody>
      </table>

      <form method="post" action="<?= deployURL ?>" class="mt-4"> 
        <div class="mb-3 row">
          <div class="col">
            <label for="kao" class="form-label">顔文字</label>
            <input type="text" class="form-control <?= alert['kao'] ? 'is-invalid' : '' ?>" id="kao" name="kao" value="<?= values['kao'] ?>">
            <div class="invalid-feedback">
              <?= alert['kao'] ?>
            </div>  
          </div>
          <div class="col">
            <label for="fruit" class="form-label">くだもの</label>
            <input type="text" class="form-control <?= alert['fruit'] ? 'is-invalid' : '' ?>" id="fruit" name="fruit" value="<?= values['fruit'] ?>">
            <div class="invalid-feedback">
              <?= alert['fruit'] ?>
            </div>              
          </div>
        </div>        
        <button type="submit" class="btn btn-primary">送信</button>
      </form>
    </div>
  </body>
</html>
```

![スクリーンショット 2023-12-17 15.57.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/96ec4f7d-9f8b-a9d0-c1af-d282078ce010.png)

![スクリーンショット 2023-12-17 15.58.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/33c5bedc-355d-e3ca-fa01-4e43a4f36180.png)


顔文字の必須バリデーションもくだものの重複バリデーションもきちんと動いていることが確認できました。

# 最後に
今日はGASで作ったフォームにバリデーション処理を追加する方法をみていきました。必須や重複チェック以外にも、在庫管理システムであれば在庫が発注数以上あるかなどのチェックや、単純なメールアドレス、電話番号のフォーマットチェックなど様々な処理を追加することができます。ぜひ色々試してみてください！