🏫 はじめに
こんにちは!HPSサークル顧問の権藤です。
第②回ではスプレッドシートの値をWebに表示しました。第③回では HTMLフォームで入力 → スプレッドシートに追記(保存) するところまで進めます。
GAS特有の google.script.run(クライアント→GAS関数呼び出し) と SpreadsheetApp での書き込み を体験します。
🧰 使用環境と注意事項
⚙️ 前提環境
- Googleアカウント/ブラウザは Google Chrome 推奨
- Googleドライブ上で作業します
⚠️ 注意事項
- 本記事は 北海道情報専門学校 HPSサークル向け教材 を一般公開したものです。
- 内容は 記事執筆時点の動作 に基づきます。将来の仕様変更で挙動が変わる可能性があります。
🎯 この回のゴール
- ブラウザのフォームから タイトル を入力し、1行追記 できるようにします。
- 入力時に最低限の バリデーション(未入力禁止・最大長) を行います。
- 追加後に 最新10件表示 も更新します(第②回を簡約した表示付き)。
🗂️ 事前に用意するシート
①スプレッドシート(例:sample_db)に以下のヘッダを1行目に揃えてください。
id | title | done | createdAt | updatedAt
②「拡張機能 → Apps Script」からGoogle Apps Scriptを作成する

📎 設定のスクリプトプロパティ に以下を設定します(後述コードで参照します)。
-
SHEET_ID: 対象スプレッドシートのID -
SHEET_NAME: 対象シート名(未設定ならシート1)
🧠 GAS独自ポイント(今回のキモ)
-
SpreadsheetApp:サーバー側でシートを開き、appendRow()などで 追記 します
🧑💻 実装(完成コード)
同一プロジェクトに
Code.gsとview.htmlを作成して貼り付けてください。
スクリプトプロパティにSHEET_IDを設定後、デプロイ(ウェブアプリ)します。
Code.gs
/**
* 第③回 最小構成:入力→保存(簡潔版)
* 前提:シート1行目ヘッダは
* id | title | done | createdAt | updatedAt
* スクリプトプロパティ:
* SHEET_ID(必須), SHEET_NAME(任意。未設定なら先頭シート)
*/
const SP = PropertiesService.getScriptProperties();
function doGet() {
return HtmlService.createTemplateFromFile('view').evaluate()
.setTitle('GAS: Input → Sheet Save (Simple)');
}
function sheet() {
const id = SP.getProperty('SHEET_ID');
if (!id) {
throw new Error('SHEET_ID をスクリプトプロパティに設定してください');
}
const name = SP.getProperty('SHEET_NAME');
const ss = SpreadsheetApp.openById(id);
return name ? ss.getSheetByName(name) : ss.getSheets()[0];
}
// 最新 n 件を updatedAt 降順で返す(ヘッダ + 行データ)
function fetchLatest(n) {
const sh = sheet();
const values = sh.getDataRange().getValues(); // [ [header...], [row...], ... ]
if (values.length < 2) {
return { header: values[0] || [], rows: [], total: 0 };
}
const header = values[0];
const rows = values.slice(1).filter(r => String(r[0] || '').trim() !== ''); // idが空を除外
// updatedAt は 4列目(0始まりで index 4)
rows.sort((a, b) => new Date(b[4]).getTime() - new Date(a[4]).getTime());
const limit = Math.max(1, Number(n) || 10);
return { header, rows: rows.slice(0, limit), total: rows.length };
}
// 1行追記(titleのみ入力、他は自動設定)
function addItem(title) {
const t = String(title || '').trim();
if (!t) {
throw new Error('タイトルは必須です');
}
if (t.length > 100) {
throw new Error('タイトルは100文字以内にしてください');
}
const now = new Date().toISOString();
const sh = sheet();
sh.appendRow([Utilities.getUuid(), t, false, now, now]);
return { ok: true };
}
view.html
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>GAS: Input → Sheet Save (Simple)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Noto Sans JP,sans-serif;max-width:900px;margin:24px auto;padding:0 12px}
h1{font-size:1.4rem;margin:0 0 12px}
.row{display:flex;gap:8px;align-items:center}
input[type=text]{flex:1;padding:10px 12px;border:1px solid #ccc;border-radius:10px}
button{padding:10px 16px;border:1px solid #ccc;border-radius:10px;background:#fff;cursor:pointer}
button[disabled]{opacity:.5;cursor:not-allowed}
.msg{margin-top:8px}
.ok{color:#1a8917}.error{color:#b00020}.muted{color:#666}
table{border-collapse:collapse;width:100%;margin-top:16px}
th,td{border:1px solid #ddd;padding:8px;text-align:left;vertical-align:top}
thead{background:#f8f9fa}
.toolbar{display:flex;gap:12px;align-items:center;margin:12px 0}
.chip{display:inline-block;padding:2px 8px;border:1px solid #ddd;border-radius:999px;font-size:12px;color:#555}
</style>
</head>
<body>
<h1>📝 フォームからスプレッドシートへ保存(簡潔版)</h1>
<div class="row">
<input id="title" type="text" placeholder="タイトル(例:牛乳を買う)" maxlength="100"/>
<button id="add">追加</button>
</div>
<div id="msg" class="msg muted"></div>
<div class="toolbar">
<span id="count" class="chip">0 件</span>
<span class="muted">(最新10件表示)</span>
<button id="reload">再読み込み</button>
</div>
<div id="root"></div>
<script>
const $ = (s) => document.querySelector(s);
// 表示だけ日本語化
const JP_HEADER = ['ID','タイトル','完了','作成日時','更新日時'];
// TODO:エスケープ処理(XSS対策)
function showMsg(text, cls='muted'){
const el = $('#msg');
el.className = 'msg ' + cls;
el.textContent = text || '';
if(text) {
setTimeout(()=>{ el.textContent=''; el.className='msg muted'; }, 2000);
}
}
// TODOテーブル表示関数
function renderTable({ header, rows, total }){
$('#count').textContent = `${rows.length} / ${total} 件`;
if(!header || header.length === 0){
$('#root').innerHTML = '<p class="muted">データがありません。</p>'; return;
}
const thead = `<thead><tr>${JP_HEADER.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead>`;
const tbody = `<tbody>${
rows.map(r => `<tr>${r.map(c => `<td>${esc(c)}</td>`).join('')}</tr>`).join('')
}</tbody>`;
$('#root').innerHTML = `<table>${thead}${tbody}</table>`;
}
function reload(){
$('#reload').disabled = true;
google.script.run
.withSuccessHandler(d => {
renderTable(d);
$('#reload').disabled = false;
})
.withFailureHandler(e => {
showMsg(e?.message || String(e), 'error');
$('#reload').disabled = false;
})
.fetchLatest(10); // ここでGASコードを呼び出す
}
function add(){
// 入力チェック
const v = $('#title').value.trim();
if(!v){
showMsg('タイトルは必須です','error'); return;
}
if(v.length > 100){
showMsg('タイトルは100文字以内です','error'); return;
}
// TODO:エスケープ処理(XSS対策)
// TODO:重複タイトルチェック
// 追加ボタン無効化
$('#add').disabled = true;
google.script.run
.withSuccessHandler(res => {
if(res?.ok) {
$('#title').value=''; showMsg('追加しました','ok'); reload();
} else {
showMsg(res?.message || '追加に失敗しました','error');
}
$('#add').disabled = false;
})
.withFailureHandler(e => {
showMsg(e?.message || String(e), 'error');
$('#add').disabled = false;
})
.addItem(v); // ここでGASコードを呼び出す
}
// 追加ボタン押下時
$('#add').addEventListener('click', add);
// 再読み込みボタン押下時
$('#reload').addEventListener('click', reload);
// Enterキー押下時
$('#title').addEventListener('keydown', e => { if(e.key === 'Enter') add(); });
reload();
</script>
</body>
</html>
🧪 動作確認のポイント
- スクリプトプロパティに
SHEET_ID(必須)と必要に応じてSHEET_NAMEを設定します。 - デプロイ(ウェブアプリ)→ URLへアクセスしてフォームからタイトルを追加します。
- 追加後、下の一覧の 最新10件 に反映されることを確認します。
- シート側にも1行追記されているかを確認します。
🚀 発展課題
- 入力チェック強化:重複タイトルの禁止、禁止文字のチェックなど。
-
多項目フォーム:
priority(優先度)を追加。 - サニタイズ:入力値のサーバ側エスケープ/正規表現検証を追加。
ヒント
view.htmlのコード内にある「TODO:〇〇〜」を対応すること
🔭 次回予告
第④回:動きのあるHTMLに変更(JavaScript埋め込み) 🎨
フォームの入力状態に応じたUI制御や、リストのインタラクション(簡易検索・強調表示)を追加して、見た目・操作感 を磨いていきます。
✍️ 執筆者情報
執筆:HPSサークル顧問 権藤俊
本記事は北海道情報専門学校 HPSサークルの教材として作成しました。

