【jQuery】【ancestry】を使用した、多階層ツリーメニューの実装

経緯
-
現在絶賛作成中のポートフォリオで、フリマアプリのラクマのような多階層ツリーメニューを実装したいと思い作成。振り返ってみればさほど難しくはなかったけれども、色々躓いたので記載しておこうと思います。

環境
-
ruby 2.6.5
Rails 6.0.3.2
haml使用


完成図
-
![moving image](https://i.gyazo.com/b43b05ac01ec87dc8a9ccfea833e5470.gif)
親のカテゴリー（第１階層）をクリックすると、子のカテゴリー（第2階層）のサブメニューが現れるという仕組み。

手順
-
1、ancestryの導入
2、HTMLで多階層の形を実装
3、jQuery（JavaScript）で、第１階層のカテゴリーをクリックすると、第2階層のサブメニューが表示されるイベントを記載する

※CSSは今回は記載しません、ごめんなさい

ancestryの導入
-
余談ですが、ancestryを導入しなくても、HTMLに記載すれば多階層ツリーメニューの実装自体は出来ます。只、第2階層のサブメニューのカテゴリをクリックして、そのカテゴリの商品一覧を表示させたいとなるとancestryを導入は必須になります。

余計な余談になりましたが、早速ancestryを導入していきましょう！

``` Gemfile.
#追記（一番下が好ましい）
gem 'ancestry'
```
記載が終了したら、
ターミナルで`% bundle install` して `% rails s`忘れずに行いましょう。
<br>
続いてcategoryモデルの作成です。

``` ターミナル.
$ rails g model category
```
この時点で、マイグレーションファイルは作成されていますので、
マイグレーションファイルを編集しましょう。

```2020xxxxxxxx_create_categories.rb
class CreateCategories < ActiveRecord::Migration[6.0]
  def change
    create_table :categories do |t|
      t.string :name, null: false　#これを追記

      t.timestamps
    end
  end
end
```
ターミナルで`rails db:migrate`

さらに、categoryモデルにancestryのカラムを追加したいのでターミナルで以下のコマンドを入力

``` ターミナル.
% rails g migration AddAncestryToCategory ancestry:string:index
```
ターミナルで`rails db:migrate`

続いて、seedにデータ作成をします。
詳しくはこちらの記事を見てみてください。
分かりやすく書いてくれている記事なので、親カテゴリー、子カテゴリーなどの意味が理解出来ると思います。
[【フリマアプリ】ancestryを用いたseedデーター作成について](https://qiita.com/sho_U/items/5e55806afb4915589f5a)

`seeds.rb` にデータを記載していくんですが、今回は僕が作ったものを貼り付けておきますので参考にして見てください。と言ってもメチャクチャ使い回しなんですけど・・・・
大事なのは、`name:xxxxx`のxxxxxの部分なので、ここの名前を実装したいものと、照らし合わせて変更してください。

```db/seed.rb
# パーツ
lady = Category.create(name: "パーツ")
lady_1 = lady.children.create(name: "パーツすべて")
lady_2 = lady.children.create(name: "マフラー")
lady_3 = lady.children.create(name: "エンジン/冷却装置")
lady_4 = lady.children.create(name: "ホイール")
lady_5 = lady.children.create(name: "タイヤ")
lady_6 = lady.children.create(name: "ハンドル")
lady_7 = lady.children.create(name: "ブレーキ")
lady_8 = lady.children.create(name: "外装")
lady_9 = lady.children.create(name: "駆動系")
lady_10 = lady.children.create(name: "電装形")
lady_11 = lady.children.create(name: "その他")

# アクセサリ
men = Category.create(name: "アクセサリ")
men_1 = men.children.create(name: "アクセサリすべて")
men_2 = men.children.create(name: "バイクカバー")
men_3 = men.children.create(name: "ステッカー/デカール")
men_4 = men.children.create(name: "タンクバック")
men_5 = men.children.create(name: "サドルバッグ/サイドバック")
men_6 = men.children.create(name: "シートバック")
men_7 = men.children.create(name: "ツーリングネット/ロープ")
men_8 = men.children.create(name: "鍵/ロック")
men_9 = men.children.create(name: "エアバック")
men_10 = men.children.create(name: "ETC")
men_11 = men.children.create(name: "ナビ")
men_12 = men.children.create(name: "オーディオ")
men_13 = men.children.create(name: "その他")

# バイクウェア
baby_kids = Category.create(name: "バイクウェア")
baby_kids_1 = baby_kids.children.create(name: "バイクウェアすべて")
baby_kids_2 = baby_kids.children.create(name: "ヘルメット/シールド")
baby_kids_3 = baby_kids.children.create(name: "ゴーグル")
baby_kids_4 = baby_kids.children.create(name: "グローブ")
baby_kids_5 = baby_kids.children.create(name: "プロテクター")
baby_kids_6 = baby_kids.children.create(name: "ジャケット")
baby_kids_7 = baby_kids.children.create(name: "革ツナギ")
baby_kids_8 = baby_kids.children.create(name: "雨具")
baby_kids_9 = baby_kids.children.create(name: "靴")
baby_kids_10 = baby_kids.children.create(name: "バック")
baby_kids_11 = baby_kids.children.create(name: "パンツ")
baby_kids_12 = baby_kids.children.create(name: "シャツ")
baby_kids_13 = baby_kids.children.create(name: "フェイスマスク/ネックウォーマー")
baby_kids_14 = baby_kids.children.create(name: "その他")

# メンテナンス
interior_residence_accessory = Category.create(name: "メンテナンス")
interior_residence_accessory_1 = interior_residence_accessory.children.create(name: "メンテナンスすべて")
interior_residence_accessory_2 = interior_residence_accessory.children.create(name: "バッテリー")
interior_residence_accessory_3 = interior_residence_accessory.children.create(name: "オイル")
interior_residence_accessory_4 = interior_residence_accessory.children.create(name: "フィルター")
interior_residence_accessory_5 = interior_residence_accessory.children.create(name: "スタンド")
interior_residence_accessory_6 = interior_residence_accessory.children.create(name: "工具")
interior_residence_accessory_7 = interior_residence_accessory.children.create(name: "その他")

# その他
book_music_game = Category.create(name: "その他")
book_music_game_1 = book_music_game.children.create(name: "すべて")
book_music_game_2 = book_music_game.children.create(name: "ファッション")
book_music_game_3 = book_music_game.children.create(name: "グッズ")
book_music_game_4 = book_music_game.children.create(name: "カタログ")
book_music_game_5 = book_music_game.children.create(name: "マニュアル")
book_music_game_6 = book_music_game.children.create(name: "本/雑誌")
book_music_game_7 = book_music_game.children.create(name: "その他")       
```
ターミナルで `% rails db:seed` でこのデータを反映させます。

Sequel pro（ホットケーキがアイコンのもの）をインストールしている方は、それにデータがちゃんと反映されているか確認して見て下さい。
ちゃんと反映されていたら下記の画像のようになります。
<img width="1000" alt="ファイル名" src="https://i.gyazo.com/aa0f419183ecbe20a83facc7770e580e.png">
`ancestry カラムのデータ中の NULL の所が親カテゴリーです`
コントローラーの入力の所で必要な知識になるので覚えて置いてください。

ちなみに、上記の記事を参考にして貰えば分かりますが、
`lady = Category.create(name: "パーツ")` ←この形の記載が親のカテゴリー（第１階層）
`baby_kids_1 = baby_kids.children.create(name: "バイクウェアすべて")` ←この形の記載が子カテゴリー（第２階層）の記載になります。


もし入力するカラム名などに不備があった場合は以下のコマンドを入力してseed.rbを修正しましょう。
`% rails db:migrate:reset`
修正してもう一度rails db:seed
`% rails db:seed`

最後に、categoryモデルで ancestryを使用できるようにモデルに下記を記載します。

```category.rb
has_ancestry
```
以上で、ancestryの導入は終了です！

<br>
<br>

HTMLで多階層の形を実装
-
続いては、HTMLで多階層の形を実装していきます。
下記のものが、コードになります。


``` index.html.haml
.categories
  .categories--contents
    .categories--title カテゴリから探す
    %ul.category__parent
      - @parents.each do |parent|
        .category-box{id: "parent_category#{parent.id}"}
          %p.parent_category{id: "parent#{parent.id}"}
            = "#{parent.name}"
          %i.fas.fa-angle-down
        %ul.category__child{id: "parent_list#{parent.id}"}
          - parent.children.each do |child|
            .category-box{id: "parent_category#{child.id}"}
              = link_to "#{child.name}", "#", class: "parent_category",id: "child#{child.id}"
              %i.fas.fa-angle-right
```
<br>

続いて、コントローラーの記載です。

```xxxxx_controller.rb
def index
  @parents = Category.where(ancestry: nil)
end
```
先ずは、HTMLでも使っているので、コントローラーの変数の説明から

これは、Categoryテーブルの ancestryカラムの nilのデータ、
要は上記のSequel proの画像を見て貰えればと思いますが、NULLと書かれたデータを
取り出して、parentsという変数に代入しています。
つまり、親カテゴリーのデータを取り出しているという事ですね。
<br>

続いてHTMLの説明ですが、
`- @parents.each do |parent|` 
コントローラーで作った変数parents と eachメソッドを使って、
親カテゴリー（第１階層）のデータを抽出します。

`- parent.children.each do |child|`
これも同様に eachメソッドを使って、
子カテゴリー（第２階層）のデータを抽出します（理屈よりもこの形で覚えた方が早いと思います）

いくつかある`id:"xxxxx"`の部分は、jQueryのイベントの際に、またCSSでのコーティングの際に必要になるので、わざわざidを振っています。

このままでは、子カテゴリー（第２階層）が見えたままなので、display: noneで消しましょう。

```xxxxx.css
.category__child{
  display: none;
}
```

これで、HTMLで多階層の形を実装は終了です！

最後に、jQueryの実装です。
<br>
<br>

jQuery（JavaScript）で、第１階層のカテゴリーをクリックすると、第2階層のサブメニューが表示されるイベントを記載する
-
それでは、最後にイベントを発火させるjQueryの記載をしましょう。
下記のものが、コードになります。

```
$(function(){
  $('#parent_category1').click(function(){
    $("#parent_list1").slideToggle('1000');
  });
  $('#parent_category13').click( function(){
    $("#parent_list13").slideToggle('1000');
  });
  $('#parent_category27').click(function(){
    $("#parent_list27").slideToggle('1000');
  });
  $('#parent_category42').click(function(){
    $("#parent_list42").slideToggle('1000');
  });
  $('#parent_category50').click(function(){
    $("#parent_list50").slideToggle('1000');
  });
});
```

ここで注意なのですが、コード自体はそれ程難しいものではないと思うのですが、
parent_category"1" parent_list"1" などの数字の部分に注意が必要です。

上記の seed.rb をそのままコピペした方は特に問題ないと思いますが、
自分で作成したという方は、恐らくここの数字が変わってくると思いますので、
大変恐縮ですが、ご自身で検証画面を見て、この数字の部分を確認してみてください。
<br>

コードの説明ですが、とても簡単です。
parent_category1（親カテゴリー）の階層をクリックしたら、parent_list1（子カテゴリー）が
表示されるというシンプルなものになっています。

この実装にはslideToggleメソッドを使用しています。
このメソッドは、指定された要素が表示されている時はslideUpで非表示にし、非表示になっている時はslideDownで表示するというメソッドになります。ちなみに、格好の中の数字〔slideToggle('1000')〕は、スライドのスピードです。

このメソッドについて詳しく知りたい方はこちらの記事を参照ください。
[slideToggle()メソッドについて](https://jquery.step-learn.com/contents/j009-slidetoggle.html)
<br>
<br>
これで、全ての実装が完了です！
実装出来なかったなどの不備が有ればご連絡ください！

ありがとうございました。