【フリマアプリ】カテゴリードロップダウンリストの作成

　某スクールにおいて、チーム開発で、フリーマーケットアプリを作成中であり、使用した技術について公開しています。
※初学者のため、ミスや認識違いが多々あると思いますがご了承ください。

# ヘッダーから、ドロップダウンリストを用いてカテゴリーを選択できるように実装した。
　　　　　　　　　　　　　　　　　　　　　**動作イメージ**

![037dccf322add94d86b341748e3fa647.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/d4aa2005-9c3f-fb0a-7371-8d7ed4e79675.gif)

## 前提条件
 ancestryにより、カテゴリーの親から孫まで紐づいている。

## 実装要件
    - サーバー
      1. クリックしたカテゴリーに紐づく商品を一覧表示する。
      2. 親カテゴリー、もしくは子カテゴリーがクリックされたら、紐づく子カテゴリ、孫カテゴリーの商品も全て一覧表示する。
    - マークアップ
      1. ヘッダーのカテゴリーにマウスホバーすると親カテゴリー一覧が表示される。
      2. リストのうちマウスホバーしているカテゴリーに紐づく、子カテゴリーが右側に表示される。（子カテゴリーは孫が）
      3. マウスホバーしているカテゴリは赤くなる。
      4. マウスホバーが外れても、赤く表示されたままである。
      5. カテゴリーからマウスが外れたら全て非表示となる。
      
## 実装方針
本機能を実装するあたり、大まかに以下の２つの方法を検討しました。
（便宜上、一番左のリストを親、その右を子、さらにその左のリストを孫と表現します）

1. 　各々のリンク先を記載した全てのカテゴリーのhtmlを予め、重ねて作成し、display:noneしておき、親が選択された場合、jqueryにて、紐づく子カテゴリーをdisplay:blockし、表示させる。
　　
2. 　ホバーされた親に紐づく子カテゴリーを、非同期でデーターベースから引っ張ってきて、子カテゴリーを組み立てて表示する。

　検討した結果、 **「２」** を採用することにしました。おそらく、１の方がデーターベースを介する通信が不要であり、作成してあるhtmlの非表示を表示に切り替えるだけなため、安定性、速度において優位ではあると思うのですが、非同期通信の学習も兼ねて２を採用することにしました。

## 構想イメージ(動作の流れ）
1. 　ヘッダーのカテゴリーにマウスホバーしたら、親カテゴリーを表示。（親カテゴリーについては、常に同じであるためhtmlで作成しておきdisplay:none から display:blockへの変更）
2. 　親カテゴリーにホバーしたら、紐づく子カテゴリーを全てデーターベースから取得し、子カテゴリーのボックスで、each do で回して、構築し表示する。

## 上記構想で、実装したところとある問題が発生しました。

![af0c2fd0daad73876f0633b76729235f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/173a3e0e-bd8c-732a-85cb-451b28206a46.gif)

このように、ゆっくりマウスを動かしたら問題なく動くのですが、マウスを素早く動かした場合、、、

![df78cc8338096a250c11fa8470c3b2ac.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/e8accf1f-d0ad-9fd6-0323-3a99a610421f.gif)

、、、お気づきになりましたでしょうか？

　マウスが停止しても、まだ子カテゴリーがシャコシャコ動いている！！しかも、停止したマウスのカテゴリーと無関係の子カテゴリーが表示されている！！
　これを、便宜上**「おみくじ状態」**とさせていただきます。なぜこのような、おみくじ状態になるか調べた結果、、、

1. マウスの素早い動きに非同期通信が追いついておらず、マウスが停止した後に遅れて、子カテゴリーが構築されている。
2. 非同期通信は、発火した順番に対するレスポンスの順番は担保されていない。つまり、発火した順ではなく、通信が終わった順にレスポンスされている。よって、最後にマウスホバーした親カテゴリーに紐づく子カテゴリーが表示されるとは限らない。

ということが判明しました。

## 解決策
以上の事を踏まえて、とりあえず以下の２つの解決策が頭をよぎりました。
1. **「マウスはゆっくり動かしてください」**と注意書きを入れる。（いやいや、それは流石に・・・と即、却下）
2. **実装方針1**のhtmlのみで実装する。

　2を採用しようとしたのですが、なんとか非同期通信を用いて実装できないか、いろいろ調べた結果、以下の３つの方法に辿りつきました。

3.マウスがホバーして、非同期通信が発火するまでに、待機時間を設ける。（例：マウスホバー後0.5秒経過した後、非同期通信を開始）
4.非同期通信開始時と、発動とレスポンスの順番の同一性を担保する。
参考記事　http://www.koikikukan.com/archives/2012/10/11-000300.php
5.非同期通信中に、新たに非同期通通信が開始されたら、旧非同期通信は、中断される。

