コンテンツにスキップ

実装パターン集

このガイドで紹介した設計方針を実装に落とし込む際の参考例を紹介します。

フレームワークや状況によって最適な実装は異なるため、あくまで「こういうアプローチもある」という参考として見てください。


詳細画面で「編集モード」に切り替えるパターンです。

<div class="inline-edit">
<input type="checkbox" id="edit-user-1" class="edit-toggle" />
<div class="view-mode">
<span>田中太郎</span>
<label for="edit-user-1" class="btn">編集</label>
</div>
<form class="edit-mode" method="post" action="/users/1">
<input type="text" name="name" value="田中太郎" />
<button type="submit">保存</button>
<label for="edit-user-1" class="btn">キャンセル</label>
</form>
</div>
<style>
.edit-toggle { display: none; }
.edit-mode { display: none; }
.edit-toggle:checked ~ .view-mode { display: none; }
.edit-toggle:checked ~ .edit-mode { display: block; }
</style>
<div x-data="{ isEditing: false, draft: { name: '田中太郎' }, original: { name: '田中太郎' } }">
<form x-show="isEditing">
<input type="text" x-model="draft.name" />
<button type="button" x-on:click="$dispatch('save', draft); isEditing = false">
保存
</button>
<button type="button" x-on:click="draft = { ...original }; isEditing = false">
キャンセル
</button>
</form>
<div x-show="!isEditing">
<p x-text="draft.name"></p>
<button x-on:click="original = { ...draft }; isEditing = true">編集</button>
</div>
</div>
  • 編集画面への遷移が不要
  • CSS のみ版はフォーム送信でサーバー処理
  • Alpine.js 版はキャンセルで元の値に戻せる

一覧画面で直接追加するパターンです。

<table>
<tbody>
<tr><td>田中太郎</td></tr>
<tr><td>佐藤花子</td></tr>
<tr class="inline-add">
<td>
<input type="checkbox" id="add-user" class="add-toggle" />
<label for="add-user" class="add-button">+ 追加</label>
<form class="add-form" method="post" action="/users">
<input type="text" name="name" placeholder="名前を入力" />
<button type="submit">追加</button>
<label for="add-user" class="btn">キャンセル</label>
</form>
</td>
</tr>
</tbody>
</table>
<style>
.add-toggle { display: none; }
.add-form { display: none; }
.add-toggle:checked ~ .add-button { display: none; }
.add-toggle:checked ~ .add-form { display: block; }
</style>
<div x-data="{ users: [], isAdding: false, newName: '' }">
<table>
<tbody>
<template x-for="user in users" x-bind:key="user.id">
<tr>
<td x-text="user.name"></td>
</tr>
</template>
<tr x-show="isAdding">
<td>
<input type="text" x-model="newName" placeholder="名前を入力" />
<button x-on:click="users.push({ id: Date.now(), name: newName }); newName = ''; isAdding = false">
追加
</button>
<button x-on:click="isAdding = false">キャンセル</button>
</td>
</tr>
<tr x-show="!isAdding">
<td>
<button x-on:click="isAdding = true">+ 追加</button>
</td>
</tr>
</tbody>
</table>
</div>
  • 登録画面への遷移が不要
  • CSS のみ版はフォーム送信でサーバー処理
  • Alpine.js 版は即座に一覧に反映される

確認画面の代わりに「取り消し」で対応するパターンです。

<div x-data="{ deleted: false, timeoutId: null }">
<div x-show="deleted">
削除しました
<button x-on:click="clearTimeout(timeoutId); deleted = false">取り消し</button>
</div>
<button
x-show="!deleted"
x-on:click="
deleted = true;
timeoutId = setTimeout(() => $dispatch('delete'), 5000)
"
>
削除
</button>
</div>
  • 確認画面より速い
  • 「本当に削除しますか?」より「削除しました。取り消しますか?」
  • 実際の削除は遅延実行する

確認画面の代わりにモーダルを使うパターンです。

<a href="#delete-confirm">削除</a>
<dialog id="delete-confirm">
<p>削除しますか?</p>
<form method="post" action="/users/1">
<input type="hidden" name="_method" value="DELETE" />
<button type="submit">削除する</button>
<a href="#">キャンセル</a>
</form>
</dialog>
<style>
dialog {
display: none;
}
dialog:target {
display: block;
}
</style>
<div x-data="{ isOpen: false }">
<button x-on:click="isOpen = true">削除</button>
<dialog x-bind:open="isOpen">
<p>田中太郎 を削除しますか?</p>
<button x-on:click="$dispatch('delete'); isOpen = false">削除する</button>
<button x-on:click="isOpen = false">キャンセル</button>
</dialog>
</div>
  • 画面遷移なしで確認できる
  • 背景が見えたまま操作できる
  • セッション管理が不要

詳細情報を展開するパターンです。JavaScript は不要です。

<details>
<summary>田中太郎</summary>
<dl>
<dt>メールアドレス</dt>
<dd>tanaka@example.com</dd>
<dt>部署</dt>
<dd>営業部</dd>
</dl>
</details>
<style>
details {
border: 1px solid #ccc;
padding: 0.5em;
}
details[open] summary {
font-weight: bold;
}
</style>
  • 詳細画面への遷移が不要
  • 一覧で複数の詳細を同時に開ける
  • JavaScript なしで動作する

保存とは別に状態を変更するパターンです。

PUT /items/:id → 内容を更新
PATCH /items/:id/status → 状態のみ変更
<select
x-data
x-on:change="$dispatch('status-change', { status: $event.target.value })"
>
<option value="draft">下書き</option>
<option value="published">公開</option>
<option value="archived">アーカイブ</option>
</select>
  • 編集画面を開かなくても状態を変えられる
  • 保存ボタンを押さなくても状態が変わる
  • 一覧画面から直接操作できる

インライン編集を実装する際に考慮すべきポイントです。

ユーザーの操作を即座に画面に反映し、サーバー処理は裏で行います。

1. ユーザーが保存ボタンを押す
2. 即座に UI を更新(成功を前提)
3. 裏でサーバーに送信
4. 失敗したら UI を元に戻す

待ち時間がなくなり、体感速度が上がります。

サーバー処理が失敗した場合の対応です。

  • 変更前の値を保持しておく
  • 失敗時は元の値に戻す
  • ユーザーにエラーを通知する
  • 再試行の手段を提供する
タイミング用途
入力中(リアルタイム)形式チェック(メール形式など)
フォーカス移動時必須チェック
保存時サーバー側の整合性チェック

すべてを保存時にまとめると、ユーザーが戸惑うことがあります。

  • フォーカス管理:編集モードに入ったら入力欄にフォーカス
  • キーボード操作:Enter で保存、Escape でキャンセル
  • スクリーンリーダー:状態の変化を通知(aria-live
  • 十分なコントラスト:編集中であることを視覚的に明示

キーボードショートカットの例

Section titled “キーボードショートカットの例”
// Enter で保存、Escape でキャンセル
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') save();
if (e.key === 'Escape') cancel();
});

これらは一例であり、プロジェクトの状況に応じて調整が必要です。

共通して言えるのは:

  • 画面遷移を減らすと、コードもシンプルになる傾向がある
  • セッション管理が不要になると、状態の組み合わせが減る
  • その場で操作できると、テストも書きやすくなる
  • CSS だけで実現できるパターンは、JavaScript の複雑さを避けられる

設計がシンプルになれば、実装もシンプルになります。その逆も然りです。