商品出品画面を作る③ancestry(多階層カテゴリー)

#ancestryを使用して多階層のカテゴリーを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565886/7c5eda29-5180-61e0-10fc-c12a403692b1.png)
こんな感じの入力欄を作ります。
[商品出品画面を作る①へ](https://qiita.com/m-kawakami/items/8617ac3d9b3528ad0326)
#導入する
＊先にカテゴリーテーブルを作成しておく必要があるので注意
Gemを導入する

```ruby:Gemfile
gem 'ancestry'
```
```ruby:ターミナル
bundle install
rails g migration add_ancestry_to_category
#カテゴリーテーブルにancestryを追加
rails db:migrate
```

#モデルを編集する
```ruby:models/item.rb
belongs_to :category
```
```ruby:models/category.rb
has_many :items
has_ancestry
```
#カテゴリーのデータを用意する
今回はseedファイルを使用します

```ruby:db/seed.rb
# レディース
ladies_child_array = ['トップス','ジャケット/アウター','パンツ','スカート','ワンピース','靴','ルームウェア/パジャマ','レッグウェア','帽子','バッグ','アクセサリー','ヘアアクセサリー','小物','時計']
#２階層目(子)のカテゴーを準備
ladies_grandchild_array = [
  #３階層目(孫)のカテゴーを２重配列で準備
  ['Tシャツ/カットソー(半袖/袖なし)','Tシャツ/カットソー(七分/長袖)','シャツ/ブラウス(半袖/袖なし)','シャツ/ブラウス(七分/長袖)','ポロシャツ','キャミソール','タンクトップ','ホルターネック','ニット/セーター','チュニック','カーディガン/ボレロ','アンサンブル','ベスト/ジレ','パーカー'],
  ['テーラードジャケット','ノーカラージャケット','Gジャン/デニムジャケット','レザージャケット','ダウンジャケット','ライダースジャケット','ミリタリージャケット','ダウンベスト','ジャンパー/ブルゾン','ポンチョ','ロングコート','トレンチコート','ダッフルコート','ピーコート'],
  ['デニム/ジーンズ','ショートパンツ','カジュアルパンツ','ハーフパンツ','チノパン','ワークパンツ/カーゴパンツ','クロップドパンツ','サロペット/オーバーオール','オールインワン','サルエルパンツ','ガウチョパンツ','その他'],
  ['ミニスカート','ひざ丈スカート','ロングスカート','キュロット','その他'],
  ['ミニワンピース','ひざ丈ワンピース','ロングワンピース','その他'],
  ['ハイヒール/パンプス','ブーツ','サンダル','スニーカー','ミュール','モカシン','ローファー/革靴','フラットシューズ/バレエシューズ','長靴/レインシューズ','その他'],
  ['パジャマ','ルームウェア'],
  ['ソックス','スパッツ/レギンス','ストッキング/タイツ','レッグウォーマー','その他'],
  ['ニットキャップ/ビーニー','ハット','ハンチング/ベレー帽','キャップ','キャスケット','麦わら帽子','その他'],
  ['ハンドバッグ','トートバッグ','エコバッグ','リュック/バックパック','ボストンバッグ','スポーツバッグ','ショルダーバッグ','クラッチバッグ','ポーチ/バニティ','ボディバッグ/ウェストバッグ','マザーズバッグ','メッセンジャーバッグ','ビジネスバッグ','旅行用バッグ/キャリーバッグ'],
  ['ネックレス','ブレスレット','バングル/リストバンド','リング','ピアス(片耳用)','ピアス(両耳用)','イヤリング','アンクレット','ブローチ/コサージュ','チャーム','その他'],
  ['ヘアゴム/シュシュ','ヘアバンド/カチューシャ','ヘアピン','その他'],
  ['長財布','折り財布','コインケース/小銭入れ','名刺入れ/定期入れ','キーケース','キーホルダー','手袋/アームカバー','ハンカチ','ベルト','マフラー/ショール','ストール/スヌード','バンダナ/スカーフ','ネックウォーマー','サスペンダー'],
  ['腕時計(アナログ)','腕時計(デジタル)','ラバーベルト','レザーベルト','金属ベルト','その他']
]
parent = Category.create(name: 'レディース')
# １階層目(親)のカテゴリーを作成

ladies_child_array.each_with_index do |child, i|
  # 2階層目(子)のカテゴリー配列から１つずつ取り出す
  child = parent.children.create(name: child)
  # 取りだりた2階層目(子)のカテゴリーを作成
  ladies_grandchild_array[i].each do |grandchild|
  # ３階層目(孫)のi番目のカテゴリー配列から１つずつ取り出す
    child.children.create(name: grandchild)
    # 取りだした３階層目(孫)のカテゴリーを作成
  end
end
```
###ポイント
**ladies_grandchild_array = [[ ]]**
の中に３階層目のカテゴリーを入れる
ここで、２重の配列にしておくのがポイント。

**ladies_child_array.each_with_index do |child, i|**
で2階層目(子)のカテゴリー配列から１つずつ取り出す時に『 **i** 』を付けて番号をつける。
**ladies_grandchild_array[i]**
とすることで、２重配列の*ladies_grandchild_array*から『 **i** 』番目の配列を取り出す事ができる。
さらに、後ろに*.each do |grandchild|*を付けて
**ladies_grandchild_array[i].each do |grandchild|**
とする事で、取り出した『 **i** 』番目の配列から１つずつ中身を取り出す事ができる。
つまり２階層目(子)のカテゴリーに紐ずく、３階層目(孫)のカテゴリーを選別している。という事。
###seedファイル実行
```ruby:ターミナル
rails db:seed
```
これで、カテゴリーテーブルに*ancestry*カラムが追加され、各カテゴリー名が入っているハズ

![スクリーンショット 2020-04-12 17.00.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565886/4b50f87d-fe32-a40a-42ac-f16fa7293c33.png)

#入力フォームを作る
###haml
```haml:new.html.haml
.sell-container__content
  %h3.sell-sub-head 商品の詳細
  .sell-container__content__details
    .sell-title
      %h3.sell-title__text
        カテゴリー
        %span.sell-title__require
          必須
    .sell-collection_select
      = f.label :category_id, {class: 'sell-collection_select__label'} do
        = f.collection_select :category_id, @category_parent, :id, :name, {prompt: "選択して下さい"},{ class: 'sell-collection_select__input', id: 'category-select-parent', required: "required"}
        %i.fas.fa-chevron-down
    .error-messages#error-category
```
###JS
```js:items_new.js
  $(function(){
    // カテゴリーセレクトボックスのオプションを作成
    function categoryOption(category){
      var optionHtml = `<option value="${category.id}">${category.name}</option>`;
      return optionHtml;
    }
    // 親カテゴリー選択後のイベント
    $('#category-select-parent').on('change', function(){
      let parentCategoryId = $(this).val();
      //選択された親カテゴリーのIDを取得
      if (parentCategoryId == ''){
        //親カテゴリーが空（初期値）の時
        $('#select-children-box').remove();
        $('#select-grandchildren-box').remove();
        //子と孫を削除するする
      }else{
        $.ajax({
          url: '/items/category_children',
          type: 'GET',
          data: { parent_id: parentCategoryId },
          dataType: 'json'
        })
        .done(function(category_children){
          $('#select-children-box').remove();
          $('#select-grandchildren-box').remove();
          //親が変更された時、子と孫を削除するする
          let optionHtml = '';
          category_children.forEach(function(child){
            optionHtml += categoryOption(child);
            //option要素を作成する
          });
          $('#error-category').before(`<div class="sell-collection_select " id="select-children-box">
                                          <label class="sell-collection_select__label" for="item_category_id">
                                            <select class="sell-collection_select__input" id="category-select-children" required="required" name="item[category_id]">
                                              <option value="">選択して下さい</option>
                                              ${optionHtml}
                                            </select>
                                            <i class="fas fa-chevron-down"></i>
                                          </label>
                                        </div>`
          );
        })
        .fail(function(){
          alert('カテゴリー取得に失敗しました');
        });
      }
    });
    // 子カテゴリー選択後のイベント
    $('.sell-container__content__details').on('change', '#category-select-children', function(){
      let childrenCategoryId = $(this).val();
      //選択された子カテゴリーのIDを取得
      if (childrenCategoryId == ''){
        //子カテゴリーが空（初期値）の時
        $('#select-grandchildren-box').remove(); 
        //孫以下を削除する
      }else{
        $.ajax({
          url: '/items/category_grandchildren',
          type: 'GET',
          data: { child_id: childrenCategoryId },
          dataType: 'json'
        })
        .done(function(category_grandchildren){
          $('#select-grandchildren-box').remove();
          //子が変更された時、孫を削除するする
          let optionHtml = '';
          category_grandchildren.forEach(function(grandchildren){
            optionHtml += categoryOption(grandchildren);
            //option要素を作成する
          });
          $('#error-category').before(`<div class="sell-collection_select " id="select-grandchildren-box">
                                          <label class="sell-collection_select__label" for="item_category_id">
                                            <select class="sell-collection_select__input" id="category-select-grandchildren" required="required" name="item[category_id]">
                                              <option value="">選択して下さい</option>
                                              ${optionHtml}
                                            </select>
                                            <i class="fas fa-chevron-down"></i>
                                          </label>
                                        </div>`
          );
        })
        .fail(function(){
          alert('カテゴリー取得に失敗しました');
        });
      }
    });
  });
```
**流れ**
1. １階層目のカテゴリーを選択
2. 選択されたカテゴリー情報をJSで所得し、ajaxでコントローラに送る
3. コントローラーでDBから１階層目のカテゴリーに紐付く２階層目のカテゴリーを全て取得し、jbuilderへ送る
4. jbuilderでデータをHTTP型からJSON型に変更し、配列にしてJSへ送る
5. JSのdone(function(category_children)で受け取り、処理をしていく
6. optionを保持するからの変数を用意
7. 受け取ったJSON型のデータの配列から１つずつ取り出し、JSの上で用意した
　*var optionHtml = `<option value="${category.id}">${category.name}</option>`;*
　でセレクトボックスで表示する為のオプションを作成
8. return optionHtml;で６で用意した変数に格納する
9. HTMLを作成してnew.html.hamlにはめ込む
10. ３階層目も同じ要領で
### コントローラー
```ruby:items.controller.rb
  def new
    @item = Item.new
    @category_parent =  Category.where("ancestry is null")
  end

  # 親カテゴリーが選択された後に動くアクション
  def category_children
    @category_children = Category.find("#{params[:parent_id]}").children
    #親カテゴリーに紐付く子カテゴリーを取得
  end

  # 子カテゴリーが選択された後に動くアクション
  def category_grandchildren
    @category_grandchildren = Category.find("#{params[:child_id]}").children
    #子カテゴリーに紐付く孫カテゴリーの配列を取得
  end
```

### jbuilder
```jbuilder:category_children.json.jbuilder
json.array! @category_children do |child|
  json.id   child.id
  json.name child.name
end
```
```jbuilder:category_grandchildren.json.jbuilder 
json.array! @category_grandchildren do |grandchild|
  json.id   grandchild.id
  json.name grandchild.name
end
```
#ポイント
seedファイルでの作成方法がわかれば大体できる。
セレクトボックスで表示する為には、**optionタグ**を作成する
**name属性が同じフォームは基本的に上書きされる**

![スクリーンショット 2020-04-12 18.02.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565886/cde017c7-4ea2-6c11-7fcc-07f76c9382c5.png)