フリマアプリ作成におけるJSを用いたカテゴリ選択フォームの実装

#はじめに
某プログラミングスクールの最終課題でフリマアプリのクローンを作成中です。
商品出品機能に必要な「カテゴリ選択機能」の実装において作業した内容について記載していきます。

- JavaScriptを用いたプルダウン選択フォームの作成

※この前段階として、ancestryを使って使用するカテゴリデータのテーブルを作っています。
その記事はコチラ▶︎[ancestryを用いた階層型カテゴリデータの作成方法](https://qiita.com/kaji-yuki/items/c707d6e24f1ccf4605bd)


#この記事でわかること
- Ajax通信で３段階のカテゴリ選択フォームを表示させる

#この機能で達成したいゴール
下記のように親要素（レディース）選択したら、子要素の選択フォームが、子要素を選択したら孫要素の選択フォームが**非同期通信で**表示されるようにします。
また、子や孫の選択肢を変えたらフォーム内容が初期化されたり、フォームそのものが消えたりします。
[![Image from Gyazo](https://i.gyazo.com/92a3fdc100c522f4ffd1a695eca83c38.png)](https://gyazo.com/92a3fdc100c522f4ffd1a695eca83c38)

#開発環境
- ruby 2.6.5
- rails 6.0.3.2
- sequel pro


#大まかな流れ
0. gemのインストール
1. JSを使うためのコントローラー、JSファイルを作成し、呼び出しの記述をする。
1. ルーティングを追加する。
2. コントローラーにデータを検索する#search機能を設定する。
3. jsファイルを編集。
4. Ajax通信時に用いるjson.jbuilderファイルを編集。
5. 上記４−６を子要素、孫要素で繰り返す。
6. 子要素・孫要素を変更した際のアクションを追加する。
7. 完成

前提として、
- ancestryを用いたデータ作成は完了していることとします。
　＊ancestryを使ったカテゴリデータ作成方法は[コチラをクリック](https://qiita.com/kaji-yuki/items/c707d6e24f1ccf4605bd)
- itemsコントローラーも作成済とする。

#具体的な実装手順
## 0. gemのインストール
まずは必要なgemをインストール。

```ruby
gem jquery-rails
```

```ruby
bundle install
```

## 1.JavaScriptを使うための記述をする

```ruby:app/javascript/packs/application.js
require('jquery')
require('item/category')
```

```ruby:config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
  })
)

module.exports = environment
```


ここで早速つまづいたのが、わたしの開発環境がrails6だったこと。
ネットで調べた記事のほとんどがrails5以前での情報になっており、
rails6ではapplication.jsファイルの生成場所や呼び出しのための記述が違うらしく、
最初うまく読み込まれなくて混乱しました。

開発の際はバージョンを確認の上、各自の環境にあわせて記述してください。

rails6でのjquery導入はこの記事を参考にさせていただきました。=>[rails6でのjqueryの導入方法](https://qiita.com/masahisa/items/eaacb0c3b82f4a11fc13)

## 2.ルーティングを追加
のちほどitemsコントローラーにsearchアクションを追加するため、
searchアクションへのルーティングを追記します。

```ruby:routes.rb
Rails.application.routes.draw do
  resources :items do
    collection do
      get :search
    end
  end
end
```

## 3.ビューにカテゴリ選択フォームを作成

こちらは、あくまで私の記述なの参考までに。
このような形でまずは親カテゴリ用のフォームを作成しました。

```ruby:qpp/view/items/new.html.haml
=form_with model @item do |f|
= f.collection_select :category_id, @parents, :id, :name, {prompt: "選択してください"}, {class: "categoryForm", id: "category_form"}
```

## 4.コントローラーにsearchアクションを追加

newアクションでレコードを取得。

```ruby:items_controller
  def new 
    @item = Item.new
    @item_image = ItemImage.new
  end
```
また、newアクションとcreateアクションの前に親要素の情報を取得しておきたいので、before_actionを設定しました。

```ruby:ite,s_controller.rb
  before_action :set_parents, only: [:new, :create]

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

ancestry＝nil、つまり親要素の値を取得し、変数@parentsへ代入しています。
newだけではなく、createの時にもset_parentsが必要な理由は、
[コチラ](https://qiita.com/kaito_program/items/3755f071625f9f5bbaba)の記事を参考にさせていただきました。
(正直、コード書いている時はわかったような、わからないような...でした。)


<br>
そして、子・孫のカテゴリ検索のため、searchアクションを設定します。

```ruby:items_controller.rb

  def search
    respond_to do |format|
      format.html
      #ajax通信開始
      format.json do
          #子カテゴリの情報を@childrensに代入
          @childrens = Category.find(params[:parent_id]).children
      end
    end
  end
```
余談ですが、複数形の"children"に更に"s"を付けるのにどうしても違和感があったのですが、
childとchildrenを使いわけるのもわかりにくいなぁと思い、仕方なくchildrensにしました。

## 5.json.jbulderファイルを作成

```ruby:app/view/items/search.json.jbuilder
json.array! @childrens do |children|
  json.id children.id
  json.name children.name
end
```
とりあえず、いまは子要素の分のみ記載しています。


## 6.jsファイルを編集し、上記アクションで送られてくるデータを受け取る

1.まずは、親要素を選択した後に現れる「子要素の選択フォーム」を作成。

```ruby:category.js
// 子要素を選択するフォーム
function add_childSelect_tag() {
  let child_select_form = `
              <select name="item[category_id]" id="item_category_id" class="child_category_id">
              <option value="">カテゴリを選択</option>
              </select>
            `
  return child_select_form;
}
```
このHTMLの部分を書くのが苦手なのですが、ビューの検証ツールで親要素のフォームをみて書くのが一番簡単な気がしています。

name="item[category_id]はデータの送り先です。

あとで、子要素のフォームを消したり初期化したりしたい（親要素を変更したときなど）ので、子要素特有のclass名も追加しました。
<br>
2.表示された選択フォームにデータを取得するためのoptionを記載。

```ruby:category.js
function add_Option(children) {
  let option_html = `
                    <option value=${children.id}>${children.name}</option>
                    `
  return option_html;
}
```
　<br>
3.親カテゴリを選択した後に起こるイベントを設定する。

```ruby:category.js
//親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
  let parentValue = $("#category_form").val();
  if (parentValue.length !== 0) {
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { parent_id: parentValue},
      dataType: 'json'
    })
    .done(function (data){
      let child_select_form = add_childSelect_tag
      $(".ItemInfo__category--form").append(child_select_form);
      data.forEach(function(d){
        let option_html = add_Option(d)
        $(".child_category_id").append(option_html);
      });
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
});
```
おおまかな流れとしては、
1. 親要素のデータを取得し、parentValueへ代入。
2. if〜でデータが初期値でなければajax通信。
3. .doneでappendを使って、親要素のフォームの後に前手順で作成した子要素のフォームを表示させる。
4. optionでとりだしたデータをひとつずつ取り出して表示させる。


###これで子要素の表示は完成！ <br>親要素を選択すれば、子要素フォームが表示されます。


## 4.孫要素の表示を作成する。
基本的には、子要素の手順を繰り返すだけです。

```ruby:app/views/items/search.json.jbuilder
json.array! @grandchildrens do |grandchildren|
  json.id grandchildren.id
  json.name grandchildren.name
end
```

```ruby:category.js
// 孫要素の選択フォーム
function add_grandchildSelect_tag(){
  let grandchild_select_form = `
              <select name="item[category_id]" id="item_category_id" class="grandchild_category_id">
              <option value="">カテゴリを選択</option>
              </select>
            `
  return grandchild_select_form
}
```
```ruby:categroy.js
// 子カテゴリを選択後のイベント
$(document).on("change", ".child_category_id", function(){
  let childValue = $(".child_category_id").val();
  if (childValue.length !=0){
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { children_id: childValue},
      dataType: 'json'
    })
    .done(function (gc_data){
      let grandchild_select_form = add_grandchildSelect_tag
      $(".ItemInfo__category--form").append(grandchild_select_form);
      gc_data.forEach(function (gc_d){
        let option_html = add_Option(gc_d);
        $(".grandchild_category_id").append(option_html);
      })
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
    });
})
```
これで、子要素を選択すれば孫要素の選択フォームがでてくるようになります。

##5.フォームを初期化する記述を追加する。

1-4の手順で一見完成したように見えるのですが、
このままだと以下のような不具合がでます。
- 親や子カテゴリを変更しても子カテゴリの内容がそのまま。
- 子や孫カテゴリを変更すると、孫の下に更に新たなカテゴリが出現してしまう。

要するに、子や孫カテゴリをいじっていると、エンドレスでフォームが追加されていってしまう状態。

なので、望ましい挙動として、

- 親カテゴリを変更したら、孫カテゴリフォームは消えて、子カテゴリのフォームの内容は親カテゴリに紐づいた内容に変わる。
- 子カテゴリを変更したら、孫カテゴリの内容は子カテゴリに紐づいたデータに変わる。
- 親や子カテゴリを初期値（選んでいない状態）にしたら、その次の階層のフォームは消える。

この動作を実現させないといけません。
そのために、下記の記述を加えました。

```ruby:category.js
// 親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
  let parentValue = $("#category_form").val();
  if (parentValue.length !== 0) {
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { parent_id: parentValue},
      dataType: 'json'
    })
    .done(function (data){
      $(".child_category_id").remove();
      $(".grandchild_category_id").remove();
      let child_select_form = add_childSelect_tag
      $(".ItemInfo__category--form").append(child_select_form);
      data.forEach(function(d){
        let option_html = add_Option(d)
        $(".child_category_id").append(option_html);
      });
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
    })
  }else{
    $(".child_category_id").remove();
    $(".grandchild_category_id").remove();  
  }
});
```
追加したのは、①.doneのあとのこの２行と

```ruby
      $(".child_category_id").remove();
      $(".grandchild_category_id").remove();
