管理画面の注文フォームで「ギフト包装」や「メッセージ欄」を OFF にしたはずが、注文データに値が紛れ込んでいた——というのが今回の話です。原因は $('#message-row').hide() だけでは送信対象から外れないこと。HTML のフォーム仕様に戻って整理し、実装手順まで落とします。
環境
- jQuery 3.7.1
- Chrome 121 / Safari 17.2 / Firefox 122
- 管理画面: Rails 7.1 + ERB + jQuery(Bootstrap 5 の
d-noneを併用) - 検証: DevTools の Console / Network パネル、Playwright 1.41
- 同様の構成は Laravel + Blade、Spring Boot + Thymeleaf でも再現します
結論:送信対象は「成功するコントロール」だけ
HTML のフォーム送信仕様では、送信データに含まれるのは successful controls(成功するコントロール) だけです。display:none はその条件に一切含まれていません。CSS は「どう見えるか」しか決めず、「送るかどうか」には関与しません。
送信対象から 外れない もの:
-
display:none(hide()、Bootstrap のd-none、hidden属性、style="display:none") -
visibility:hidden(余白が残るだけで送信される) opacity:0- 親が非表示で、子だけ表示指定されているケース
-
readonlyが付いた入力欄
送信対象から 外れる もの:
-
disabledが付いたコントロール -
name属性が無いコントロール - DOM に存在しないコントロール(
remove()、Vue のv-if、React の条件レンダリング) - チェックボックス / ラジオで
checkedが false のもの - 送信操作に使われなかったボタン(
type="button"、押されていない submit)
実際に検証してみる
まず、よくあるアンチパターンから。
<form id="order-form" method="post" action="/orders">
<label><input type="checkbox" name="gift_wrap" checked> ギフト包装</label>
<div id="message-row">
<input type="text" name="message" value="熨斗をお願いします">
</div>
<button type="submit">保存</button>
</form>
// 見た目だけ消す(アンチパターン)
$('#message-row').hide();
Array.from(new FormData(document.querySelector('#order-form')));
// => [["gift_wrap", "on"], ["message", "熨斗をお願いします"]]
message は画面から消えているのに、送信配列にはしっかり残っています。jQuery の serialize() も同じ規則で動きます。
$('#order-form').serialize();
// "gift_wrap=on&message=%E7%87%…"
つまり、非表示にしただけの項目はサーバに届きます。サーバ側が「OFF なら無視する」実装になっていなければ、そのまま注文データに混入します。
正しい実装:表示の制御と送信の制御を分ける
hide() は表示の制御、disabled は送信の制御です。役割が違うので、関数を分けて同時に呼びます。
function setOptionEnabled(enabled) {
const $row = $('#message-row');
const $inputs = $row.find('input, select, textarea');
$row.toggle(enabled); // 見た目の制御
$inputs.prop('disabled', !enabled); // 送信の制御
if (!enabled) {
// チェックボックス・ラジオは checked も落としておく
$inputs.filter(':checkbox, :radio').prop('checked', false);
}
}
ポイントは prop('disabled', true) が 値そのものを消さない ことです。ユーザーが入力した内容は DOM に残るので、ON に戻したときに復元処理を書かなくて済みます。remove() で消す実装にすると値が失われ、name を外す実装にすると退避と復元が必要になります。
実装の初手を「送信されるか?」に変える手順
- フォームを実際に submit し、DevTools の Network → Payload で送信キーを全部書き出す
- その中から「条件によって隠す項目」を洗い出し、キー名を一覧にする
- 表示制御(
toggle/hide)と送信制御(prop('disabled'))を別関数に分離する - 無効化と再有効化を必ずペアで実装する(片方だけ書くと ON に戻したとき送信されない)
- サーバ側にも「OFF のときは無視する」バリデーションを入れて二重防御にする
- テストは「送信ペイロードにそのキーが存在しないこと」を assert する
Playwright なら、実際の POST ボディを覗いて検証できます。
test('OFFにした項目は送信ペイロードに含まれない', async ({ page }) => {
const [request] = await Promise.all([
page.waitForRequest(r => r.method() === 'POST' && r.url().endsWith('/orders')),
page.click('button[type=submit]'),
]);
const body = new URLSearchParams(request.postData() ?? '');
expect(body.has('message')).toBe(false); // 隠した項目は届かない
expect(body.has('gift_wrap')).toBe(true);
});
手法の比較
| 手法 | 見た目 | 送信 | 値の保持 | 復元 |
|---|---|---|---|---|
.hide() / display:none
|
消える | 含まれる | 保持 | 容易 |
visibility:hidden |
消える(余白は残る) | 含まれる | 保持 | 容易 |
hidden 属性 / d-none
|
消える | 含まれる | 保持 | 容易 |
prop('disabled', true) |
変化なし | 含まれない | 保持 | 容易 |
name 属性を外す |
変化なし | 含まれない | 保持 | 退避が必要 |
remove() / v-if
|
消える | 含まれない | 失われる | 不可 |
type="hidden" に変える |
消える | 含まれる | 保持 | 容易 |
readonly |
変化なし | 含まれる | 保持 | 容易 |
Vue の v-show は display:none を出すだけ、v-if は DOM から削除します。「表示を切り替えたいだけ」のつもりで v-show を選ぶと、今回と同じ穴が開きます。
ハマりポイント
-
checkedを false にしただけでは不十分。チェックボックスは送信されなくなりますが、テキストやセレクトにはcheckedの概念がなく送信され続けます。disabledを軸にしましょう -
readonlyは送信対象。disabledと混同しやすい典型です -
requiredはdisabled要素には効かない。ブラウザのバリデーション対象から外れるため、「必須のはずが空で通る」状態になり得ます -
テンプレートに
style="display:none"が残っている。CSS で消しているのか JS で消しているのかが二重管理になり、どちらが効いているか分からなくなります -
送信直前に
disabledを一時解除する実装。値を拾うために解除した瞬間、送信対象に戻ります。解除と再有効化は必ず関数化しましょう
FAQ
Q. .serialize() なら display:none の値は除外されますか?
いいえ。serializeArray() は successful controls と同じ規則で、除外されるのは disabled と name 無しだけです。display:none は含まれます。
Q. サーバ側で無視すればよいのでは?
多重防御としては有効ですが、それだけでは POST を受け付ける API や別経路で同じ穴が開きます。「クライアントで送らない」が第一防御です。
Q. disabled にすると見た目がグレーになって困ります
disabled が変わらせるのはブラウザ既定のスタイルです。CSS で色を上書きするか、name 属性の退避で対応します。
Q. name を外す方法と disabled はどちらが良いですか?
基本は disabled です。name を外す方法は、値の退避と復元、およびサーバ側のパラメータ名の一貫性を自分で管理する必要があります。
まとめ
hide() は表示の制御、disabled は送信の制御です。隠す処理を書いたら、「この項目は今、送信対象か?」を必ずセットで確認する。今回の実装は次の 1 行に集約しました。
$row.toggle(enabled).find(':input').prop('disabled', !enabled);
隠す前に送信キーを洗い出す——これを実装の初手にしています。あなたの管理画面は、hide() だけで止まっていませんか?
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中