ncestryによる多階層構造データを表示、投稿！！　~Ajax~


# はじめに
ancestryで作成したカテゴリーデータを用いて、選択肢を動的に変化させる機能を実装しました。

学習メモとして投稿します。
まだ、理解が浅いところもありますが参考になればと思います！


完成形

https://gyazo.com/8a5adc080698873d544b8665855c0901



以下が完成コードです！



```ruby:routes

resources :products, except: [:index]  do 
    get 'new/children_category', to: 'products#children_category'
    get 'new/grandchildren_category', to: 'products#grandchildren_category'
  end
```

```ruby:puroducts_controller
before_action :set_categories, only: [:edit, :update]


〜省略〜

  def children_category
    @children_category = Category.where(ancestry: params[:parent_category_id])
    render json:  @children_category
  end

  def grandchildren_category
    @grandchildren_category = Category.where(ancestry: "#{params[:parent_category_id]}/#{params[:children_category_id]}")
    render json: @grandchildren_category 
  end

```

```ruby:puroducts/new_html_haml
.input-field__contents
  .input-field__contents-data
    %p.subline
      商品の詳細
    .input-field__contents-image__headline
      .headlabel
        = f.label :category_id, "カテゴリー"
        %span.necessary
           必須
           .sell__about__right__wrap-box.parent
             %select.select-box1#parent
               %option{value: 0} ---
               -  @parents.each do |parent|
                 %option{value: "#{parent.id}"} #{parent.name}
                    
           .child
             %select.select-box2#child
           .grand_child
             .select-box3
               = f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

```

```javascript:category_js

$(function(){
  let buildPrompt = `<option value>---</option>`
  let buildHtmlOption = function(parent) {
    let option = `<option value ="${parent.id}">${parent.name}</option>`
    return option
  }
  $('#parent').change(function() {
    let parent_id = $(this).val();
    $.ajax({
      type: 'GET',
      url: 'products/new/children_category',
      data: {parent_category_id: parent_id},
      dataType: 'json'
    })
    .done(function(parent) {
      $('.child').css('display', 'block');
        $('#child').empty();
        $('.grand_child').css('display', 'none');
        $('#child').append(buildPrompt);

      parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
        $('#child').append(html_option);
      });
    })
    .fail(function() {
      alert('エラー')
    });
  });
  $(this).on("change", "#child", function() {
    let parent_id = $("#parent").val();
    let child_id = $("#child").val();
    $.ajax({
        type: 'GET',
        url: 'products/new/grandchildren_category',
        data: {
          parent_category_id: parent_id,
          children_category_id: child_id
        },
        dataType: 'json'
    })
    .done(function(parent) {
      $('.grand_child').css('display', 'block');
      $('#grand_child').empty();
      $('#grand_child').append(buildPrompt);

       parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
         console.log(buildHtmlOption(html_option));
        $('#grand_child').append(html_option);
      });
    })
  });
})
```


#考え方

・親カテゴリーを選択しイベントを発火させたら、**子**カテゴリーをappend(追加)する
・子カテゴリーを選択しイベントを発火させたら、**孫**カテゴリーをappend(追加)する
・ajaxを使用を子カテゴリー及び孫カテゴリーが表示されるための**通り道**を作成する
・最終的には孫カテゴリーの値が保存される様にする


ざっくりとこんな感じです。

では、一つ一つ見ていきましょう！

#ルーティング

プログラムの処理の流れとして、最終的にviewに子カテゴリーと親カテゴリーを表示させます。
それは実際に、コントローラーとjsで処理を行いますのでリクエストが会った際のコントローラーへの通り道を作成します。

```ruby:routes

resources :products, except: [:index]  do 
　　　#children_categoryアクションに行くためのパス
    get 'new/children_category', to: 'products#children_category'
　　　#grandchildren_categoryアクションに行くためのパス
    get 'new/grandchildren_category', to: 'products#grandchildren_category'
  end
```

#コントローラー

前提として、ajax処理行うのでjsでajax処理が行われたあとはコントローラーに行きます。
その際、コントローラーではカテゴリーの値を探してjsに返してあげる必要があります。
したがって、以下の様に書きます。

```ruby:puroducts_controller
before_action :set_categories, only: [:edit, :update]


〜省略〜

  def children_category
    #.whereを使ってancestryから値を探して、インスタンス変数に代入する
    @children_category = Category.where(ancestry: params[:parent_category_id])
　　 #ancestryから探した値をjsに返してあげる
    render json:  @children_category
  end

  def grandchildren_category
　　#.whereを使ってancestryから値を探して、インスタンス変数に代入する
    @grandchildren_category = Category.where(ancestry: "#{params[:parent_category_id]}/#{params[:children_category_id]}")
　　#ancestryから探した値をjsに返してあげる
    render json: @grandchildren_category 
  end

```