```
②.failのあとにelseで条件分けしたこの部分です。

```ruby
else{
    $(".child_category_id").remove();
    $(".grandchild_category_id").remove();  
  }
```
①で親カテゴリの内容が変わるたびに子/孫カテゴリを一度消し、その上で子カテゴリを出現させるようにしました。
②のelse文の後ろは、親が初期値を選択した場合。

同じコードを子カテゴリ選択後のイベント部分にも追記しました。

そして、全部ページを読み込んでからこれらのJSを作動させるため、

```ruby:category.js
window.addEventListener('load', function () {
}
```
これでコード全体を括りました。

###これで本当に完成！！
<br>
このあと、うっかりチームメンバーに「db:seedしてね」と言うのを忘れ、
「カテゴリ選択できない！！なぜ？！」という事態もおきましたが、無事に解決にいたりました。

##参考にした記事
[[Rails]某フリマアプリのカテゴリー機能の実装方法](https://qiita.com/kaito_program/items/3755f071625f9f5bbaba)
[フリマアプリのカテゴリ機能〜gem : ancestryを使用〜](https://qiita.com/renaboo/items/e9ad2a1d2033cc23c0d8)
[[Rails] Ajax通信を用いたカテゴリボックス作成](https://qiita.com/mihou/items/0fbda161527fcec48e81)