スプレッドシートで入力フォームを作ったのはいいけれど、毎回『ヘッダー項目』を自分で作るのって面倒ですよね。
今回は、AIツール『bolt』と『Google Apps Script (GAS)』を使って、スプレッドシートが空の状態から、項目を作ってデータ入力まで完結するフォームを作成した経緯を記事にしました。

前回は既存のシートを読み込むフォームを作りましたが、今回は一歩進んで初期状態の扱いを強化しました。
具体的には、『スプレッドシートに項目が一つもない状態』で、フォームを使ってA列に項目を追加してから、その項目に基づいたデータ入力・修正ができるようにするという仕様です。

boltで作成した際の指示文
まずはboltにスプレッドシートのアドレスを渡して、構造を認識させます。
やりたいことを伝えます。
この機能で今回はスプレッドシートでの項目行に合わせて新規追加フォーム・内容修正フォームを作成してます。
機能の変更で、スプレッドシート初期状態値が入ってない状態でA列に項目を挿入するフォームを作成してその内容に沿って、データの新規追加・内容修正を行えるように修正できますか?
boltが生成してくれたコードを実際にGASへ貼り付けて試します。
『ここの動作が上手くいかない』『修正ボタンを押した時に値が反映されない』といった具体的なフィードバックをboltに戻し、コードを再生成してもらう……この『対話』を繰り返すことで、徐々に精度を高めていきました。
GASに記入したコード
完成しました!空のスプレッドシートが、フォームからの操作で項目が作成され、フォームを使ってデータの修正・削除が可能になりました。
Code.gs
/**
* 動的テーブル管理アプリ — サーバー側(GAS)
* ============================================================
* スプレッドシートをデータベース代わりに使い、
* 項目(列)を自由に定義 → その項目に合わせてデータの新規追加・修正ができる。
*
* シート構成:
* 1. 「項目定義」シート — 定義された項目一覧(A列: 項目名、B列: 表示順)
* 2. 「データ」シート — 登録されたデータ(1行目が項目名のヘッダー、2行目以降がデータ)
*/
// シート名の定義
var COLUMNS_SHEET = '項目定義';
var DATA_SHEET = 'データ';
/**
* スプレッドシートを開いたときに自動実行される(シンプルトリガー)
* 毎回メニューを追加するので、シートを開き直してもボタンが消えない
*/
function onOpen() {
addMenuToSpreadsheet();
}
/**
* アドオンインストール時にも実行される(念のため)
*/
function onInstall() {
onOpen();
}
/**
* メニューをスプレッドシートに追加する(共通処理)
*/
function addMenuToSpreadsheet() {
SpreadsheetApp.getActiveSpreadsheet().addMenu('データ管理アプリ', [
{ name: 'アプリを開く', functionName: 'showApp' },
{ name: '初期セットアップ', functionName: 'setup' }
]);
}
/**
* インストール可能なトリガーを作成する(一度実行すれば以降ずっと有効)
* Apps Scriptエディタで createTrigger を実行する
*/
function createTrigger() {
// 既存のonOpenトリガーを削除(重複を防ぐ)
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() === 'onOpen') {
ScriptApp.deleteTrigger(triggers[i]);
}
}
// 新しいインストール可能トリガーを作成
ScriptApp.newTrigger('onOpen')
.forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet())
.onOpen()
.create();
SpreadsheetApp.getUi().alert('トリガーを作成しました。\n以降、スプレッドシートを開くだけで自動的にメニューが表示されます。');
}
/**
* 初回セットアップ — シートを2つ作成し、初期状態にする
* メニューの「データ管理アプリ」→「初期セットアップ」から実行する
*/
function setup() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 項目定義シートを作成(なければ)
var colsSheet = ss.getSheetByName(COLUMNS_SHEET);
if (!colsSheet) {
colsSheet = ss.insertSheet(COLUMNS_SHEET);
}
// ヘッダー行をセット(A列: 項目名、B列: 表示順)
if (colsSheet.getLastRow() === 0) {
colsSheet.getRange(1, 1, 1, 2).setValues([['項目名', '表示順']]);
colsSheet.getRange(1, 1, 1, 2).setFontWeight('bold');
colsSheet.getRange(1, 1, 1, 2).setBackground('#dbeafe');
colsSheet.setColumnWidth(1, 200);
colsSheet.setColumnWidth(2, 100);
}
// データシートを作成(なければ)
var dataSheet = ss.getSheetByName(DATA_SHEET);
if (!dataSheet) {
dataSheet = ss.insertSheet(DATA_SHEET);
}
// メニューは onOpen で毎回自動追加されるので、ここでは不要
// スプレッドシートを開いたときに自動でメニューが表示されるよう
// インストール可能なトリガーを作成する
createTrigger();
// 今回すぐメニューを表示する
addMenuToSpreadsheet();
SpreadsheetApp.getUi().alert('セットアップが完了しました。\n以降、スプレッドシートを開くだけでメニューの「データ管理アプリ」が表示されます。');
}
/**
* HTML画面を表示する(メニューから呼ばれる)
*/
function showApp() {
var html = HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('データ管理アプリ')
.setWidth(900)
.setHeight(700)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
SpreadsheetApp.getUi().showModalDialog(html, 'データ管理アプリ');
}
/**
* 画面表示用の初期データを取得する
* @return {{ columns: string[], rows: Object[] }}
*/
function getInitialData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var colsSheet = ss.getSheetByName(COLUMNS_SHEET);
var dataSheet = ss.getSheetByName(DATA_SHEET);
var columns = [];
var rows = [];
// 項目定義を取得(2行目以降、A列)
if (colsSheet && colsSheet.getLastRow() > 1) {
var colData = colsSheet.getRange(2, 1, colsSheet.getLastRow() - 1, 1).getValues();
for (var i = 0; i < colData.length; i++) {
if (colData[i][0]) columns.push(String(colData[i][0]));
}
}
// データを取得(1行目=ヘッダー、2行目以降=データ行)
if (dataSheet && dataSheet.getLastRow() > 1 && columns.length > 0) {
var lastCol = Math.max(columns.length, dataSheet.getLastColumn());
var rangeData = dataSheet.getRange(2, 1, dataSheet.getLastRow() - 1, lastCol).getValues();
for (var j = 0; j < rangeData.length; j++) {
var values = rangeData[j].map(function(v) { return v === '' ? '' : String(v); });
rows.push({ id: j + 2, values: values }); // id = スプレッドシートの行番号
}
}
return { columns: columns, rows: rows };
}
/**
* 新しい項目(列)を追加する
* @param {string} name 項目名
* @return {{ success: boolean, error?: string, columns?: string[] }}
*/
function addColumn(name) {
if (!name || !name.trim()) {
return { success: false, error: '項目名を入力してください' };
}
name = name.trim();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var colsSheet = ss.getSheetByName(COLUMNS_SHEET) || ss.insertSheet(COLUMNS_SHEET);
var dataSheet = ss.getSheetByName(DATA_SHEET) || ss.insertSheet(DATA_SHEET);
// 既存の項目一覧を取得
var existing = getColumnsList(colsSheet);
if (existing.indexOf(name) !== -1) {
return { success: false, error: '同じ名前の項目がすでにあります' };
}
// 項目定義シートに追加(A列: 項目名、B列: 表示順)
var nextRow = colsSheet.getLastRow() + 1;
var nextOrder = existing.length + 1;
colsSheet.getRange(nextRow, 1, 1, 2).setValues([[name, nextOrder]]);
// データシートのヘッダー行に項目名を追加
var headerCol = existing.length + 1; // 追加位置は既存項目数+1列目
dataSheet.getRange(1, headerCol).setValue(name);
dataSheet.getRange(1, 1, 1, headerCol).setFontWeight('bold');
dataSheet.getRange(1, 1, 1, headerCol).setBackground('#dbeafe');
dataSheet.setColumnWidth(headerCol, 150);
var columns = getColumnsList(colsSheet);
return { success: true, columns: columns };
}
/**
* 項目(列)を削除する
* @param {string} name 項目名
* @return {{ success: boolean, error?: string, columns?: string[] }}
*/
function deleteColumn(name) {
if (!name) return { success: false, error: '項目名が指定されていません' };
var ss = SpreadsheetApp.getActiveSpreadsheet();
var colsSheet = ss.getSheetByName(COLUMNS_SHEET);
var dataSheet = ss.getSheetByName(DATA_SHEET);
if (!colsSheet) return { success: false, error: '項目定義シートが見つかりません' };
// 項目定義シートから該当行を探して削除
var lastRow = colsSheet.getLastRow();
if (lastRow < 2) return { success: false, error: '削除できる項目がありません' };
var colData = colsSheet.getRange(2, 1, lastRow - 1, 1).getValues();
var targetRow = -1;
for (var i = 0; i < colData.length; i++) {
if (String(colData[i][0]) === name) {
targetRow = i + 2; // シート上の行番号(ヘッダー+1ずらし)
break;
}
}
if (targetRow === -1) return { success: false, error: '該当する項目が見つかりません' };
// 項目定義シートから行を削除
colsSheet.deleteRow(targetRow);
// データシートのヘッダーから該当列を探して削除
if (dataSheet && dataSheet.getLastColumn() > 0) {
var headerRow = dataSheet.getRange(1, 1, 1, dataSheet.getLastColumn()).getValues()[0];
for (var c = 0; c < headerRow.length; c++) {
if (String(headerRow[c]) === name) {
dataSheet.deleteColumn(c + 1);
break;
}
}
}
var columns = getColumnsList(colsSheet);
return { success: true, columns: columns };
}
/**
* 新しいデータ行を追加する
* @param {Object} values 項目名→値 のオブジェクト
* @return {{ success: boolean, error?: string }}
*/
function addRow(values) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var colsSheet = ss.getSheetByName(COLUMNS_SHEET);
var dataSheet = ss.getSheetByName(DATA_SHEET) || ss.insertSheet(DATA_SHEET);
if (!colsSheet) return { success: false, error: '項目定義シートが見つかりません' };
var columns = getColumnsList(colsSheet);
if (columns.length === 0) {
return { success: false, error: '項目が定義されていません。先に項目を追加してください。' };
}
// ヘッダー行がまだ無ければセット
if (dataSheet.getLastColumn() < columns.length) {
dataSheet.getRange(1, 1, 1, columns.length).setValues([columns]);
dataSheet.getRange(1, 1, 1, columns.length).setFontWeight('bold');
dataSheet.getRange(1, 1, 1, columns.length).setBackground('#dbeafe');
for (var c = 0; c < columns.length; c++) {
dataSheet.setColumnWidth(c + 1, 150);
}
}
// valuesのオブジェクトを配列に変換(項目の順序に合わせる)
var rowValues = columns.map(function(col) { return values[col] || ''; });
// データシートの最終行+1に行を追加
var targetRow = dataSheet.getLastRow() + 1;
dataSheet.getRange(targetRow, 1, 1, columns.length).setValues([rowValues]);
return { success: true };
}
/**
* 既存のデータ行を更新(修正)する
* @param {number} rowId スプレッドシートの行番号
* @param {Object} values 項目名→値 のオブジェクト
* @return {{ success: boolean, error?: string }}
*/
function updateRow(rowId, values) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var colsSheet = ss.getSheetByName(COLUMNS_SHEET);
var dataSheet = ss.getSheetByName(DATA_SHEET);
if (!colsSheet || !dataSheet) {
return { success: false, error: 'シートが見つかりません' };
}
var columns = getColumnsList(colsSheet);
if (columns.length === 0) {
return { success: false, error: '項目が定義されていません' };
}
// valuesのオブジェクトを配列に変換
var rowValues = columns.map(function(col) { return values[col] || ''; });
// 指定行を更新
dataSheet.getRange(rowId, 1, 1, columns.length).setValues([rowValues]);
return { success: true };
}
/**
* データ行を削除する
* @param {number} rowId スプレッドシートの行番号
* @return {{ success: boolean, error?: string }}
*/
function deleteRow(rowId) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dataSheet = ss.getSheetByName(DATA_SHEET);
if (!dataSheet) return { success: false, error: 'データシートが見つかりません' };
if (rowId < 2) return { success: false, error: '削除対象の行が不正です' };
dataSheet.deleteRow(rowId);
return { success: true };
}
/**
* 項目定義シートから項目名一覧を取得する(ヘルパー関数)
* @param {GoogleAppsScript.Spreadsheet.Sheet} colsSheet
* @return {string[]}
*/
function getColumnsList(colsSheet) {
var lastRow = colsSheet.getLastRow();
if (lastRow < 2) return [];
var data = colsSheet.getRange(2, 1, lastRow - 1, 1).getValues();
var list = [];
for (var i = 0; i < data.length; i++) {
if (data[i][0]) list.push(String(data[i][0]));
}
return list;
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>データ管理アプリ</title>
<style>
/* ===== 全体リセット ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: #dbeafe;
--success: #16a34a;
--success-hover: #15803d;
--error: #dc2626;
--error-hover: #b91c1c;
--neutral-50: #f9fafb;
--neutral-100: #f3f4f6;
--neutral-200: #e5e7eb;
--neutral-300: #d1d5db;
--neutral-400: #9ca3af;
--neutral-500: #6b7280;
--neutral-600: #4b5563;
--neutral-700: #374151;
--neutral-800: #1f2937;
--radius: 10px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', sans-serif;
background: var(--neutral-50);
color: var(--neutral-800);
line-height: 1.6;
padding: 16px;
}
/* ===== ヘッダー ===== */
.app-header {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
color: white;
padding: 20px;
border-radius: var(--radius);
margin-bottom: 16px;
}
.app-header h1 { font-size: 1.3rem; font-weight: 700; }
.app-header p { font-size: 0.8rem; opacity: 0.85; margin-top: 2px; }
/* ===== タブ ===== */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 2px solid var(--neutral-200);
}
.tab {
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: var(--neutral-500);
border-bottom: 3px solid transparent;
margin-bottom: -2px;
font-family: inherit;
}
.tab:hover { color: var(--neutral-700); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
/* ===== カード ===== */
.card {
background: white;
border: 1px solid var(--neutral-200);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card-header h2 { font-size: 1rem; font-weight: 600; }
.badge {
font-size: 0.72rem;
padding: 3px 8px;
border-radius: 999px;
background: var(--primary-light);
color: var(--primary);
font-weight: 500;
}
/* ===== ボタン ===== */
.btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 7px 14px;
border: none;
border-radius: 7px;
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
font-family: inherit;
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover { background: var(--success-hover); }
.btn-danger { background: var(--error); color: white; }
.btn-danger:hover { background: var(--error-hover); }
.btn-secondary {
background: var(--neutral-100);
color: var(--neutral-700);
border: 1px solid var(--neutral-300);
}
.btn-secondary:hover { background: var(--neutral-200); }
.btn-sm { padding: 4px 9px; font-size: 0.76rem; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* ===== フォーム ===== */
.form-row {
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
min-width: 150px;
}
.form-group label {
font-size: 0.78rem;
font-weight: 500;
color: var(--neutral-600);
}
.form-group input {
padding: 7px 11px;
border: 1px solid var(--neutral-300);
border-radius: 7px;
font-size: 0.82rem;
font-family: inherit;
background: white;
color: var(--neutral-800);
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
/* ===== チップ ===== */
.column-chips {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 12px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 11px;
background: var(--primary-light);
color: var(--primary);
border-radius: 999px;
font-size: 0.78rem;
font-weight: 500;
}
.chip-delete {
cursor: pointer;
opacity: 0.6;
font-size: 1rem;
line-height: 1;
border: none;
background: none;
padding: 0;
color: inherit;
}
.chip-delete:hover { opacity: 1; }
/* ===== テーブル ===== */
.table-wrapper {
overflow-x: auto;
margin-top: 10px;
border: 1px solid var(--neutral-200);
border-radius: 8px;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.data-table th {
background: var(--neutral-100);
padding: 8px 10px;
text-align: left;
font-weight: 600;
color: var(--neutral-600);
white-space: nowrap;
border-bottom: 2px solid var(--neutral-200);
}
.data-table th.action-col { text-align: center; width: 130px; }
.data-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--neutral-100);
color: var(--neutral-700);
}
.data-table td.action-col { text-align: center; white-space: nowrap; }
.data-table tbody tr:hover { background: var(--neutral-50); }
.data-table tbody tr:last-child td { border-bottom: none; }
/* ===== 空状態 ===== */
.empty-state {
text-align: center;
padding: 28px 16px;
color: var(--neutral-400);
}
.empty-state p { margin-bottom: 8px; }
/* ===== モーダル ===== */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: var(--radius);
padding: 20px;
width: 90%;
max-width: 480px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.modal-header h3 { font-size: 1.1rem; font-weight: 600; }
.modal-close {
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
color: var(--neutral-400);
line-height: 1;
}
.modal-close:hover { color: var(--neutral-600); }
.modal-body { display: flex; flex-direction: column; gap: 12px; }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
/* ===== 通知 ===== */
.notice {
padding: 8px 12px;
border-radius: 7px;
font-size: 0.8rem;
margin-bottom: 10px;
}
.notice-success { background: #d1fae5; color: #065f46; }
.notice-error { background: #fee2e2; color: #991b1b; }
/* ===== ローディング ===== */
.loading {
text-align: center;
padding: 40px;
color: var(--neutral-400);
}
</style>
</head>
<body>
<!-- ヘッダー -->
<div class="app-header">
<h1>データ管理アプリ</h1>
<p>項目(列)を自由に定義して、データの新規追加・修正ができます</p>
</div>
<!-- 通知エリア -->
<div id="notice-area"></div>
<!-- タブ -->
<div class="tabs">
<button class="tab active" id="tab-data" onclick="switchTab('data')">データ管理</button>
<button class="tab" id="tab-columns" onclick="switchTab('columns')">項目設定</button>
</div>
<!-- ローディング表示 -->
<div id="loading" class="loading">読み込み中...</div>
<!-- メインコンテンツ(ロード後に表示) -->
<div id="main-content" style="display:none;">
<!-- ===== タブ1: データ管理 ===== -->
<div id="tab-content-data">
<!-- 新規追加フォーム -->
<div class="card">
<div class="card-header">
<h2>新規追加</h2>
<span class="badge" id="data-col-count"></span>
</div>
<div id="add-form-area"></div>
</div>
<!-- データ一覧 -->
<div class="card">
<div class="card-header">
<h2>データ一覧</h2>
<span class="badge" id="data-row-count"></span>
</div>
<div id="table-area"></div>
</div>
</div>
<!-- ===== タブ2: 項目設定 ===== -->
<div id="tab-content-columns" style="display:none;">
<div class="card">
<div class="card-header">
<h2>項目設定</h2>
<span class="badge" id="col-count"></span>
</div>
<!-- 項目追加フォーム -->
<div class="form-row">
<div class="form-group">
<label>新しい項目名</label>
<input type="text" id="new-col-name" placeholder="例: ID、アドレス、パスワード など"
onkeydown="if(event.key==='Enter') addColumn()">
</div>
<button class="btn btn-primary" onclick="addColumn()">項目を追加</button>
</div>
<!-- チップ表示 -->
<div id="chips-area"></div>
</div>
</div>
</div>
<!-- 編集モーダル -->
<div id="edit-modal" style="display:none;">
<div class="modal-overlay" onclick="closeEditModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>データを修正</h3>
<button class="modal-close" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body" id="edit-form-area"></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">キャンセル</button>
<button class="btn btn-primary" onclick="saveEditRow()">保存する</button>
</div>
</div>
</div>
</div>
<script>
// ===== アプリ全体で使うデータ =====
var columns = []; // 項目名の配列
var rows = []; // データ行の配列
var editingRowId = null; // 編集中の行番号
// ===== 通知を表示(3秒後に消える) =====
function showNotice(type, message) {
var area = document.getElementById('notice-area');
area.innerHTML = '<div class="notice notice-' + type + '">' + escapeHtml(message) + '</div>';
setTimeout(function() { area.innerHTML = ''; }, 3000);
}
// ===== HTMLエスケープ(XSS対策) =====
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ===== タブ切り替え =====
function switchTab(tabName) {
var tabData = document.getElementById('tab-data');
var tabCols = document.getElementById('tab-columns');
var contentData = document.getElementById('tab-content-data');
var contentCols = document.getElementById('tab-content-columns');
if (tabName === 'data') {
tabData.className = 'tab active';
tabCols.className = 'tab';
contentData.style.display = '';
contentCols.style.display = 'none';
} else {
tabData.className = 'tab';
tabCols.className = 'tab active';
contentData.style.display = 'none';
contentCols.style.display = '';
}
}
// ===== 初期データを読み込む =====
function loadInitialData() {
google.script.run
.withSuccessHandler(function(result) {
columns = result.columns || [];
rows = result.rows || [];
document.getElementById('loading').style.display = 'none';
document.getElementById('main-content').style.display = '';
renderAll();
})
.withFailureHandler(function(err) {
document.getElementById('loading').innerHTML = 'データの読み込みに失敗しました: ' + escapeHtml(err.message);
})
.getInitialData();
}
// ===== 画面をすべて描画し直す =====
function renderAll() {
renderAddForm();
renderTable();
renderChips();
updateBadges();
}
// ===== バッジ(件数表示)を更新 =====
function updateBadges() {
document.getElementById('data-col-count').textContent = columns.length + '項目';
document.getElementById('data-row-count').textContent = rows.length + '件';
document.getElementById('col-count').textContent = columns.length + '項目';
}
// ===== 新規追加フォームを描画 =====
function renderAddForm() {
var area = document.getElementById('add-form-area');
if (columns.length === 0) {
area.innerHTML = '<div class="empty-state">' +
'<p>まず「項目設定」タブで項目(列)を追加してください。</p>' +
'<button class="btn btn-primary" onclick="switchTab(\'columns\')">項目設定へ移動</button>' +
'</div>';
return;
}
var html = '<div class="form-row">';
for (var i = 0; i < columns.length; i++) {
html += '<div class="form-group">' +
'<label>' + escapeHtml(columns[i]) + '</label>' +
'<input type="text" id="new-' + i + '" placeholder="' + escapeHtml(columns[i]) + 'を入力">' +
'</div>';
}
html += '</div>';
html += '<div style="margin-top:12px;"><button class="btn btn-success" onclick="addRow()">追加する</button></div>';
area.innerHTML = html;
}
// ===== データ一覧テーブルを描画 =====
function renderTable() {
var area = document.getElementById('table-area');
if (columns.length === 0) {
area.innerHTML = '<div class="empty-state"><p>項目が定義されていません。</p></div>';
return;
}
if (rows.length === 0) {
area.innerHTML = '<div class="empty-state"><p>データがまだありません。上のフォームから追加してください。</p></div>';
return;
}
var html = '<div class="table-wrapper"><table class="data-table"><thead><tr>';
for (var c = 0; c < columns.length; c++) {
html += '<th>' + escapeHtml(columns[c]) + '</th>';
}
html += '<th class="action-col">操作</th></tr></thead><tbody>';
for (var r = 0; r < rows.length; r++) {
html += '<tr>';
var values = rows[r].values || [];
for (var c = 0; c < columns.length; c++) {
var val = values[c] !== undefined ? values[c] : '';
html += '<td>' + escapeHtml(val) + '</td>';
}
html += '<td class="action-col">' +
'<button class="btn btn-secondary btn-sm" style="margin-right:5px;" onclick="openEditModal(' + r + ')">修正</button>' +
'<button class="btn btn-danger btn-sm" onclick="deleteRow(' + r + ')">削除</button>' +
'</td></tr>';
}
html += '</tbody></table></div>';
area.innerHTML = html;
}
// ===== 項目チップを描画 =====
function renderChips() {
var area = document.getElementById('chips-area');
if (columns.length === 0) {
area.innerHTML = '<div class="empty-state" style="margin-top:14px;">' +
'<p>項目がまだありません。上のフォームから追加してください。</p>' +
'<p style="font-size:0.78rem;">追加した項目が「データ管理」タブのフォームに反映されます。</p>' +
'</div>';
return;
}
var html = '<div class="column-chips">';
for (var i = 0; i < columns.length; i++) {
html += '<span class="chip">' + escapeHtml(columns[i]) +
'<button class="chip-delete" onclick="deleteColumn(' + i + ')" title="削除">×</button></span>';
}
html += '</div>';
html += '<div style="margin-top:16px;padding:10px 12px;background:var(--neutral-50);border-radius:7px;font-size:0.78rem;color:var(--neutral-500);">' +
'ヒント: 項目を追加すると、自動的に「データ管理」タブのフォームに入力欄が追加されます。</div>';
area.innerHTML = html;
}
// ===== 項目を追加する =====
function addColumn() {
var input = document.getElementById('new-col-name');
var name = input.value.trim();
if (!name) {
showNotice('error', '項目名を入力してください');
return;
}
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
columns = result.columns || [];
input.value = '';
renderAll();
showNotice('success', '項目「' + name + '」を追加しました');
} else {
showNotice('error', result.error || '項目の追加に失敗しました');
}
})
.withFailureHandler(function(err) {
showNotice('error', 'エラー: ' + err.message);
})
.addColumn(name);
}
// ===== 項目を削除する =====
function deleteColumn(index) {
var name = columns[index];
if (!confirm('項目「' + name + '」を削除しますか?\nデータシートの該当列も削除されます。')) return;
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
columns = result.columns || [];
renderAll();
showNotice('success', '項目「' + name + '」を削除しました');
} else {
showNotice('error', result.error || '項目の削除に失敗しました');
}
})
.withFailureHandler(function(err) {
showNotice('error', 'エラー: ' + err.message);
})
.deleteColumn(name);
}
// ===== 新規データ行を追加する =====
function addRow() {
if (columns.length === 0) {
showNotice('error', '項目が定義されていません');
return;
}
var values = {};
for (var i = 0; i < columns.length; i++) {
var input = document.getElementById('new-' + i);
values[columns[i]] = input ? input.value.trim() : '';
}
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
// フォームをクリア
for (var i = 0; i < columns.length; i++) {
var input = document.getElementById('new-' + i);
if (input) input.value = '';
}
// データを再読み込み
reloadData();
showNotice('success', 'データを追加しました');
} else {
showNotice('error', result.error || 'データの追加に失敗しました');
}
})
.withFailureHandler(function(err) {
showNotice('error', 'エラー: ' + err.message);
})
.addRow(values);
}
// ===== 編集モーダルを開く =====
function openEditModal(rowIndex) {
var row = rows[rowIndex];
if (!row) return;
editingRowId = row.id;
var area = document.getElementById('edit-form-area');
var html = '';
for (var i = 0; i < columns.length; i++) {
var val = (row.values && row.values[i] !== undefined) ? row.values[i] : '';
html += '<div class="form-group">' +
'<label>' + escapeHtml(columns[i]) + '</label>' +
'<input type="text" id="edit-' + i + '" value="' + escapeHtml(val) + '">' +
'</div>';
}
area.innerHTML = html;
document.getElementById('edit-modal').style.display = '';
}
// ===== 編集モーダルを閉じる =====
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
editingRowId = null;
}
// ===== 編集したデータを保存する =====
function saveEditRow() {
if (!editingRowId) return;
var values = {};
for (var i = 0; i < columns.length; i++) {
var input = document.getElementById('edit-' + i);
values[columns[i]] = input ? input.value.trim() : '';
}
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
closeEditModal();
reloadData();
showNotice('success', 'データを更新しました');
} else {
showNotice('error', result.error || 'データの更新に失敗しました');
}
})
.withFailureHandler(function(err) {
showNotice('error', 'エラー: ' + err.message);
})
.updateRow(editingRowId, values);
}
// ===== データ行を削除する =====
function deleteRow(rowIndex) {
var row = rows[rowIndex];
if (!row) return;
if (!confirm('このデータを削除しますか?')) return;
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
reloadData();
showNotice('success', 'データを削除しました');
} else {
showNotice('error', result.error || 'データの削除に失敗しました');
}
})
.withFailureHandler(function(err) {
showNotice('error', 'エラー: ' + err.message);
})
.deleteRow(row.id);
}
// ===== データだけを再読み込み(項目はそのまま) =====
function reloadData() {
google.script.run
.withSuccessHandler(function(result) {
columns = result.columns || [];
rows = result.rows || [];
renderAll();
})
.withFailureHandler(function(err) {
showNotice('error', 'データの再読み込みに失敗しました: ' + err.message);
})
.getInitialData();
}
// ===== 画面表示時にデータを読み込む =====
loadInitialData();
</script>
</body>
</html>
appsscript.json
{
"timeZone": "Asia/Tokyo",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"sheets": {
"macros": []
}
}
今回のように『AIに明確な前提条件を渡すこと』と『細かな修正をいとわずに繰り返すこと』で、短時間で実用的なツールが作れます。


コメント