ancestryとjQueryで多階層型カテゴリの入力フォームを段階的に表示させてみた。

##何をしたか
ショッピングサイトの検索・購入ページなどでよく見かける
「多階層型カテゴリの入力フォームが順に表示される機能」を
ancestryとjQueryを使って実装してみました。
振り返りを兼ねて記事を書いていきます。

##下準備
<details><summary>長いので見たい人だけ展開してください</summary><div>

※コードは載せますがここでは特に説明しません。
※scssは必要ないのですが味気ないので入れました。

```terminal.
$ rails _5.2.4_ new ancestry_sample --database=mysql --skip-test --skip-turbolinks --skip-bundle
$ gem install ancestry jquery-rails haml-rails
$ bundle install
$ rails g model category
$ rails g model item
$ rails g controller categories
$ rails g controller items
```
```asesst/javascripts/application.js
rails-ujsより上段にjqueryを追加
//= require jquery
//= require rails-ujs
```
```db/migrate/202***********_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
  def change
    create_table :categories do |t|
      t.string :name, null: false
      t.timestamps      
    end
    add_index :categories, :name
  end
end
```
```db/migrate/202***********_create_items.rb
class CreateItems < ActiveRecord::Migration[5.2]
  def change
    create_table :items do |t|
      t.references :category, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```
```models/category.rb
class Category < ApplicationRecord
  has_many :items
  has_ancestry
end

```
```models/item.rb
class Item < ApplicationRecord
  belongs_to :category
end

```
```controllers/items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.all
  end

  def new
    @item = Item.new
    @categories = []
    @categories.push(Category.new(id: 0,name:"---"))
    @categories.concat(Category.where(ancestry: nil))
  end

  def create
    Item.create(item_params)
    redirect_to items_path
  end

  private
  def item_params
    params.require(:item).permit(:category_id)
  end
end
```
```config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
end
```
```views/items/new.html.haml
.items
  =form_with(model:@item,local:true) do |f|
    .items__parent
      = select_tag 'parent', options_for_select(@categories.pluck(:name,:id))
    .items__child
    .items__grandchild
    = f.submit "登録する",class:"button"
``` 
```views/items/index.html.haml
%table
  %tr 
    %td No.
    %td 親
    %td 子
    %td 孫
  -@items.each_with_index do |item,i|
    %tr
      %td 
        = i+1
      %td 
        =item.category.parent.parent.name
      %td
        =item.category.parent.name
      %td
        =item.category.name
%button
  =link_to '戻る',new_item_path,class:"button"
```
```db/seed.rb
ary_tops = [{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "その他"}]
ary_jacket = [{name: "テーラードジャケット"},{name: "ノーカラージャケット"},{name: "Gジャン/デニムジャケット"},{name: "その他"}]
ary_shoes = [{name: "スニーカー"},{name: "サンダル"},{name: "その他"}]

lady = Category.create(name: "レディース")
lady_tops = lady.children.create(name: "トップス")
lady_tops.children.create(ary_tops)
lady_jacket = lady.children.create(name: "ジャケット/アウター")
lady_jacket.children.create(ary_jacket)
lady_shoes = lady.children.create(name: "靴")
lady_shoes.children.create(ary_shoes)

men = Category.create(name: "メンズ")
men_tops = men.children.create(name: "トップス")
men_tops.children.create(ary_tops)
men_jacket = men.children.create(name: "ジャケット/アウター")
men_jacket.children.create(ary_jacket)
men_shoes = men.children.create(name: "靴")
men_shoes.children.create(ary_shoes)
```
```terminal.
$ rails db:create
$ rails db:migrate
$ rails db:seed
```
```assets/stylesheets/items.scss
*{
  font-family: Arial,游ゴシック体,YuGothic,メイリオ,Meiryo,sans-serif;
  box-sizing: border-box;
}
%__select-form{
  width: 300px;
  height: 48px;
  background-color: #fff;
  border-radius: 4px;
  font-size: 16px;
  border: 1px solid #ccc;
  color: #222;
}
#parent{
  @extend %__select-form;
}
#child{
  @extend %__select-form;
}
#item_category_id{
  @extend %__select-form;
}
.button{
  width: 300px;
  height: 48px;
  background-color: #f5f5f5;
  border-radius: 5px;
  font-size: 17px;
  transition: 0.2s;
  text-decoration:none;
  line-height: 48px;
  color: #222;
}
```
下準備ここまで。
</div></details>

##いざ、実装
では早速やっていきましょう。
※メインはancestryの値の抽出 → ajax通信のため、js内のhtml作成部分には特に触れません。

####親入力欄変更 → 子入力欄表示
- イベント開始点作成（親カテゴリ"parent"を変更した時にイベント開始）

```asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
  }
}
```
- ajax通信に必要な値の抽出（selectタグから選択された項目のvalue値を抽出）

```asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
  };
}
```

- コントローラーへのajax通信処理の記述

```asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
    $.ajax({
      url: "/categories",
      type: 'GET',
      dataType: 'json',
      data: {id: int}
    })
    .done(function() {
    })
    .fail(function() {
    });
  });
})
```
- コントローラー内の処理の記述（ancestryの値が選択した親カテゴリのidと同値のレコードを取得）