## 検討
３.については最もシンプルに実装できそうだと思ったのですが、マウスがホバーしてからしばらく待機時間があるのはユーザーインターフェイス上あまり好ましくないのでは、と却下。
４.については、順番が実行されるものの、マウスが停止後も子カテゴリーのシャコシャコ状態は改善されないのでは？と却下。
５.については、確かに順番は担保される。（最後にホバーしたカテゴリーに紐づく子がレスポンスされる。）かつ、シャコシャコ状態も回避できそう。

よって**５**を方針として選択しました。
参考記事　http://www.koikikukan.com/archives/2013/12/06-015555.php
参考記事　https://qiita.com/otsukayuhi/items/31ee9a761ce3b978c87a

自分なりに解釈した内容と端的に説明すると、

1. 非同期通信開始時にajaxの返却値である **jqxhrオブジェクト** を変数に格納する。
（jqchrオブジェクトとは、XMLHttpRequestオブジェクトをjQueryがより使いやすい形に拡張したもの。XMLHttpRequestオブジェクトとは。。。イマイチ理解できませんでした。なんとなくですが、非同期通信を開始した際に、必要な各種データが返却値として格納されいるもの、、、みたいな感じですかね、、、検証から中身を確認したところ
<img width="870" alt="5ffac2f1ab891ad7c065c0574b8ce300.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/11032e54-68ad-81d0-f99b-fa8c7579f783.png">
jsonとして、返されるデーターをはじめ、様々なデーターが格納されいるようです。statusやstatusTextも、ここに格納されているのですね）

2. 通信中は、**jqxhrオブジェクト**にデーターが格納されているため、次回非同期通信開始時に、**条件分岐** として

```
　　    if (jqxhr) {
            jqxhr.abort();
        }
```

のように記述することにより、jqxhrが存在する時は、abortメソッドによりすでにより、実行中の通信を中断することができます。
参考　https://sutara-lumpur.hatenadiary.org/entry/20090131/1233365972
      
ちなみに、非通信中のjqxhtの中身はを、検証で確認したところ
<img width="426" alt="ef545592ea7906a6960100c9b1b0ae08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/182325ec-91c9-d2d4-0651-8eb7517d835e.png">
となっており、何も格納されていません。

3 . よって結果的に、最後に通信を開始したレスポンスが、帰ってくるということです。

なんとかサーバーサイドについては実装完了しました。

## マークアップについて

マークアップについては、わりかしスムーズに行き、後は選んだカテゴリーを赤くするだけの段階まできました。
「ふむふむ、cssでマウスホバーを当てて、カーソルが乗ってる部分を赤くすればいいだけね！」と思い、チームメンバーに対し、**デキル男風**に「あと、５分もあれば完成っスね！！」などと吹聴していたところ、とあることに気付き愕然としました。
　上記の動作イメージは某メルカ○を参考にしたんですが、マウスが乗ったカテゴリーが部分が赤くなるのはそうなんですが、そこから子、孫カテゴリーに移動した時に、親カテゴリーは赤くなったままなんですね、、、
　そのことに気づいて、顔面蒼白で冷や汗を垂らしながら震えていると、一つの解決策が突如閃きました！
**「よし！!気づかなかったことにして、シラッと押し通そう!!」**
と思ったのですが、チームメンバーが細かい事にこだわり一生懸命に思考錯誤している姿をみて、思いとどまり「、、、やっぱりもうちょっとかかります、、、」と訂正し、完コピすることにいたしました。

上記のとおり、cssでhover時にbackground-colorを与えると、カーソルが外れると、色が元どおりになります。そこで、考えた事は、**「カーソルが外れた時に、色は戻す。でも、右に外れた時は赤いままにする。」**とういう発想でした。でも、この考えだと、親から子に行って、さらに別の親に戻ると、前回の親は赤いままで、新しくカーソルが乗った親も赤くなるとうことになりそうです。
発想を転換して、カーソルが外れた時の挙動は特に指定せず、（カーソルが外れても赤いまま）、**カーソルが乗った時に、兄弟要素を全て元に戻す**という考えに切り替えました。これだと、目的の実装が可能そうです。
　jqueryにはsiblings()という、同階層の要素を全て取得するメソッドがあるのでこれを用いて、カーソルが乗った後の挙動として以下のように記載しました。

```
 $(this).siblings().removeClass("red");  // 兄弟要素のredクラスを削除。親から子に行った時は削除しない
        $(this).addClass("red");    // 自分を赤くする。親から子に遷移したときに残すよう。
```
すると目的の挙動が達成しました。

