jQueryのバグとしか思えない動作を見つけました!
…が実はブラウザのバグのような気がします 

◆ jQuery1.9 以降で .attr("checked","checked") でチェックがつかない 

※最後の追記もごらんください

jQueryのバグ見つけた!

jQuery の1.9以降で
$("input").attr("checked", "checked");
でチェックボックスをONにしようとすると一度はできるんですが 一度OFFにすると次にONにになりません

サンプルです

1.8.3 までは動くのですが 適当に古いバージョンでよく見る1.7.2と 最新の1.11.3 のjQueryを選んでます
新旧 2 バージョンの jQuery と 生の JS で 属性 checked="checked"  をつけはずし と プロパティ checkedtrue/false の切り替えができます

具体的なコードは

[attr]
$("input").attr("checked", "checked") $("input").removeAttr("checked") // raw js elem.setAttribute("checked", "checked") elem.removeAttribute("checked")

[prop]
$("input").prop("checked", true) $("input").prop("checked", false) // raw js elem.checked = true elem.checked = false

サンプルの左側は最初からチェックついてないもので 右側が最初からチェックついてるものです

1.7.2 のほうは つけて 消して を繰り返せます
attr でも prop でもどっちでもOKです

ですが 1.11.3 だと 左側では attr-ON を一度は押せますが attr-OFF を押した後には反応しません
右側では元々チェックついていて attr-OFF をした後は一度も attr-ON でチェックが付きません
prop-ON prop-OFF は問題なく動きます

生のJSでやってみるとattr-ON attr-OFF で切替できますし prop-ON と prop-OFF でも切り替えられます

やっぱりjQueryはダメだなー

とりあえず原因調べてみる

jQuery ほどの有名なもので 1.9 リリースから数年経ってるのにこんな引っかかるひとが多そうなバグを放置しているのは謎です


とりあえず内部の動きを見てみます
色々分岐はあるものの結局 setAttributeremoveAttr で普通にやってるように見えます

OFF にした後 ON にできないので 1.7.2 と 1.11.3 で removeAttr を比べてみると
・1.7.2 は 属性を削除 にした後で プロパティ を false
・1.11.3 は プロパティを false にした後で 属性を削除
となっていました

生のJSで1.11.3の順で操作してみました
prop-OFFattr-OFF

すると

・・・

・・・・・

・・・・・・・・

・・・・・・・・・・・・生JSでも動かないです

これはもうブラウザのバグ

Chromeだけならそうとも思えるんですがFirefoxでも動かないです
ですが仕様にしては謎すぎます

もうちょっと詳しく

調べてみると プロパティを今の値と異なる値にするとその後の属性の追加・削除は属性は変わるけどプロパティは変わらないということになってるようです

属性と同じ値にプロパティを変更してもこのおかしな現象は起きないです
たぶん同じ値だと内部的にも「何もしない」からだと思います

チェックボックスにチェックがついて表示させるかは属性でなくプロパティで決まります
この現象では属性を変更したときにプロパティが変わらないので チェック状態の見た目は変わってませんが属性はちゃんと変わっています
属性を変えているのでチェックが付いてるかどうかも属性を読み取って判断すれば問題無いです
ただ 見た目としては今がチェックついてるのか分からないので チェックボックスを非表示にして画像などでチェックボックスの状態を表示するとき限定になります

jQuery1.8.3 以前ではこのバグのような謎仕様を考えられていたのかたまたまなのか属性を変えたあとでプロパティを変えるという手順なので attr-ON と attr-OFF と prop-ON と prop-OFF を適当にいっぱい押しても動かなくなるようなことはなかったです

jQuery 使わない人からすると elem.checked true/false を直接入れるのが普通でわざわざ setAttributeremoveAttribute は使わないと思いますが jQueryを使うのならidやclassなどと一緒で全部 attr でやる人も多いと思います
私もjQuery使っても prop 使うことがまずないです
そんな人がハマって原因がわからず困りそうです(実際困りました)

生のJS的には属性かプロパティのどっちかだけ変更すればよいのですが jQueryが古いブラウザ対応のためなのか removeAttr で属性だけでなくプロパティまでいじってるのがそもそもな原因なんですよね
attrremoveAttr でプロパティを触らなければ問題は起きないのに

対策

生のJS使う時

チェックボックスの値変えたい時は checked プロパティの true/false だけいじる
余計に属性まで変えておこうなんて考えない(正しい順番なら問題ないのですが面倒が増えるだけで逆効果です)

古いjQuery

jQuery1.8.3 以前のはちゃんと考えられてるので気にせず好きに使っても大丈夫です

新しいjQuery

jQuery1.9以降は改悪されてるので注意が必要です

  • attr でチェックボックスのON/OFF はしないで prop を使うのが一番ラクで安全
  • チェックボックスを直接表示しない場合は attr でも大丈夫
    ただし 今のチェック状態を読み取るときにも attr を使わないとダメ
  • cssやjsのセレクタ:checked 」はプロパティを見るので attr で書き換えているなら使ってはいけないです
    属性を [checked="checked"] というセレクタで指定する必要があります
    ※ 直接表示していないときは input:checked+label とかやること多そうなので注意です

追記

属性とプロパティの違いの理解がちゃんと出来ていなかった頃の記事なので根本的におかしな部分もあるので補足の追記です

属性とプロパティ:初期値と現在値

要素の属性とプロパティは 常に対応していて hidden や id みたいに属性を変えれば対応してプロパティも変わり プロパティを変えれば属性も変わるというものと それぞれが独立していて変更がもう一方に反映されないものがあります

フォームの入力項目では 対応していない方になります
それぞれの意味は

属性: 初期値
プロパティ: 現在の値


となります

最初に表示されるとき 属性に基づいて チェックされてるかや 入力されてる文字列や 選択しているアイテムが設定されプロパティが更新されます
それ以降 ユーザが操作して変わったものはプロパティに反映され 属性には反映されません

form.reset によるリセットでは属性の値をプロパティ反映にすることで画面を表示したときの初期値に戻します

JavaScript での操作でも プロパティの更新は現在の値の変更になります
チェック状態や選択しているアイテムを更新しますが 初期値はそのままです

また JavaScript での属性の更新は初期値を変更するものです
初期値を変える必要は基本的にはないものの フォームの初期データを HTML に書いておくのでなく localStorage から読み込んで JavaScript で制御する場合に reset の初期化で JavaScript の初期化後の値に戻したい というときに使えます

基本は初期値を変えるだけなので 現在の値のプロパティには影響しません
しかし 例外としてプロパティを一度も更新していない場合はプロパティも更新されます
それぞれの意味的には 初期化時には初期値と現在値両方を変えないといけないのですが それが手間なので初期値を変えると現在値も一緒に変えてくれる便利機能です
チェックボックスに限らず 文字入力 input の value でも同じです

一度ユーザの操作や JavaScript によって現在値を変更したなら その後の初期値の変更はもう初期化処理ではないはずなので 初期値を変えても初期値だけの変更になり 現在値は変わりません

jQuery

jQuery だと昔は attr の処理でプロパティも変えていたからか 値の変更もほぼ attr で prop はあまり使われてませんでした (私が見ていた限り)
そういうことから基本 attr を使うものだと先入観もあって attr を使っていましたが 本来の DOM 要素の意味からしても現在の値を変えるのなら prop を使うべきです

1.9 の変更で HTML 要素の直接変更と同じになったわけではなくまだ違いもあります
1.11.3 の jQuery ではプロパティの変更後でも attr で checked を削除した場合は現在値のほうも更新されチェック状態ではなくなります
最新の 3.3.1 では直接操作するのと同じ動きになります