追記(2020-05-25)
Chrome 83.0.4103.61 だともう起きません。
概要
Chrome で documentFragment をつかいつつ appendChild
すると存在してるはずの要素が表示されないことがある。
条件
調べた限りだと、以下の全てを満たしてると起きる。
- Chrome 81.0.4044.122
- 他のバージョンは調べてない
- 子要素が隠れる可能性のある要素
-
<select>
と<option>
とか <details>
とその子要素とか
-
- 一つの関数内で、親要素を空にして子要素をいったんdocumentFragmentにいれてから本物のDOMに追加する処理を行う
- DOM上にかつて存在したが今は存在していない子要素を追加する
つまり以下のいずれかの条件を満たすと起きない。
- Firefox
- 常にブラウザ上に表示される親子要素に対する操作
-
<ul>
と<li>
とか -
<div>
とか - (要はほぼ全ての要素)
-
- documentFragment を使わない
-
document.createElement
で新たに作成した要素を追加する - 親要素を空にする操作と子要素の追加の操作を、ちょっと時間置いて実行する
例
<select id="list">
<option class="item" value="1">1</option>
<option class="item" value="2">2</option>
<option class="item" value="3">3</option>
</select>
上のようなHTMLに対して jQuery で以下の操作を行う。
const $list = $('#list');
const $items = $('.item');
(() => {
// リストを空にする
$list.empty();
// DOM上には存在しなくなった items を再度 append する
$items.each((index, el) => $list.append(el))
})()
すると開発者ツールの Elements タブでは存在して見えるのに、ブラウザ上では表示されなくなる。 <select>
要素をクリックしても何も出てこなかったり、2, 3 だけ出てきたりして不審な挙動を起こす。
jQuery を使ってなくても以下のようにやると再現する。 jQuery append も内部で document.createDocumentFragment()
をつかってるので fragment の挙動がなんかおかしいっぽい。
const list = document.querySelector('#list');
const items = document.querySelectorAll('.item');
const append = (parent, child) => {
const fragment = document.createDocumentFragment();
fragment.appendChild(child);
parent.appendChild(fragment);
}
(() => {
// リストを空にする
list.innerHTML = '';
// DOM上には存在しなくなった items を再度 append する
items.forEach( item => append(list, item) );
)();
解決策
どうやら appendChild
したあとにレイアウト(リフロー)が起きてないっぽい。なのでリフローすればよい。見えなくなった要素の高さを取得したり、開発者ツールでハイライトしたりするとリフローが起きて表示される。
もしくは DOM を使い回さない。適宜 createElement
などする。
// 高さを取得したり
item.offsetHeight
// jQueryならcloneしたりで要素を作り直してやるとか
$items.each((index, el) => $list.append($(el).clone()))