LoginSignup
3
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-06

はじめに

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

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

※この前段階として、ancestryを使って使用するカテゴリデータのテーブルを作っています。
その記事はコチラ▶︎ancestryを用いた階層型カテゴリデータの作成方法

この記事でわかること

  • Ajax通信で3段階のカテゴリ選択フォームを表示させる

この機能で達成したいゴール

下記のように親要素(レディース)選択したら、子要素の選択フォームが、子要素を選択したら孫要素の選択フォームが非同期通信で表示されるようにします。
また、子や孫の選択肢を変えたらフォーム内容が初期化されたり、フォームそのものが消えたりします。
Image from Gyazo

開発環境

  • ruby 2.6.5
  • rails 6.0.3.2
  • sequel pro

大まかな流れ

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

前提として、
- ancestryを用いたデータ作成は完了していることとします。
 *ancestryを使ったカテゴリデータ作成方法はコチラをクリック
- itemsコントローラーも作成済とする。

具体的な実装手順

0. gemのインストール

まずは必要なgemをインストール。

gem jquery-rails
bundle install

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

app/javascript/packs/application.js
require('jquery')
require('item/category')
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の導入方法

2.ルーティングを追加

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

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

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

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

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アクションでレコードを取得。

items_controller
  def new 
    @item = Item.new
    @item_image = ItemImage.new
  end

また、newアクションとcreateアクションの前に親要素の情報を取得しておきたいので、before_actionを設定しました。

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が必要な理由は、
コチラの記事を参考にさせていただきました。
(正直、コード書いている時はわかったような、わからないような...でした。)



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

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ファイルを作成

app/view/items/search.json.jbuilder
json.array! @childrens do |children|
  json.id children.id
  json.name children.name
end

とりあえず、いまは子要素の分のみ記載しています。

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

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

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名も追加しました。


2.表示された選択フォームにデータを取得するためのoptionを記載。

category.js
function add_Option(children) {
  let option_html = `
                    <option value=${children.id}>${children.name}</option>
                    `
  return option_html;
}

 

3.親カテゴリを選択した後に起こるイベントを設定する。

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でとりだしたデータをひとつずつ取り出して表示させる。

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

4.孫要素の表示を作成する。

基本的には、子要素の手順を繰り返すだけです。

app/views/items/search.json.jbuilder
json.array! @grandchildrens do |grandchildren|
  json.id grandchildren.id
  json.name grandchildren.name
end
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
}
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の手順で一見完成したように見えるのですが、
このままだと以下のような不具合がでます。
- 親や子カテゴリを変更しても子カテゴリの内容がそのまま。
- 子や孫カテゴリを変更すると、孫の下に更に新たなカテゴリが出現してしまう。

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

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

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

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

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のあとのこの2行と

      $(".child_category_id").remove();
      $(".grandchild_category_id").remove();

②.failのあとにelseで条件分けしたこの部分です。

else{
    $(".child_category_id").remove();
    $(".grandchild_category_id").remove();  
  }

①で親カテゴリの内容が変わるたびに子/孫カテゴリを一度消し、その上で子カテゴリを出現させるようにしました。
②のelse文の後ろは、親が初期値を選択した場合。

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

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

category.js
window.addEventListener('load', function () {
}

これでコード全体を括りました。

これで本当に完成!!



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

参考にした記事

[Rails]某フリマアプリのカテゴリー機能の実装方法
フリマアプリのカテゴリ機能〜gem : ancestryを使用〜
[Rails] Ajax通信を用いたカテゴリボックス作成

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4