カテゴリープルダウン機能の実装


 某プログラミングスクールの最終課題において、カテゴリープルダウン機能(トップページのヘッダーにあるカテゴリーという文字に触れるとプルダウンでカテゴリーが表示される機能)を実装したので、自分のアウトプットのためにも解説していきたいと思います。

早速ですが、プルダウン機能実装の流れは以下の通りとなります。

① カテゴリーの文字に触れる

② jQueryでイベントが発生し、カテゴリーに貼られているリンクを読み取る

③ そのリンクでAjax通信を行い、取得したカテゴリーID(0の場合は親要素)に応じて子要素をデータベースから取得し、json形式で値を戻す。

④ 後は、取得したデータをHTMLの形に組み直して、カテゴリーの文字の下に付け加えるだけです。

⑤ 子要素に対しても、以上と同様な流れで表示し、孫要素までいくと表示されないようにしています。

これを実現したソースコードは以下の通りとなります。(該当する部分だけ抜き出しています)

_header.html.haml
.header-bottom-left-category
  .header-bottom-left-category-title
    = link_to categories_path ,class:"category_name", data:{category_id:0} do
      %i.fas.fa-list
        カテゴリから探す
  .header-bottom-left-category-field   
categories_pulldown.js
$(function(){

  // プルダウンのHTMLを生成
  function buildHTML(categories){

    var link_tag, outline = $("<ul>");
    var link_class =$(".header-bottom-left-category-title").find("a").attr("class")
    var base_href = $(".header-bottom-left-category-title").find("a").attr("href") + "/";

    categories.forEach(function(category){
      link_tag = $("<a>", { href: base_href + category.id ,"class":link_class }).text(category.name)
      link_tag = $("<li>").append(link_tag)
      outline.append(link_tag)
    })

    return outline

  }

  // カーソルが触れたときに起動
  $(document).on({

    // カーソルが乗ったときに起動
    'mouseenter' : function(enter_event) {

      // mouseoverのデフォルト動作をクリアする
      enter_event.preventDefault();

      // カテゴリーのIDを取得できた場合には、子要素を取得
      $.ajax({
        url: $(this).attr("href"),
        type: "GET",
        dataType: "json",
        context: this,
        cache: false
      })
      .done(function(categories){

        // 子要素がなければ選択肢を表示しない
        if( categories.length > 0 ){

          // 「カテゴリから探す」以外に触れたら一掃する
          if( $(this).data("category-id") == 0){
            $(".header-bottom-left-category-field").empty();
          }

          // それ以外の場合は自分の後に表示されている子要素を削除する
          else{
            $(this).closest("ul").nextAll().remove()
          }

          // 選択したフォームの下に新たなフォームを追加
          var html = buildHTML(categories)
          $(".header-bottom-left-category-field").append(html)
        }

      })
      .fail(function(error){
        alert(error)
      })
    },

    // カーソルが離れたときに起動
    'mouseleave' : function(leave_event){

      // mouseleaveのデフォルト動作をクリアする
      leave_event.preventDefault();

      // 出る前と後の親要素のインデックスを取得する。
      var old_parent = $(this).closest("ul").index();
      var new_parent = $(leave_event.relatedTarget).closest("ul").index();

      // 子要素へ移る場合はオレンジ色のままにする
      if( old_parent < new_parent ){
        $(this).css("color","orange");
      }

      // 親要素に移る場合には、その先の要素全てを黒にする。
      else if(old_parent > new_parent ){
        $(leave_event.relatedTarget).closest("ul").find("a").css("color","");
      }

    }
  },".category_name");

  // カテゴリーリストからカーソルが離れたらすべて消す
  $(".header-bottom-left-category").mouseleave(function(){
    $(".header-bottom-left-category-field").empty();
    $(".header-bottom-left-category-title").find("a").css("color","");
  })

})
routes.rb
 resources :categories, only: [:index,:show]
categories_controller.rb
class CategoriesController < ApplicationController

  def index

    @categories = Category.where(ancestry:nil)

    respond_to do |format|
      format.html
      format.json{
        render json: @categories
      }
    end 
  end

  def show

    @category = Category.find(params[:id])
    @categories = @category.children

    respond_to do |format|
      format.html
      format.json{
        render json: @categories
      }
    end 

  end

end

_header.scss
 & a {
      color:black;
      padding:10px;
      display: block;
      text-decoration: none;
      &:hover {
        color:orange;
      }
    }
    > div {
      display: flex;
    }
    &-left{
      &-category {
        position: relative;
        &-field {
          position: absolute;
          display: flex;
          background-color:white;
          z-index:1;
          & ul {
            width:200px;
          }
        }
      }
    }

(※)自分の環境に合うようにしてから、使用していただくようお願いします。