これでようやく、目的の実装が全て完了しました。動作確認をしっかり確認した上で、チームメンバーに**「あと、５分もあれば完成っスね！！」**と宣言し、きっちり５分後に**「できました！！」**と報告しました。（何事も余裕を持った準備が大切ですね）

参考までに、jqueryのコードと、htmlを載せておきます。

```javascript:jquery
$(function(){
  let btn = $(".category-btn")
  let category_downlists = $(".category-downlists")
  valGlobal1 = "nextbox";    // メソッドを超えて使用するのでグローバル変数

  btn.hover(      // カテゴリーボタンに乗った時
  function(){
    category_downlists.removeClass("non-show-list").addClass("show-list");
  });      // リストの型枠を表示

  $(".header__inner__main").hover(
    function(){
      category_downlists.removeClass("show-list").addClass("non-show-list");
    // 親リストを非常時にする。
    $(".ones").eq(1).empty();
    // 子を空にする
    $(".ones").eq(2).empty();
    // 孫を空にする
    });
  

  category_downlists.mouseleave(    // カテゴリーダウンリストから外れた時
    function(){
      category_downlists.removeClass("show-list").addClass("non-show-list");
    // 親リストを非常時にする。
    $(".ones").eq(1).empty();
    // 子を空にする
    $(".ones").eq(2).empty();
    // 孫を空にする
    });

    $(".category-downlist").on("mouseenter","#oya",(function () {
      $(".ones").eq(2).empty();    
          // 親要素に乗った場合、孫を消去。孫から親に移動した時、孫が残り続けるのを防ぐ
    }));

      function childrenDownBuild(children){     // 一個一個整形用
        $(".ones").eq(nextBox).empty();     // すでに表示しているものを空にする
        $.each(children,
          function(index,child) {
          let html = 
          `<div class= "category-downlist__in__one" data-id = ${child.id}>
            <a href= "/items/${child.id}/list_from_category">${child.name}</a>
          </div>`
            // json を取り出して表示。idはカテゴリーのid。nameはカテゴリの名前
          $(".ones").eq(nextBox).append(html)      // onesにhtmlを追加する
          });
      };

    let parentIDs = [];
    var jqxhr;
      $(".ones").on("mouseenter",".category-downlist__in__one",(function () {// マウスが一つ一つのブロックに入ったら発動
        // ※hoverメソッドは、動的なクラスに対し指定できないためonメソッドでmouseenterを使用する必要あり！！
        nextBox = $(this).closest(".ones").data("next");
        // マウスが入った親のカスタムデータ（親なら１、子なら２、孫ならnil）を取得

        $(this).siblings().removeClass("red");     // 兄弟要素のredクラスを削除。親から子に行った時は削除しない
        $(this).addClass("red");
        // 自分を赤くする。親から子に遷移したときに残すよう。実際はcssのhoverで実装。（jqueryで実装すると動きがもたつくため）
        if(jqxhr){          // jqxhrが存在したら、abortで中断する。連続送信を避けるため
          jqxhr.abort();
          }
            let parentID = $(this).data('id');        //選択されたカテゴリーのカスタム属性data-id（つまり、そのカテゴリーのid)を格納
            parentIDs.push(parentID);      // hoverの順番を担保するため、取得順にidをparentIDsに格納
              var nextParentID = parentIDs.shift();
              // shiftメソッドで配列の最初の要素を削除。返り値は、削除した要素なので、nextnextParentIDに格納される。
              jqxhr = $.ajax({ // ajax通信の中身をjqxhrに格納する。
                              url: "/items/category_children",  
                              type: "GET",      // サーバに送信する
                              data: {     // nextParentID(parent_id)をparamsに格納しておくる。params[:parent_id]
                              parent_id: nextParentID
                              },
                              dataType: "json",
                              })
                              .done(function (children) {
                                childrenDownBuild(children);
                              })
                              .fail(function (jqxhr,textStatus) { 
                                // abort（中断）した場合、failを処理する仕様なので、alertを出現させせないために、textStatus === 'abort'の場合は、何もしない処理をさせる。
                                if (textStatus === 'abort'){return;}
                               alert("カテゴリー取得に失敗しました");
                              });
        }));
});
```

```haml:haml
.category-downlists.non-show-list
  .category-downlist 
    .category-downlist__in#oya
      .ones{"data-next" => "1"}
        - Category.roots.each do |category|
          .category-downlist__in__one{"data-id" => "#{category.id}"}
            -# カテゴリーのidをカスタム属性のdata-idに格納
            = link_to category.name, list_from_category_item_path(category.id)
    .category-downlist__in
      .ones{"data-next" => "2"}
    .category-downlist__in
      .ones
 ```