7
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-24

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

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

                     動作イメージ

037dccf322add94d86b341748e3fa647.gif

前提条件

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

実装要件

- サーバー
  1. クリックしたカテゴリーに紐づく商品を一覧表示する。
  2. 親カテゴリー、もしくは子カテゴリーがクリックされたら、紐づく子カテゴリ、孫カテゴリーの商品も全て一覧表示する。
- マークアップ
  1. ヘッダーのカテゴリーにマウスホバーすると親カテゴリー一覧が表示される。
  2. リストのうちマウスホバーしているカテゴリーに紐づく、子カテゴリーが右側に表示される。(子カテゴリーは孫が)
  3. マウスホバーしているカテゴリは赤くなる。
  4. マウスホバーが外れても、赤く表示されたままである。
  5. カテゴリーからマウスが外れたら全て非表示となる。

実装方針

本機能を実装するあたり、大まかに以下の2つの方法を検討しました。
(便宜上、一番左のリストを親、その右を子、さらにその左のリストを孫と表現します)

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

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

構想イメージ(動作の流れ)

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

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

af0c2fd0daad73876f0633b76729235f.gif

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

df78cc8338096a250c11fa8470c3b2ac.gif

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

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

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

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

解決策

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

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

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

検討

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

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

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

  1. 非同期通信開始時にajaxの返却値である jqxhrオブジェクト を変数に格納する。
    (jqchrオブジェクトとは、XMLHttpRequestオブジェクトをjQueryがより使いやすい形に拡張したもの。XMLHttpRequestオブジェクトとは。。。イマイチ理解できませんでした。なんとなくですが、非同期通信を開始した際に、必要な各種データが返却値として格納されいるもの、、、みたいな感じですかね、、、検証から中身を確認したところ
    5ffac2f1ab891ad7c065c0574b8ce300.png
    jsonとして、返されるデーターをはじめ、様々なデーターが格納されいるようです。statusやstatusTextも、ここに格納されているのですね)

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

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

のように記述することにより、jqxhrが存在する時は、abortメソッドによりすでにより、実行中の通信を中断することができます。
参考 https://sutara-lumpur.hatenadiary.org/entry/20090131/1233365972

ちなみに、非通信中のjqxhtの中身はを、検証で確認したところ
ef545592ea7906a6960100c9b1b0ae08.png
となっており、何も格納されていません。

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

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

マークアップについて

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

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

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

すると目的の挙動が達成しました。

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

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

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");
        // マウスが入った親のカスタムデータ(親なら1、子なら2、孫なら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
.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
7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5