業務システム(基幹連携や管理会計など)の開発提案や要件定義フェーズで、こんな場面に出くわしたことはないでしょうか。
業務側・PM:「仕様はまだ固まってないけど、とりあえず動くデモ画面作ってよ。A画面で入力した値がB画面の一覧に反映されるやつね!」
開発者:「(…要件も決まってないのにSQLiteやバックエンドAPI組んでも、全部作り直しになって捨てることになるんだが…)」
要件が未確定の段階でDBやサーバーサイドを構築するのは、工数とコストの無駄遣いです。
業務側が求めているのは「入力した値が別画面で見えるというUI/UXの体験」だけであることが大半です。
本記事では、サーバー・DB構築を一切行わず、ブラウザ標準の localStorage と HTML5の data-* 属性を活用して、最小限のコード量で「入力画面 ⇄ 照会画面」を連動させる実装パターンを解説します。
全体設計:なぜ「data属性」と「localStorage」なのか?
JavaScriptで表(テーブル)を扱う際、document.createElement でDOMを動的に1セルずつ組み立てるとコード量が膨れ上がり、保守性が低下します。
そこで今回は、以下の設計方針をとります。
- HTML側にあらかじめテーブルの骨組みを静的に書いておく
data-code(科目コード)やdata-month(月)を目印としてセルに埋め込む- JavaScriptは「DOM生成」をせず、「セルの値の吸い出し / 流し込み」だけに専念させる
【入力画面 (input.html)】
ユーザー入力 ──> 各行のinputから値収集 ──> localStorage.setItem()
▼ (ブラウザ内ストレージで保持)
【照会画面 (view.html)】
localStorage.getItem() ──> data属性を目印に既存のtdセルへ流し込み
パターン1:入力画面(input.html)— 既存フォームから集約して保存する
入力画面では、ユーザーが入力したマトリクス表の数値を走査し、オブジェクト配列にまとめて localStorage に保存します。
HTML構成
各行に data-code と data-name、各 <input> に data-month を持たせます。
<table id="plan-input-table">
<thead>
<tr>
<th>勘定科目</th>
<th>4月</th><th>5月</th><!-- 中略 --><th>3月</th>
</tr>
</thead>
<tbody>
<tr data-code="411000" data-name="売上高">
<td>売上高</td>
<td><input type="number" data-month="1" class="month-input"></td>
<td><input type="number" data-month="2" class="month-input"></td>
<!-- 3〜12月 -->
</tr>
<tr data-code="511000" data-name="売上原価">
<td>売上原価</td>
<td><input type="number" data-month="1" class="month-input"></td>
<td><input type="number" data-month="2" class="month-input"></td>
<!-- 3〜12月 -->
</tr>
</tbody>
</table>
<button type="button" id="save-btn">保存する</button>
<a href="view.html">照会画面へ</a>
JavaScript実装(保存&前回復元)
document.querySelectorAll で行を取得し、中の input を回すだけです。
const STORAGE_KEY = "budget_mock_data";
// 1. 保存処理
document.getElementById("save-btn").addEventListener("click", () => {
const result = [];
const rows = document.querySelectorAll("#plan-input-table tbody tr");
rows.forEach(tr => {
const code = tr.dataset.code;
const name = tr.dataset.name;
const months = {};
tr.querySelectorAll(".month-input").forEach(input => {
const month = input.dataset.month;
// 未入力は0、入力値は数値に変換
months[month] = input.value !== "" ? parseFloat(input.value) : 0;
});
result.push({ code, name, months });
});
// JSON文字列にしてlocalStorageに保存
localStorage.setItem(STORAGE_KEY, JSON.stringify(result));
alert("ブラウザに一時保存しました!");
});
// 2. 画面表示時の復元処理(再編集用)
document.addEventListener("DOMContentLoaded", () => {
const savedData = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
savedData.forEach(account => {
const tr = document.querySelector(`tr[data-code="${account.code}"]`);
if (!tr) return;
for (let m = 1; m <= 12; m++) {
const input = tr.querySelector(`input[data-month="${m}"]`);
if (input && account.months[m] !== undefined) {
input.value = account.months[m] || "";
}
}
});
});
パターン2:照会画面(view.html)— 静的テーブルへデータを流し込む
照会画面側では、入力欄ではなく閲覧用の <td> に対して、取得したJSONデータを流し込みます。
HTML構成
照会画面の <td> にも data-month や data-total を配置しておきます。
<table id="plan-view-table">
<thead>
<tr>
<th>勘定科目</th>
<th>4月</th><th>5月</th><!-- 中略 --><th>3月</th>
<th>年間合計</th>
</tr>
</thead>
<tbody>
<tr data-code="411000">
<td>売上高</td>
<td data-month="1">-</td>
<td data-month="2">-</td>
<!-- 3〜12月 -->
<td data-total>-</td>
</tr>
<tr data-code="511000">
<td>売上原価</td>
<td data-month="1">-</td>
<td data-month="2">-</td>
<!-- 3〜12月 -->
<td data-total>-</td>
</tr>
</tbody>
</table>
<a href="input.html">入力画面へ戻る</a>
JavaScript実装(流し込み処理)
DOM生成処理がないため、わずか15行足らずで完結します。
document.addEventListener("DOMContentLoaded", () => {
const STORAGE_KEY = "budget_mock_data";
const planList = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
planList.forEach(account => {
// 該当コードの行をピンポイントで取得
const tr = document.querySelector(`#plan-view-table tr[data-code="${account.code}"]`);
if (!tr) return;
let rowSum = 0;
// 1〜12月のセルにテキストを反映
for (let m = 1; m <= 12; m++) {
const td = tr.querySelector(`td[data-month="${m}"]`);
if (td) {
const val = account.months[m] || 0;
td.textContent = val.toLocaleString(); // 3桁カンマ区切り
rowSum += val;
}
}
// 合計セルに反映
const totalTd = tr.querySelector("td[data-total]");
if (totalTd) {
totalTd.textContent = rowSum.toLocaleString();
}
});
});
実務でプロトタイプを提示する際の注意点
この手法を使うと、実質2〜3時間で「あたかもバックエンドと通信しているかのような動くデモ」が完成します。
ただし、非エンジニアに見せる際には必ず以下の予防線を張る必要があります。
- 画面のヘッダーに注記を入れる
「※本画面はUI検証用モックです(ブラウザ内保存のみ/SAP連携・排他制御・DB永続化なし)」と明記する。 - 「動いているから完成間近」と錯覚させない
「本番開発では、ここからSAPとの整合性チェック、トランザクション管理、例外処理、認可制御の設計が必要です。今回はあくまで項目の過不足と操作感の合意形成を目的としています」と釘を刺しておく。
まとめ
- 要件定義前の「動くモックが欲しい」という要望に、本気でバックエンドやDBを作る必要はない。
localStorage+data-*属性 を組み合わせれば、HTMLの骨組みに値を流し込むだけの最小コードで2画面連動が作れる。- 捨てコードになる実装工数は極限まで削り、業務要件の整理・合意形成にリソースを集中させよう。
データの扱い
単一キーに巨大なJSONとして集約する方式(★推奨)
キーは app_mock_state の1つだけに絞り、その中に全画面で共有するデータを丸ごとオブジェクトとして格納する設計です(ReduxやVuexなどのグローバル状態管理と同じ考え方です)。
// キーはこれ1つだけ!
const STORAGE_KEY = "budget_app_state";
// 格納するデータ構造(State)のイメージ
const appState = {
// 1. 前提条件・メタデータ(画面1用)
metadata: {
fiscalYear: "2026",
version: "V1_見通し",
costCenter: "CC100"
},
// 2. 計画入力データ(画面2〜3用)
planData: [
{ code: "411000", name: "売上高", months: { 1: 100000, 2: 120000, /* ... */ } },
{ code: "511000", name: "売上原価", months: { 1: 60000, 2: 70000, /* ... */ } }
],
// 3. 実績データ(画面4用:比較表示用の固定値)
actualData: [
{ code: "411000", months: { 1: 95000, 2: 118000, /* ... */ } }
],
// 4. 承認・ステータス管理(画面7用)
status: {
isSubmitted: false,
submittedBy: "",
submittedAt: null
}
};
// 保存
localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
- 「全消去・初期化」が1行で終わるデモ中に「最初からやり直します」となった際、
localStorage.removeItem("budget_app_state")またはlocalStorage.setItem("budget_app_state", JSON.stringify(initialDefaultState))とするだけで、7画面全体のデータが完全にクリーンな初期状態に戻ります。 - 初期データ(マスタや実績のダミー値)を流し込みやすいデモで「前年実績」や「固定の勘定科目一覧」をあらかじめ表示しておきたい場合、JavaScriptファイル内に
DEFAULT_STATEを1つ定義しておけば、初回起動時にそれを保存するだけで全画面の準備が整います。 - 他画面の値を参照しやすい例えば「画面5(年次サマリ)」を作る際、
appState.planDataとappState.metadata.fiscalYearを一度に取得できるため、画面間の連携ロジックがシンプルになります。
実務で使える共通ユーティリティ関数
複数画面それぞれで localStorage.getItem や JSON.parse をベタ書きするとコードが重複します。
共通のスクリプト(store.js など)を用意するか、各画面の <script> に以下の3つの関数を置いておくだけで、設計が非常に堅牢になります。
const STORAGE_KEY = "budget_app_state";
// デモ用の初期データ(初回起動時やリセット時に使う)
const DEFAULT_STATE = {
metadata: { fiscalYear: "2026", version: "見通し案1" },
planData: [
{ code: "411000", name: "売上高", months: {} },
{ code: "511000", name: "売上原価", months: {} },
{ code: "611100", name: "人件費", months: {} },
{ code: "611200", name: "旅費交通費", months: {} },
{ code: "611300", name: "業務委託費", months: {} }
]
};
// 1. データ取得(無ければ初期データを返す)
function getState() {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : DEFAULT_STATE;
}
// 2. データ保存(一部だけ更新して保存)
function saveState(partialState) {
const currentState = getState();
// 現在の状態に新しいデータをマージして保存
const newState = { ...currentState, ...partialState };
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
}
// 3. 全リセット
function resetState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(DEFAULT_STATE));
}
