多階層カテゴリの検索ウインドウを作る

##作るもの
[![Image from Gyazo](https://i.gyazo.com/e62c05c6108015f1fe7be61b2f171d1e.gif)](https://gyazo.com/e62c05c6108015f1fe7be61b2f171d1e)


カテゴリ検索のウインドウを作ります。
適切な呼び方がよくわからなかったのですが、ベロベロリストと個人的に呼んでいました。


調べたらあんまり情報がなかったので、少しでもお役に立てば...

今回はカテゴリの作成方法（扱う方法）、ウインドウの作成方法を概観していこうと思います。
使用するgemの導入方などについては、リンク先の資料を参照してください。

#### バージョン情報です！
ruby 2.5.1
Rails 5.2.3

## 多階層のカテゴリ
今回作成するウインドウでは、３段階のカテゴリがあります。
説明しやすいように、親、子、孫という風に名前をつけます。

この分類を扱いやすくするためにancestryというgemを使用しました。

[ancestryのgithub](https://github.com/stefankroes/ancestry)
[参考にさせていただいた記事](https://qiita.com/Sotq_17/items/120256209993fb05ebac)

多階層カテゴリの仕組みがどのようなものか、順を追って説明したいと思います。

###ただの分類リスト
まず、分類するときのグループ名に使えそうな名前のリストを用意しました。
ただ無秩序に並べてみたものです。

|id|name|
|:------:|:----:|
|1|食べ物|
|2|飲み物| 
|3|牛肉| 
|4|お肉| 
|5|ソフトドリンク| 
|６|お酒| 
|7|ビール|

これを３段階のカテゴリーにするとしたらどうでしょう。

食べもの　＞　お肉　＞　牛肉
飲み物　＞　ソフトドリンク　＞　（何かソフトドリンクの種類）
飲み物　＞　お酒　＞　ビール

このような形に分類できるのではないでしょうか。
そしてこれを以下のように、、、

・parentsテーブル

|id|name|
|:------:|:----:|
|1|食べ物|
|2|飲み物| 

・childrenテーブル

|id|name|parent_id
|:------:|:----:|:----:|
|1|お肉|１
|2|ソフトドリンク|２
|3|お酒|２

・grand_childrenテーブル

|id|name|child_id
|:------:|:----:|:----:|
|1|牛肉|１
|2|ビール|３

　
３つのテーブルに分けてアソシエーションを組んで管理するのは非常にめんどくさそうです。

##ancestoryの仕組み

|id|name|ancestry|
|:------:|:----:|:----:|
|1|食べ物||
|2|飲み物| |
|３|お肉|1
|４|ソフトドリンク|2
|５|お酒|2
|6|牛肉|1/3
|7|ビール|2/5

こんな風に、ancestryは親も子も孫も、一つのテーブルの中で管理します。
ancestryというカラムの中にある数字が自分の属するカテゴリを示しています。
3のお肉であれば１の食べ物のカテゴリーに属している、７のビールであれば2の飲み物カテゴリーと、それに属する５のお酒というカテゴリに属している、ということです。

```ruby:seed.rb
lady = Category.create(name:"レディース")

lady_tops = lady.children.create(name:"トップス")
lady_tops.children.create([{name:"Tシャツ/カットソー(半袖/袖なし)"}, {name:"Tシャツ/カットソー(七分/長袖)"},{name:"シャツ/ブラウス（半袖/袖なし）"},
                           {name:"シャツ/ブラウス（七分/長袖）"},{name:"ポロシャツ"},{name:"キャミソール"},{name:"タンクトップ"},{name:"ホルターネック"},
                           {name:"ニット/セーター"},{name:"チュニック"},{name:"カーディガン/ボレロ"},{name:"アンサンブル"},
                           {name:"ベスト/ジレ"},{name:"パーカー"}])
```

こんな風に、ancestryを導入しますとchildrenという記述で子カテゴリを作ることができますし、さらに子のchildrenで孫も作り出すことができます。非常に便利です。
（Categoryというモデルを作成しました）

上記のseedを入れるだけで以下のようになります。


|id|name|ancestry|
|:------:|:----:|:----:|
|1|	レディース	　|NULL	
|2|	トップス　　|	1	
|3|	Tシャツ/カットソー(半袖/袖なし)|		1/2	
4|	Tシャツ/カットソー(七分/長袖)	|	1/2	
5|	シャツ/ブラウス（半袖/袖なし)   |  1/2	
6|	シャツ/ブラウス（七分/長袖）	|	1/2	
7|	ポロシャツ		|1/2	
8|	キャミソール	|	1/2	
9|	タンクトップ	|	1/2	
10|	ホルターネック |1/2	
11|	ニット/セーター	|	1/2	
12|	チュニック		|1/2	
13|	カーディガン/ボレロ	|	1/2	
14|	アンサンブル	|	1/2	
15|	ベスト/ジレ	|	1/2
16|	パーカー	|	1/2	

ファッションに疎い私が手作業で分類していたら発狂していたことでしょう。

##親カテゴリを持ってこよう
全てのseedを入れ終わり、親カテゴリを取得する場合には、ancestryのカラムがnillのところを持ってくれば良いですし、なんと、childrenで子要素を取得することができるようになります。


```ruby:application_controller.rb
def set_parents
    @parents = Category.where(ancestry: nil)
end
```

このような形で親要素を取得することができます。よく使うので、application_controllerに記述しておきました。before_actionで呼び出して、カテゴリを使いたい時にすぐ使えるようにセットしちゃいましょう。

```ruby:window.haml
.category_list
  .parents_list
    - @parents.each do |parent|
      = link_to "#{parent.name}", category_path(parent), class: "parent_category",id: "#{parent.id}"
  .children_list
  .grand_children_list
```

このような形でウインドウのhamlを用意しておきまして、子要素のリスト、孫要素のリストにjsでhtmlを足していこうという発想です。（sassは別途あてています）

##非同期通信
どの親カテゴリにの上にマウスがいるのか、それに属する子カテゴリや孫カテゴリを取得して、追加します。
コメントで解説をつけておきましたので、ご覧ください！


```javascript:search.js
$(function() {
// 子カテゴリーを追加するための処理です。
  function buildChildHTML(child){
    var html =`<a class="child_category" id="${child.id}" 
                href="/category/${child.id}">${child.name}</a>`;
    return html;
  }

  $(".parent_category").on("mouseover", function() {
    var id = this.id//どのリンクにマウスが乗ってるのか取得します
    $(".now-selected-red").removeClass("now-selected-red")//赤色のcssのためです
    $('#' + id).addClass("now-selected-red");//赤色のcssのためです
    $(".child_category").remove();//一旦出ている子カテゴリ消します！
    $(".grand_child_category").remove();//孫、てめえもだ！
    $.ajax({
      type: 'GET',
      url: '/category/new',//とりあえずここでは、newアクションに飛ばしてます
      data: {parent_id: id},//どの親の要素かを送ります　params[:parent_id]で送られる
      dataType: 'json'
    }).done(function(children) {
      children.forEach(function (child) {//帰ってきた子カテゴリー（配列）
        var html = buildChildHTML(child);//HTMLにして
        $(".children_list").append(html);//リストに追加します
      })
    });
  });

  // 孫カテゴリを追加する処理です　基本的に子要素と同じです！
  function buildGrandChildHTML(child){
    var html =`<a class="grand_child_category" id="${child.id}"
               href="/category/${child.id}">${child.name}</a>`;
    return html;
  }

  $(document).on("mouseover", ".child_category", function () {//子カテゴリーのリストは動的に追加されたHTMLのため
    var id = this.id
    $(".now-selected-gray").removeClass("now-selected-gray");//灰色のcssのため
    $('#' + id).addClass("now-selected-gray");//灰色のcssのため
    $.ajax({
      type: 'GET',
      url: '/category/new',
      data: {parent_id: id},
      dataType: 'json'
    }).done(function(children) {
      children.forEach(function (child) {
        var html = buildGrandChildHTML(child);
        $(".grand_children_list").append(html);
      })
      $(document).on("mouseover", ".child_category", function () {
        $(".grand_child_category").remove();
      });
    });
  });  
});
```

```ruby:category_controller.rb
 def new
    @children = Category.find(params[:parent_id]).children
    respond_to do |format|
      format.html
      format.json
    end
  end
```
childrenで子カテゴリを持ってこられます。（もし子の子なら孫カテゴリ）

公式のREAD MEにはたくさんのメソッドが乗っています！
[ancestryのgithub](https://github.com/stefankroes/ancestry)


```ruby:new.json.jbuilder
json.array! @children do |child|
  json.id child.id
  json.name child.name
end
```

複数子、孫、要素が帰ってきますので、このように処理する必要があります。

##まとめ
思った以上に簡単にカテゴリ検索の中と外を作ることができました。
カテゴリーテーブルの中身さえ変えれば良いので、項目の追加も楽チンですね！
何かのお役に立っていれば幸いです。ありがとうございました！