0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

display:none の入力欄は今もフォーム送信に含まれる — jQuery の hide() だけで管理画面フォームを無効化しない

0
Posted at

管理画面の注文フォームで「ギフト包装」や「メッセージ欄」を 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 を外す実装にすると退避と復元が必要になります。

実装の初手を「送信されるか?」に変える手順

  1. フォームを実際に submit し、DevTools の Network → Payload で送信キーを全部書き出す
  2. その中から「条件によって隠す項目」を洗い出し、キー名を一覧にする
  3. 表示制御(toggle / hide)と送信制御(prop('disabled'))を別関数に分離する
  4. 無効化と再有効化を必ずペアで実装する(片方だけ書くと ON に戻したとき送信されない)
  5. サーバ側にも「OFF のときは無視する」バリデーションを入れて二重防御にする
  6. テストは「送信ペイロードにそのキーが存在しないこと」を 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) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?