I just realized a strange behaviour when removing and re-appending options to a select element. It happens that if one of the options is selected, after appended, the next item becomes selected instead of the original one. Consider the following html:
var $opts = $("#sel option").remove();
console.log($opts);
$("#sel").append($opts);
<select id="sel">
<option>A</option>
<option>B</option>
<option selected="selected">C</option>
<option>D</option>
</select>
(Look in the console.)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
(Or as a fiddle)
It makes the option with value "D"
to be selected and not the option with value "C"
, as defined originally. Notice the options printed in the console, the attribute selected
is changed after the remove()
method.
Why does that happen?
Note: I know how to fix it or work around it, that's not the question. The question is why does it happen?
The option to be removed is selected by getting the select box. The value to be removed is specified on the value selector (value='optionValue') on the select box. The remove() method is then used to remove this selected option.
Select remove() MethodThe remove() method is used to remove an option from a drop-down list. Tip: To add an option to a drop-down list, use the add() method.
Method 1: Append the option tag to the select boxThe select box is selected with the jQuery selector and this option is added with the append() method. The append() method inserts the specified content as the last child of the jQuery collection. Hence the option is added to the select element.
Fascinating question.
I believe it's down to the browser automatically selecting an option when the only selected option is removed, and then what happens when you add an option into a box that has the selected
flag set. It also appears to be browser-specific, which probably shouldn't surprise us. But even more, Chrome varies its behavior depending on how closely we're watching. That did surprise me. Details below.
First, let's use something like your updated example, but where the one that we start out with selected is neither first nor last, and see where we end up. So we'll have A, B, C, D, and E, and start out with B selected:
$("input[type=button]").on("click", function() {
var $opts = $("#sel option").remove();
$("#sel").append($opts);
});
<select id="sel">
<option>A</option>
<option selected="selected">B</option>
<option>C</option>
<option>D</option>
<option>E</option>
</select>
<input type="button" value="Go">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
The results we observe vary by browser:
To make matters worse, Chrome's behavior varies depending on whether we observe the selected
property of the elements as this is happening or not. Heisenbug! If we observe it, it ends up doing what Firefox does (E) instead of C.
That gives us a big clue as to what's happening in Chrome. But let's start with the easy ones:
It would appear that IE11 just leaves the flags alone, probably because it doesn't worry about updating them while the JavaScript code is running, only when it yields back and IE updates its UI.
Firefox makes sense if it's proactive about setting the selected
flag on an option when there are none selected in the box (because we removed the selected one):
selected
flag set.selected
flag is set, it becomes the selected property and A is de-selected.We can watch IE11 and Firefox happen, since they don't change their behavior depending on whether we observe them:
var $opts = $("#sel option");
showOptions("Before", $opts);
$opts.each(function() {
var opt = $(this);
opt.remove();
showOptions("After removing " + opt.text(), $opts);
});
$opts.each(function() {
var opt = $(this);
$("#sel").append(opt);
showOptions("After appending " + opt.text(), $opts);
});
function showOptions(label, $opts) {
var row = $("<tr>");
row.append($("<td>").text(label).addClass("label"));
$opts.each(function() {
row.append($("<td>").text(this.selected ? "*" : "o"));
});
row.appendTo("#results");
}
th, td {
min-width: 3em;
text-align: center;
padding: 4px;
}
table, th, td {
border-collapse: collapse;
border: 1px solid #eee;
}
.label {
text-align: left;
}
<select id="sel">
<option>A</option>
<option selected="selected">B</option>
<option>C</option>
<option>D</option>
<option>E</option>
</select>
<table>
<thead>
<tr>
<th></th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
<th>E</th>
</tr>
</thead>
<tbody id="results">
</tbody>
</table>
* = selected, o = not selected
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
Which brings us to the hard one: Chrome. Chrome changes its behavior based on whether we observe the selected
property as we go. Specifically, it changes its behavior if we observe selected
on the options that are still in the select box as we remove them. It doesn't care if we look at selected
on the removed ones, and it doesn't care whether we look at selected
as we're appending. Just as we're removing, and just the ones we haven't removed yet.
First, here's proof:
$("#btn").on("click", function() {
var observeLTE = true;
var observeGT = true;
var index;
var $opts = $("#sel option");
showOptions("Before", $opts);
observeLTE = $("#cbremove")[0].checked;
observeGT = $("#cbremoveun")[0].checked;
$opts.each(function(i) {
var opt = $(this);
index = i;
opt.remove();
showOptions("After removing " + opt.text(), $opts);
});
observeLTE = observeGT = $("#cbappend")[0].checked;
$opts.each(function(i) {
var opt = $(this);
index = i;
$("#sel").append(opt);
showOptions("After appending " + opt.text(), $opts);
});
function showOptions(label, $opts) {
var row = $("<tr>");
row.append($("<td>").text(label).addClass("label"));
$opts.each(function(i) {
var selected;
if ((observeLTE && i <= index) || (observeGT && i > index)) {
selected = this.selected ? "*" : "o";
} else {
selected = "-";
}
row.append($("<td>").text(selected));
});
row.appendTo("#results");
}
});
th, td {
min-width: 3em;
text-align: center;
padding: 4px;
}
table, th, td {
border-collapse: collapse;
border: 1px solid #eee;
}
.label {
text-align: left;
}
<div>
<label>
<input id="cbremove" type="checkbox">Observe <code>selected</code> on removed options while removing
</label>
</div>
<div>
<label>
<input id="cbremoveun" type="checkbox">Observe <code>selected</code> on UNremoved options while removing
</label>
</div>
<div>
<label>
<input id="cbappend" type="checkbox">Observe <code>selected</code> while appending
</label>
</div>
<select id="sel">
<option>A</option>
<option selected="selected">B</option>
<option>C</option>
<option>D</option>
<option>E</option>
</select>
<input id="btn" type="button" value="Go">
<table>
<thead>
<tr>
<th></th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
<th>E</th>
</tr>
</thead>
<tbody id="results">
</tbody>
</table>
* = selected, o = not selected, - = not observed
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
If you give that a spin (on Chrome, obviously), you'll notice that you end up on C regardless of what you do with the first or last checkbox, but checking the middle checkbox makes you end up on E.
That gives us enough data to infer an explanation. I can't say it actually is the explanation, just that it fits the available data. If someone wanted to be more thorough, they could step through Chrome's internal code in a debug build. I decided that was overkill.
So here's my answer, which I stress again is inferred:
selected
property on the HTMLOptionElement
instance lazily. That is, if you don't read the property's value, Chrome doesn't check to make sure that the property reflects the current state of things.selected
property when queuing a change
event.change
event if one is already pending for that select box.Based on that, here's what I think happens in Chrome:
change
event because the value hasn't changed.change
event, which observes selected
and thus sets the property on C.change
event pending, no new change
event is queued, and so D's selected
isn't observed and its value isn't updated; it remains false
even though if we read it, it would turn true
.selected
as true
on B and C, but none of the others, because D and E weren't observed while they were the selected option and didn't update their value. We know those are their values because we're allowed to observe them at that point.selected = true
when another element is already selected deselects the previous element. So B is deselected when C is added. C is left alone when we add D and E because their selected
property is false
.Whew, that was fun to figure out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With