```controllers/items_categories.rb
  def index
    @categories = Category.where(ancestry: params[:id])
    respond_to do |format|
      format.json
    end
  end
```
- routeの記述

```config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
  resources :categories ,only: :index
end
```

- json.jbulderの作成・記述

```views/categories/index.json.jbuilder
json.array! @categories do |category|
  json.id category.id
  json.name category.name
end
```

- 返り値と表示処理

```asesst/javascripts/items.js
$(function() {
  function buildHTML(result){
    var html =
      `<option value= ${result.id}>${result.name}</option>`
    return html
  }
#省略#
    .done(function(categories) {
      var insertHTML = `<select name="child" id="child">
                        <option value=0>---</option>`;
      $.each(categories, function(i, category) {
        insertHTML += buildHTML(category)
      });
      insertHTML += `</select>`
      $('.items__child').append(insertHTML);
    })
    .fail(function() {
    });
  });
})
```

####子入力欄変更 → 孫入力欄表示
- イベント開始点作成（子カテゴリ"child"を変更した時にイベント開始）

```asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
#中略#
  });
  $("#child").on("change",function(){
  });
})
```
- コントローラーでバインドするancestryの値「'親id'/'子id'」を取得、およびコントローラーへのajax通信処理を記述

```asesst/javascripts/items.js
#省略#
  $("#child").on("change",function(){
    var intParent = document.getElementById("parent").value
    var intChild = document.getElementById("child").value
    var int = intParent + '/' + intChild
    $.ajax({
      url: "/categories",
      type: 'GET',
      dataType: 'json',
      data: {id: int}
    })
    .done(function() {
    })
    .fail(function() {
    });
  });
})
```

**※ controller.rb、route.rb、json.jbuilderは前述のものを使用するため割愛します**

- 返り値と表示処理

```asesst/javascripts/items.js
#省略#
    .done(function(categories) {
      var insertHTML = `<select name="item[category_id]" id="item_category_id">
                        <option value=0>---</option>`;
      $.each(categories, function(i, category) {
        insertHTML += buildHTML(category)
      });
      insertHTML += `</select>`
      $('.items__grangchild').append(insertHTML);
    .fail(function() {
    });
  });
})
```
###完成？
**完成！**

と言いたいところですが、このままだと親や子を変更する度に入力欄が無限に増殖してしまいます。

![スクリーンショット 2020-03-14 1.20.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600698/7264e2fc-8302-81b7-e08a-7591d2beb2ac.png)


- 条件式を追加
 - 「"---"を選択した時」 → 下位の要素をremove
 - 「追加する要素が既に存在する時」 → 要素をreplace
 - それ以外 → append

```asesst/javascripts/items.js
##省略##
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
    if(int == 0){
      $('#child').remove();
      $('#item_category_id').remove();
    }else{
      $.ajax({
        url: "/categories",
        type: 'GET',
        dataType: 'json',
        data: {id: int}
      })
      .done(function(categories) {
        var insertHTML = `<select name="child" id="child">
                          <option value=0>---</option>`;
        $.each(categories, function(i, category) {
          insertHTML += buildHTML(category)
        });
        insertHTML += `</select>`
        if($('#child').length){
          $('#child').replaceWith(insertHTML);
          $('#item_category_id').remove();
        } else {
          $('.items__child').append(insertHTML);
        };
      })
##中略##
  $(document).on("change","#child",function(){
    var intParent = document.getElementById("parent").value
    var intChild = document.getElementById("child").value
    var int = intParent + '/' + intChild
    if(intChild == 0){
      $('#item_category_id').remove();
    } else {
      $.ajax({
        url: "/categories",
        type: 'GET',
        dataType: 'json',
        data: {id: int}
      })
      .done(function(categories) {
        var insertHTML = `<select name="item[category_id]" id="item_category_id">
                          <option value=0>---</option>`;
        $.each(categories, function(i, category) {
          insertHTML += buildHTML(category)
        });
        insertHTML += `</select>`
        if($('#item_category_id').length){
          $('#item_category_id').replaceWith(insertHTML);
        } else {
          $('.items__grandchild').append(insertHTML);
        };
      })
##後略##
```

条件式により、無限に増殖することもなくなりました。
![スクリーンショット 2020-03-14 1.41.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600698/dde0d47b-292f-fd19-3e8a-037e6ae8cd6e.png)


レコードも無事できました。
![スクリーンショット 2020-03-14 1.41.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600698/84e061d7-5134-056e-551f-1c328f23d473.png)
![スクリーンショット 2020-03-14 1.41.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600698/51e540fe-7460-deac-b887-4dbb94213424.png)



というわけで、
**完成です！**

##注意事項
**・**エラー処理は何もしていないので、保存ができない場合が多々ありますが仕様です。
（孫まで入力しないと、form_withで送信するパラメータを拾えないのでレコード登録できません。）

以上です。

##参考にさせていただいた記事
[多階層カテゴリでancestryを使ったら便利すぎた](https://qiita.com/Sotq_17/items/120256209993fb05ebac)