部署名の変更や新しいカテゴリの追加が発生するたびに、GASエディタを開いてHTMLを修正し、Webアプリを「新しいバージョン」として再デプロイするのは手間がかかります。
スプレッドシートの指定シートから動的にデータを読み込み、HTML Serviceの「スクリプトレット(<? ?>)」を用いてプルダウンメニューを生成すれば、スプレッドシートの行を編集するだけで即座に選択肢が自動反映されるようになります。
この記事では、サーバー側(コード.gs)の配列処理からテンプレート(index.html)への展開までの仕組み、実装手順、安全な設計のポイントを分かりやすく解説します。
スプレッドシート・サーバー側GAS・HTMLスクリプトレットの連携フロー
スプレッドシート連携によるプルダウン動的生成のメリット
Webフォームのセレクトボックス(<select>タグ)の選択肢を動的に生成する構成には、保守・運用の面で大きな利点があります。
スプレッドシート動的連携の利点
- コード修正と再デプロイが不要:選択肢が増減しても、スプレッドシートのセルに入力するだけで即座にWebアプリへ反映されます。
- 非エンジニアでもマスタ管理が可能:現場の運用担当者がGoogleスプレッドシート上で部署名やカテゴリ一覧をいつでも自由に追加・編集できます。
- 画面のチラつき(遅延描画)を防止:ページ読み込み完了時に最初からHTMLとして選択肢が組み立てられているため、画面表示時のストレスがありません。
スクリプトレット方式と非同期通信方式の違い
GASでスプレッドシートのデータをHTML画面に渡す方法には、大きく分けて「スクリプトレット方式(HTMLテンプレート)」と「非同期通信方式(google.script.run)」の2通りがあります。
| 比較項目 | スクリプトレット方式(今回採用) | 非同期通信方式(google.script.run) |
|---|---|---|
| 処理の実行タイミング | サーバー側でHTMLを生成するタイミング | ブラウザで画面が表示された後のタイミング |
| 初期表示のスムーズさ | 良好(最初からoptionが存在するためチラつきなし) | 通信待ち時間が発生し、後から選択肢が描画される |
| クライアント側JavaScript | 不要(HTML構文だけで展開可能) | 必要(DOM操作やローディング表示の制御が必要) |
| 適した用途 | 画面表示時に必須となるマスタデータ・選択肢の展開 | ボタンクリック後のデータ保存や部分的な動的更新 |
フォーム初期表示に必要な選択肢リストは、スクリプトレット方式を採用することで最もシンプルかつ直感的に実装できます。
全体のデータ連携フローと仕組み
ブラウザからWebアプリへアクセスした際、サーバー側(GAS)でスプレッドシートを読み込み、HTMLテンプレートに値を流し込んでから完成したHTMLを返却します。
連携の3ステップ
- スプレッドシート(マスタ管理):指定シート(例: 「カテゴリ一覧」)のA列に選択肢項目を入力・保持します。
- コード.gs(サーバー側処理):スプレッドシートからA列の値を抽出し、2次元配列から扱いやすい1次元配列に平坦化して空セルを除外します。
- index.html(テンプレート評価):
createTemplateFromFile('index').evaluate()の実行時、スクリプトレット(<? ?>)がサーバー上で実行され、<option>タグが組み立てられた完全なHTMLとしてブラウザに届きます。
サーバー側の実装(コード.gs)
まずは、Googleスプレッドシートの指定シートから選択肢データを抽出し、扱いやすい配列として返却するサーバー側の関数を実装します。
選択肢取得関数の作成
GASプロジェクトの「コード.gs」に以下の関数を配置します。スプレッドシートの構造に合わせてシート名や列番号を指定します。
/**
* スプレッドシートの指定シートA列から選択肢一覧を取得する関数
* @return {string[]} 選択肢の配列(1次元配列)
*/
function getCategoriesFromSheet() {
// コンテナバインドスクリプト(スプレッドシート付属)の場合
const ss = SpreadsheetApp.getActiveSpreadsheet();
// スタンドアロン型スクリプトの場合は、以下のようにIDで明示的に指定します
// const SPREADSHEET_ID = 'ここにスプレッドシートIDを入力';
// const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
// マスタを管理しているシート名を取得
const sheet = ss.getSheetByName('カテゴリ一覧');
if (!sheet) {
return [];
}
// データの最終行を取得
const lastRow = sheet.getLastRow();
// データが1行目(ヘッダー行)しかない、または空の場合は空の配列を返す
if (lastRow < 2) {
return [];
}
// A2セルからA列の最終行までを取得(開始行:2, 開始列:1, 行数:lastRow - 1, 列数:1)
const values = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
// 2次元配列を扱いやすい1次元配列に変換し、空白セルを除外して返す
return values.map(row => row[0]).filter(val => val !== "");
}
コードの処理ポイントと解説
配列変換と安全対策の要点
- ヘッダー行のスキップ:
getRange(2, 1, lastRow - 1, 1)により、1行目の見出し(「カテゴリ名」や「部門名」)を読み込まず、データ行(2行目以降)のみを取得します。 - 2次元配列から1次元配列への変換:スプレッドシートの
getValues()は、[["総務部"], ["営業部"], ["開発部"]]という形式(行×列の2次元配列)で値を返します。HTMLテンプレートでそのまま扱うと扱いにくいため、map(row => row[0])を使って["総務部", "営業部", "開発部"]というシンプルな1次元配列に平坦化(フラット化)します。 - 空白セルの安全な除外:データ途中の空行や末尾の不要な空文字を
filter(val => val !== "")で除外することで、空の<option>が生成されるのを防ぎます。
Webアプリのエントリポイント(doGet関数)
ブラウザからWebアプリのURLにアクセスされた際にHTMLテンプレートを読み込んで評価(evaluate)する doGet 関数を定義します。
/**
* Webアプリ公開時のHTML返却処理
*/
function doGet(e) {
// index.htmlをテンプレートとして読み込む
const template = HtmlService.createTemplateFromFile('index');
// 必要に応じてユーザー情報などをテンプレートに渡すことも可能
// template.userEmail = Session.getActiveUser().getEmail();
// テンプレートを評価(スクリプトレットを実行)してHTMLを出力
return template.evaluate()
.setTitle('申請フォーム')
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
createHtmlOutputFromFile との違い
通常の静的HTMLを出力する際は HtmlService.createHtmlOutputFromFile('index') を使用しますが、後述するスクリプトレット構文(<? ?>)をサーバー側で解釈させるには、必ず HtmlService.createTemplateFromFile('index') を使用し、末尾で .evaluate() を呼び出す必要があります。
フロントエンド側の実装(index.html)
続いて、HTMLファイル側でサーバーの関数を呼び出し、セレクトボックス(<select>)内に選択肢(<option>)を動的に展開します。
HTMLテンプレートの記述
GASエディタ内の「index.html」において、プルダウンを表示させたい箇所を以下のように記述します。
<!-- カテゴリ選択プルダウン -->
<div class="form-group" style="margin-bottom: 20px;">
<label for="category-select" style="display: block; font-weight: bold; margin-bottom: 8px;">カテゴリ</label>
<select id="category-select" name="category" style="width: 100%; max-width: 400px; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 1rem;">
<option value="">-- 選択してください --</option>
<?
// サーバー側(コード.gs)で定義した関数を直接呼び出して配列を取得
const categories = getCategoriesFromSheet();
for (let i = 0; i < categories.length; i++) {
?>
<option value="<?= categories[i] ?>"><?= categories[i] ?></option>
<? } ?>
</select>
</div>
スクリプトレット構文の役割と安全性
GASのHTML Serviceでは、HTMLの中にサーバーサイドのJavaScriptコードを埋め込むための特別なタグ(スクリプトレット)が用意されています。
3つのスクリプトレット記法
- <? … ?>(コード評価スクリプトレット)
画面に直接出力せず、ロジックを実行します。変数宣言や関数の呼び出し、forループ、if文などの制御構文に使用します。 - <?= … ?>(エスケープ出力スクリプトレット)
評価結果の値をHTMLに出力します。HTMLの特殊文字(&, <, >, ", ' 等)を自動でエスケープ処理(実体参照変換)してくれるため、クロスサイトスクリプティング(XSS)の脆弱性を防ぎ、安全に出力できます。一般的なテキストや選択肢の出力には必ずこれを使用します。 - <?! … ?>(非エスケープ出力スクリプトレット)
エスケープを行わず、生のHTMLタグをそのままブラウザに送りたい場合に使用します(ユーザー入力値の出力には不向きです)。
上記のテンプレートでは、まず <? const categories = getCategoriesFromSheet(); for (...) { ?> で配列のループを開始し、ループの内側で <option value="<?= categories[i] ?>"><?= categories[i] ?></option> を評価してHTMLを組み立てています。
実装時の安全設計とベストプラクティス
実務で運用する際には、予期せぬデータ入力やエラーに対処できるよう、以下の点に配慮した実装を行うとより堅牢になります。
データ件数が0件の場合のエラーハンドリング
スプレッドシートを新規作成した直後や、運用担当者がデータを全削除してしまった場合、1行目の見出しのみが残った状態になります。
getLastRow() < 2 のチェックを省略して getRange(2, 1, 0, 1) を実行しようとすると、GASで「行数が無効です」というエラーが発生してWebアプリ全体が表示できなくなります。必ずデータ行の有無を事前に判定して空配列 [] を返却するガード節を設けましょう。
重複する選択肢の自動排除
スプレッドシートの運用において誤って同じ部署名やカテゴリが複数行に入力されてしまう可能性があります。重複を排除して一意(ユニーク)な選択肢のみを表示したい場合は、JavaScriptの Set オブジェクトを活用すると簡潔に整理できます。
// 重複を排除して1次元配列にする場合
const rawList = values.map(row => row[0]).filter(val => val !== "");
const uniqueList = Array.from(new Set(rawList));
return uniqueList;
大量データを扱う場合のパフォーマンス
選択肢が数十件〜100件程度であれば、スクリプトレット方式で極めて高速にレンダリングされます。
しかし、選択肢が数千件を超えるような大規模マスタを扱う場合は、HTML全体のデータサイズが肥大化し初期読み込みに時間がかかる可能性があります。その場合は、検索入力付きの非同期補完UI(オートコンプリート)やキャッシュ機構(CacheService)の導入を検討すると効果的です。
まとめ
GASのHTML Serviceにおけるスクリプトレット(<? ?>)を活用したスプレッドシート連携プルダウンの作成方法について解説しました。
今回のポイント
- スプレッドシートのセルを書き換えるだけで、Webアプリの再デプロイなしに選択肢が自動反映される。
- サーバー側で
map()とfilter()を使い、2次元配列を安全な1次元配列に整理して渡す。 - HTMLテンプレート側で
<? ?>によるループと<?= ?>によるエスケープ出力を組み合わせる。 HtmlService.createTemplateFromFile().evaluate()により、画面表示時のチラつきやローディング待ちが発生しない。
社内申請フォームや問い合わせ受付画面など、選択肢のメンテナンス頻度が高いWebアプリを構築する際は、ぜひこの仕組みを取り入れてみてください。