# JSの処理
jsでは、カテゴリーが選択されるたびにイベントが発火する様にします。
具体的に、
・**親**カテゴリーが選択されたら、イベントが発火し**子**要素のカテゴリー表示させる
・**子**カテゴリーが選択されたら、イベントが発火し**孫**要素のカテゴリー表示させる

処理としては、イベントが発火したらajaxでコントローラーから値を取得し**forEach**で全てを表させる流れになります。

```javascript:category_js
//①=====HTMLで表示させるviewを定義＝＝＝＝＝＝＝＝＝＝=================
$(function(){
  let buildPrompt = `<option value>---</option>`
  let buildHtmlOption = function(parent) {
    let option = `<option value ="${parent.id}">${parent.name}</option>`
    return option
  }
//======＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝================



//②=====親カテゴリーが選択され子カテゴリーを呼び出す処理＝＝＝＝＝＝＝＝＝＝＝＝
  $('#parent').change(function() {
    let parent_id = $(this).val();
    //ajaxでコントローラーに送る
    $.ajax({
      type: 'GET',
      url: 'products/new/children_category',
      data: {parent_category_id: parent_id},
      dataType: 'json'
    })
　　//以下はコントローラーからのレスポンス後の処理
    .done(function(parent) {
      $('.child').css('display', 'block');
        $('#child').empty();
        $('.grand_child').css('display', 'none');
        $('#child').append(buildPrompt);

　　//コントローラーから取得した値をforEachで全て取得し、.appendでHTML要素に追加する
      parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
        $('#child').append(html_option);
      });
    })
    .fail(function() {
      alert('エラー')
    });
  });
//======＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝


//②=====子カテゴリーが選択され孫カテゴリーを呼び出す処理＝＝＝＝＝＝＝＝＝＝＝＝
  $(this).on("change", "#child", function() {
    let parent_id = $("#parent").val();
    let child_id = $("#child").val();
　　//ajaxでコントローラーに送る
    $.ajax({
        type: 'GET',
        url: 'products/new/grandchildren_category',
        data: {
          parent_category_id: parent_id,
          children_category_id: child_id
        },
        dataType: 'json'
    })
　　//以下はコントローラーからのレスポンス後の処理
    .done(function(parent) {
      $('.grand_child').css('display', 'block');
      $('#grand_child').empty();
      $('#grand_child').append(buildPrompt);
　　//コントローラーから取得した値をforEachで全て取得し、.appendでHTML要素に追加する
       parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
         console.log(buildHtmlOption(html_option));
        $('#grand_child').append(html_option);
      });
    })
  });
//======＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝
})
```

#最後には、HTML
HTMLで注意する点は、jsのid属性とHTMLでのid属性に齟齬かないかぐらいです。

ただし、最後の孫カテゴリーの値を保存するためには少し工夫が必要です。

```ruby:puroducts/new_html_haml
.input-field__contents
  .input-field__contents-data
    %p.subline
      商品の詳細
    .input-field__contents-image__headline
      .headlabel
        = f.label :category_id, "カテゴリー"
        %span.necessary
           必須
           .sell__about__right__wrap-box.parent
             %select.select-box1#parent
               %option{value: 0} ---
                #　親カテゴリーの値を全て表示させる
               -  @parents.each do |parent|
                 %option{value: "#{parent.id}"} #{parent.name}
                    
           .child
　　　　　　　　# #childのところにjsで定義したviewが挿入される
             %select.select-box2#child
           .grand_child
             .select-box3
　　　　　　　　　　# id:grand_childのところにjsで定義したviewが挿入される
                # また、選択孫カテゴリーの値が保存正しく保存されるために以下の様に書きます。
               = f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

```

補足で、以下の記述については以下のサイトを参考にしましたのでご確認ください

```ruby
 f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

 #参考記述
 #collection_select(オブジェクト名, メソッド名, 要素の配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])
```

参考記事:
https://railsdoc.com/page/collection_select

#終わりに

処理としては、そこまで複雑ではないため１つ１つ確認しながら行ったら上手く行きました！

もし、エラーや上手く値が取得できていない場合は、**binding.pry**や、**console.log();**、**debugger**で確認してみてください！

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