Help us understand the problem. What is going on with this article?

jQueryのdetachが超便利なのに、その力が知られていない

ReactやVue.jsといった新規フレームワークが席捲する中で、あえてjQueryを採り上げます。その理由は、detachがあまりに便利すぎるのに、全くその凄さが知られていないと感じたからです。

detachってどんなメソッド?

detach()メソッドはDOM要素を削除するremove()やempty()によく似た働きをするメソッドであり、まずはその3つを比べてみます。

single.html
<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メソッドはセレクタに対し、そのセレクタが持つ子要素すべてを削除します。

remove.js
   $(function(){
       $('#sel').remove(); //$selの子要素が全て削除される
   })

/* このようになる
<select id="sel">
</select>
*/

empty()の場合

emptyメソッドはセレクタに対し、そのセレクタと同階層のDOM要素に対し、削除します。また、optionメソッドに対して用いるとvalueの値を空っぽにするだけなので、空っぽになったプルダウンメニューが表示されてしまいます。

empty.js
   $(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()の場合

detach.js
   $(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要素をオブジェクトとして保持することができるので、一度削除したデータを再度挿入することができます。

detach.js
   $(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要素は複数にも対応しているので、このようなプルダウンメニューでも有効です。

multiple.html
<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>
detach.js
   $(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属性を入れていますが、クラス属性は複数指定が可能なので、それに合わせて要素を出し入れできるようになります。

multiple.html
<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>
detach.js
   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);
           }
          });
       })
   })

この方法を使えば、optionタグにおけるdisplay属性で可視、不可視の切換が効かないIE11でも対応している上に、クラス名で制御しているために、どんなに属性が複合していても対応できます。そして、いちいちDOM要素を作成しないので、動作が非常に速くなります。ただ、複数のクラス要素を用いた場合、順番が入れ違いになることがあるので(appendとbeforeメソッドを使い分けて振り分け可能ですが)、順番を厳格にしたい場合は次に挙げるやり方を推奨します。

全選択を入れたい場合

もし、最初の路線選択のプルダウンで、全選択を入れたい場合は、一度全要素を隔離したDOM要素を作っておくといいでしょう。detachはそれぞれのメソッド実行時ごとに、DOM要素が保持されるので、複数の格納用変数を作っても混同することはありません。

なお、先ほどと同じように各路線の選択ごとにdetachで呼び出すと、appendメソッドを実行するたびに順番が崩れてしまいますので、上記のパターンでもこっちの方がいいかも知れません。

multiple2.html
<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>
detach.js
   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);
               }
           }
          });
       })
   })

多階層プルダウンも関数一つで連動

また、クラス名を取得するだけの簡単な仕組みなのでn階層以上のプルダウンメニューを簡単に連動させることもできます(共通の制御用関数を用いるだけ)し、工夫次第では2つ以上の選択条件に対応した階層プルダウンなども作ることができます。下の例では4階層ですが、理屈上は何階層でもメモリが許す限りは可能です(10階層とかもテストしてみましたが、OKでした)。

multiple3.html
<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>
multi3.js
let sel_line; //選択された路線
   $(function(){
       let sel_parent_cls;
       //各オプションの退避
       const sel_data = {
       "sel_station":$('#sel_station').find('option').not(':first').detach(), 
       "sel_area":$('#sel_area').find('option').not(':first').detach(),
       "sel_hotel":$('#sel_hotel').find('option').not(':first').detach(),
       };
        //全要素を格納

       $(".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");
              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);
               }
           }
       }
   })

ほかにも…

つまり、detachはイベントを保持したままあらゆるブロック要素を出し入れできるので、テーブルタグ<tr><td>要素といった行列要素を欲しい分だけ表示したり、<dl>要素のうち、<dt>要素や<dd>要素だけを、再作成することなく表示したり、データを随時差し替えたりといった擬似的な双方向バインディングっぽいことができたりします。

BRSF
職業、PG・SE・DBエンジニア。オープン環境のwebプログラムをメインにシステム構築担当。使用言語はPHP(cakePHP、Laravel含)jQuery、JavaScript、ExcelVBA、Perl、Ruby、Python。現在Vue、React、Angular強化中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした