ReactやVue.jsといった新規フレームワークが席捲する中で、あえてjQueryを採り上げます。その理由は、detach
があまりに便利すぎるのに、全くその凄さが知られていないと感じたからです。
detachってどんなメソッド?
detach()メソッドはDOM要素を削除するremove()やempty()によく似た働きをするメソッドであり、まずはその3つを比べてみます。
<script src="remove.js"></script>
<script src="empty.js"></script>
<script src="detach.js"></script>
<body>
<select id="sel">
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
<option class="sanyo">新神戸</option>
</select>
</body>
remove()の場合
removeメソッドはセレクタに対し、そのセレクタが持つ子要素すべてを削除します。
$(function(){
$('#sel').remove(); //$selの子要素が全て削除される
})
/* このようになる
<select id="sel">
</select>
*/
empty()の場合
emptyメソッドはセレクタに対し、そのセレクタと同階層のDOM要素に対し、削除します。また、optionメソッドに対して用いるとvalueの値を空っぽにするだけなので、空っぽになったプルダウンメニューが表示されてしまいます。
$(function(){
//$selの子要素optionのうち、クラス名がsanyoのものだけ削除される
$('#sel').find('option').filter('.sanyo').empty();
})
/* このようになる
<select id="sel">
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
<option></option>
</select>
*/
この2つを踏まえて、detachを見てみます
detach()の場合
$(function(){
//$selの子要素optionのうち、クラス名がsanyoのものだけ削除される
$('#sel').find('option').filter('.sanyo').detach();
})
/* このようになる
<select id="sel">
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
</select>
*/
これだけだと、empty要素と相違ないですが、detach要素は削除されたdom要素をオブジェクトとして保持することができるので、一度削除したデータを再度挿入することができます。
$(function(){
//$selの子要素optionのうち、クラス名がsanyoのものだけ削除される
var detached = $('#sel').find('option').filter('.sanyo').detach();
detached.prependTo('#sel'); //子要素の先頭に追加
})
/* このようになる
<select id="sel">
<option class="sanyo">新神戸</option>
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
</select>
*/
しかも、このdetachで保持されたDOM要素は複数にも対応しているので、このようなプルダウンメニューでも有効です。
<script src="detach_multi.js"></script>
<body>
<select id="sel">
<option> --駅を選択-- </option>
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
<option class="sanyo">新神戸</option>
<option class="sanyo">岡山</option>
<option class="sanyo">広島</option>
</select>
</body>
$(function(){
//$selの子要素optionのうち、クラス名がsanyoのものだけ削除される
var detached = $('#sel').find('option').filter('.sanyo').detach();
detached.prependTo('#sel'); //子要素の先頭に追加
})
/* このようになる
<select id="sel">
<option class="sanyo">新神戸</option>
<option class="sanyo">岡山</option>
<option class="sanyo">広島</option>
<option class="tokaido">新横浜</option>
<option class="tokaido">静岡</option>
<option class="tokaido">名古屋</option>
<option class="tokaido">京都</option>
</select>
*/
ここからが本番です
さて、このようにdetachメソッドは削除というより、ブロック・インライン要素をオブジェクト単位で隔離する機能なので、好きなタイミングで出し入れできるのが魅力です。しかもイベントハンドラとキャッシュデータを保持するという特長があります。なので、データを維持した状態のまま書き換えることができるという、いわばReactやVue.jsのようなことができるので、それを使って簡単に階層プルダウンが作れます。ポイントはoptionタグにはclass属性を入れていますが、クラス属性は複数指定が可能なので、それに合わせて要素を出し入れできるようになります。
<script src="detach_multi.js"></script>
<body>
<select id="sel_line">
<option> --路線を選択-- </option>
<option value="tohoku">東北新幹線</option>
<option value="joetsu">上越新幹線</option>
<option value="tokaido">東海道新幹線</option>
<option value="sanyo">山陽新幹線</option>
<option value="kyushu">九州新幹線</option>
</select>
<!-- クラス名は路線の値に紐付ける(複数可) -->
<select id="sel_station">
<option> --駅を選択-- </option>
<option class="tohoku">仙台</option>
<option class="tohoku joetsu">大宮</option>
<option class="tohoku joetsu tokaido">東京</option>
<option class="joetsu">新潟</option>
<option class="tokaido">名古屋</option>
<option class="tokaido sanyo">新大阪</option>
<option class="sanyo">広島</option>
<option class="sanyo kyushu">博多</option>
<option class="kyushu">熊本</option>
</select>
</body>
let sel_line; //選択された路線
var detached //一時的に分離されたDOM要素を格納
$(function(){
$("#sel_line").each(function(){
$(this).change(function(){
sel_line = $(this).val();
//分離されたDOM要素を戻し、先頭にフォーカス
$("#sel_station").prop("disabled",false).find(':first').after(detached).val("");
if(sel_line != ""){
//先頭と選択されたクラス要素以外を分離
detached = $('#sel_station').find('option').not(':first').not("."+sel_line).detach();
}else{
//先頭以外の全要素を分離
detached = $('#sel_station').find('option').not(':first').detach();
$("#sel_station").prop("disabled",true);
}
});
})
})
この方法を使えば、クラス名で制御しているために、どんなに属性が複合していても対応できます。そして、いちいちDOM要素を作成しないので、動作が非常に速くなります。ただ、複数のクラス要素を用いた場合、順番が入れ違いになることがあるので(appendとbeforeメソッドを使い分けて振り分け可能ですが)、順番を厳格にしたい場合は次に挙げるやり方を推奨します。
全選択を入れたい場合
もし、最初の路線選択のプルダウンで、全選択を入れたい場合は、一度全要素を隔離したDOM要素を作っておくといいでしょう。detachはそれぞれのメソッド実行時ごとに、DOM要素が保持されるので、複数の格納用変数を作っても混同することはありません。
なお、先ほどと同じように各路線の選択ごとにdetachで呼び出すと、appendメソッドを実行するたびに順番が崩れてしまいますので、上記のパターンでもこっちの方がいいかも知れません。
<script src="detach_multi.js"></script>
<body>
<select id="sel_line">
<option> --路線を選択-- </option>
<option value="all">全路線</option>
<option value="tohoku">東北新幹線</option>
<option value="tokaido">東海道新幹線</option>
<option value="sanyo">山陽新幹線</option>
<option value="kyushu">九州新幹線</option>
</select>
<!-- クラス名は路線の値に紐付ける(複数可) -->
<select id="sel_station">
<option> --駅を選択-- </option>
<option class="tohoku">大宮</option>
<option class="tohoku tokaido">東京</option>
<option class="tokaido">名古屋</option>
<option class="tokaido sanyo">新大阪</option>
<option class="sanyo">広島</option>
<option class="sanyo kyushu">博多</option>
<option class="kyushu">熊本</option>
</select>
</body>
let sel_line; //選択された路線
const detached_all = $('#sel_station').find('option').not(':first').detach(); //全要素を格納
$(function(){
$("#sel_line").each(function(){
$(this).change(function(){
sel_line = $(this).val();
//予め隔離させた全要素を選択可能にする(一度削除してから入れ直す)
$('#sel_station').not(':first').remove().append(detached_all);
if(sel_line == "all"){
$('#sel_station').prop("disabled",false).val("");
}else{
if(sel_line != ""){
//先頭と選択されたクラス要素以外を削除
$('#sel_station').find('option').not(':first').not("."+sel_line).detach().prop("disabled",false);
}else{
//先頭以外の全要素を削除
$('#sel_station').find('option').not(':first').detach().prop("disabled",true);
}
}
});
})
})
応用1:多階層プルダウン
また、クラス名を取得するだけの簡単な仕組みなのでn階層以上のプルダウンメニューを簡単に連動させることもできます(共通の制御用関数を用いるだけ)し、工夫次第では2つ以上の選択条件に対応した階層プルダウンなども作ることができます。下の例では4階層ですが、理屈上は何階層でもメモリが許す限りは可能です(10階層とかもテストしてみましたが、OKでした)。
無論、リストボックスにも応用できます(実装実績あり)。
<script src="detach_multi3.js"></script>
<body>
<select id="sel_line" class="sel">
<option> --路線を選択-- </option>
<option value="tohoku">東北新幹線</option>
<option value="joetsu">上越新幹線</option>
<option value="tokaido">東海道新幹線</option>
<option value="sanyo">山陽新幹線</option>
<option value="kyushu">九州新幹線</option>
</select>
<select id="sel_station" class="sel">
<option> --駅を選択-- </option>
<option class="tohoku" value="sendai">仙台</option>
<option class="tohoku joetsu" value="saitama">大宮</option>
<option class="tohoku joetsu tokaido" value="tokyo">東京</option>
<option class="joetsu" value="niigata">新潟</option>
<option class="tokaido" value="nagoya">名古屋</option>
<option class="tokaido sanyo" value="osaka">新大阪</option>
<option class="sanyo" value="hiroshima">広島</option>
<option class="sanyo kyushu" value="fukuoka">博多</option>
<option class="kyushu" value="kumamoto">熊本</option>
</select>
<select id ="sel_area" class="sel">
<option> --エリアを選択-- </option>
<option class="sendai" value="ichibancho">一番町</option>
<option class="tokyo" value="marunouchi">大手町・丸の内</option>
<option class="tokyo" value="shinjuku">新宿</option>
<option class="tokyo" value="shibuya">渋谷</option>
<option class="tokyo" value="ikebukuro">池袋</option>
<option class="niigata" value="furumachi">古町</option>
<option class="niigata" value="bandai">万代</option>
<option class="nagoya" value="sakae">栄・伏見</option>
<option class="nagoya" value="meieki">名駅</option>
<option class="osaka" value="umeda">キタ(梅田・中之島)</option>
<option class="osaka" value="nanba">ミナミ(難波・本町)</option>
<option class="osaka" value="tennoji">天王寺・阿倍野</option>
<option class="osaka" value="osaka">大阪市内その他</option>
<option class="hiroshima" value="hiroshima">駅前</option>
<option class="fukuoka" value="hakata">博多</option>
<option class="fukuoka" value="tenjin">天神・中洲</option>
</select>
<select id ="sel_hotel" class="sel">
<option> --ホテルを選択-- </option>
<option class="shinjuku osaka hakata">ハイアットリージェンシー</option>
<option class="ichibancho shibuya umeda">ウェスティン</option>
<option class="tokyo meieki tennoji">マリオット</option>
<option class="marunouchi umeda">コンラッド</option>
<option class="marunouchi umeda">リッツ・カールトン</option>
<option class="marunouchi tennoji hiroshima">シェラトン</option>
<option class="shinjuku umeda hiroshima">リーガロイヤル</option>
</select>
</body>
let sel_line; //選択された路線
$(function(){
let sel_parent_cls;
//各オプションの退避(親プルダウン以外のオプションを全部隔離しておく)
const sel_data = {};
$(".sel").not(":first").each(function(){
let sel_id = $(this).attr("id");
let sel_value = $(this).find('option').not(':first').detach();
sel_data[sel_id] = sel_value;
});
//全要素を格納
$(".sel").each(function(){
$(this).change(function(){
sel_parent = $(this).attr("id");
$(this).nextAll("select").find(":first").prop("selected",true).siblings().detach();
sel_child = $(this).next("select").attr("id");
//テーブルタグに収まっている場合
//sel_child = $(this).closest("tr").next().find("select").attr("id");
changePulldownList(sel_parent,sel_child);
})
})
//プルダウンを制御
changePulldownList = function(sel_parent,sel_child){
sel_parent_cls = $('#'+sel_parent).find('option:selected').val();
//予め隔離させた全要素を選択可能にする(一度削除してから入れ直す)
$('#'+sel_child).append(sel_data[sel_child]);
if(sel_parent_cls == "all"){
$('#'+sel_child).prop("disabled",false).val("");
}else{
if(sel_parent_cls != ""){
//先頭と選択されたクラス要素以外を削除
$('#'+sel_child).find('option').not(':first').not("."+sel_parent_cls).detach().prop("disabled",false);
}else{
//先頭以外の全要素を削除
$('#'+sel_child).find('option').not(':first').detach().prop("disabled",true);
}
}
}
})
応用2:絞り込み機能付きプルダウン
絞り込み機能付きのプルダウンを実装する際にも、このdetachメソッドを用いると、処理が非常に高速になります。プルダウンの候補が膨大、かつselect2などのプラグインが重くて実用化できないときは重宝します。
let sel;
let opt;
let len;
//事前準備
let detached_options = $("#sel").find("option").not(":first").detach();
$("#sel").append(detached_options);
//検索結果にしたがい、該当しないものを除去する
selOptions = function(word){
opt = $("#sel").find("option");
len = opt.length;
for(let i = 1; i < len ; i++){
sel = opt.eq(i).text();
if(sel.indexOf(word) === -1){
opt.eq(i).detach(); //該当しないプルダウンを除外(0以外を条件にすると前方一致となる)
}
}
}
//検索ワード欄
$("#word").on("mouseenter",function(){
$("#sel").append(detached_options); //検索欄にフォーカスされた段階でオプションタグ復帰
})
$("#word").on("mouseleave",function(){
let word = $(this).val(); //再度検索欄からフォーカスが外れた段階で絞り込み
if(word != ""){
selOptions(word);
}
})
こうすれば、検索ワード欄に挿入した文字にしたがい、一致するものだけをプルダウンメニューから引き出すことができ、一致しないプルダウンを除外する減算方法を用いることで、検索を高速化できます(Ajaxなどを用いて検索候補からタグを逐一作成する場合と比較しても、比べ物にならないほど高速で、PCのスペックにもよりますが、10000件以上のプルダウンメニューに対しても瞬時に絞り込みできます)。注意点としては、連動プルダウンの場合はremoveメソッドを用いて、逐一初期化していましたが、この場合だとremoveメソッドを入れてしまうと、プルダウンメニューが除外されてしまいます。
また、このdetachメソッドは、オブジェクトを同期させているわけじゃないのでループを回している最中に一致しないoptionタグを除外しても、インデックス番号がずれない(VueやReactでこれをやると大変なことになります)という優れた特長があります。なので、上記の除外方法を用いるだけで、検索にヒットするoptionタグだけを残すことができます。次に検索する場合は、またdetachによって隔離された全optionタグを復帰させているので、検索条件をやり直すことができます。
JSONなどの軽量化データを用いて予めインポートしておくと、大量のデータに対してもそこまで遅延なく操作できます。
※detachメソッドは変数に退避させなかった場合は、そのまま消去される利点もあります。
【追記】
detachで隔離されたoptionタグを的確に差し戻すためには、mouseenterメソッドで削除されたオプションタグを復帰、mouseleaveメソッドで絞り込みと、段階的に処理すると確実のようです。
ほかにも…
つまり、detachはイベントを保持したままあらゆるブロック要素を出し入れできるので、テーブルタグ<tr>
や<td>
要素といった行列要素を欲しい分だけ表示したり、<dl>
要素のうち、<dt>
要素や<dd>
要素だけを、再作成することなく表示したり、データを随時差し替えたり、ページング機能を作成したりといった擬似的な双方向バインディングっぽいことができたりします。