LoginSignup
7
3

More than 3 years have passed since last update.

【Rails】セレクトボックスを活用した多階層構造データの表示について

Last updated at Posted at 2020-05-29

経緯

とあるプログラミングスクール受講生です。
存知の方も多いでしょう「フリマクローンサイト」作成にあたり、出品カテゴリーをajaxで実装しましたので、自分の頭の整理を兼ねてまとめていきたいと思います。

なお、カテゴリーの設定は「ancestry」というGemを使用しております。
今回の記事につきましては、カテゴリー設定後を想定しております。

※説明が必要ない方はコードのみ追ってもらえれば実装できると思います。

完成イメージ

category.gif

コード

ビュー(items.new.html.haml)
.form-title
  =f.label "カテゴリ"
  .form-title__required
    %label 必須
.form-input-select
  = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'}
.listing-product-detail__category
  • 今回、重要なのは下3行のみです。他はご自由に。
  • = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'} の {} は超重要ですので消さないでください。
    → 軽く触れておきます。{}の部分が2つありますね。一つ目が「オプション」の記載、二つ目(今回記載がある方)が「HTMLオプション」となります。
    「オプション」を設定しない場合は空欄でも{}の記載しておかないと、「HTMLオプション」が反映されません。
コントローラー(items_controller.rb)
  def new
    @category_parent_array = ["---"]
    Category.where(ancestry: nil).each do |parent|
      @category_parent_array << parent.name
    end
  end

  def get_category_children
    @category_children = Category.find_by(name: "#{params[:parent_name]}", ancestry: nil).children
  end

  def get_category_grandchildren
    @category_grandchildren = Category.find("#{params[:child_id]}").children
  end

デモを見てもらうと、カテゴリーが三段階で表示されているのが確認できると思います。
・ 一段目の表示がnew
・ 二段目の表示がget_category_children
・ 三段目の表示がget_category_grandchildren
となっております。

複雑な部分はnewのコードでしょうか。
@category_parent_array に "---" しか入っていない配列を代入していますね。
次の行のeach文で先ほど設定した@category_parent_array配列にカテゴリを1件ずつ代入しています。
(ancestry: nil)につきましては、「ancestry」でカテゴリーを設定した後に保存データを確認してみてください。意味がわかると思います。

二段落目、三段落目の記載はajaxでの処理となりますので、アクションを分けています。
params[:parent_name]、params[:child_id]については、ajaxでJavaScriptから飛んでくる値です。
あとで設定しますので、覚えておきましょう。

routes.rb
 resources :items do
    collection do
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
    end
  end

先ほどコントローラーで出てきた2つのajax用アクションを設定しています。
itemsにネストしております。
軽くcollectionについて簡単すると、routingにidが付くのがmember付かないのがcollectionです。
今回は、「個(id)」を特定する必要がないので、collectionですね。

コントローラーは基本的に処理をビューに返すのですが、
defaults: { format: 'json' }の設定をしておくと、デフォルトでjsonファイルに処理を返すようになります。
(コントローラーでrespond_toを使用してjsonファイルに振り分ける必要が無くなります。)

get_category_children.json.jbuilder
json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end
get_category_grandchildren.json.jbuilder
json.array! @category_grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

ファイルの場所、間違えないでくださいね!!ビューと同じ場所に格納します。

コントローラーで二段落目、三段落目のアクション処理を行うと、このjsonファイルに飛びます。
(routes.rbで先ほど設定しましたね。)
コントローラーで設定した変数をここでajax用データに変換している訳ですね。

ちなみに、json.array!は配列形式のデータをコントローラーから受け取る時に設定します。

ajax処理の流れは、
ビュー(カテゴリー選択)→JavaScript(発火)→コントローラー(処理)→json.jbuilder(データ変換)→JavaScript(処理)→ビュー
の繰り返し(と認識しています)。

さて、最後にJavaScriptのお出ましです。

JS(items.js)
$(function)(){

  //子カテゴリー、孫カテゴリーのセレクトボックスの選択肢
  function appendOption(category){
    //value="${category.name}"については、ストロングパラメーターでの値の取り方によってcategory.idの場合もあると思います。
    var html = `<option value="${category.name}" datacategory="${category.id}">${category.name}</option>`;
    return html;
  }

  //子カテゴリーのビュー作成
  function appendChildrenBox(insertHTML){
    var childSelectHtml = '';
    childSelectHtml = `<div class='listing-select-wrapper__added' id= 'children_wrapper'>
                        <div class='listing-select-wrapper__box'>
                          <select class="listing-select-wrapper__box--select" id="child_category" name="category_id">
                            <option value="---" data-category="---">---</option>
                            ${insertHTML}
                          <select>
                        </div>
                      </div>`;
    $('.listing-product-detail__category').append(childSelectHtml);
  }

 //孫カテゴリーのビュー作成
  function appendGrandchildrenBox(insertHTML){
    var grandchildSelectHtml = '';
    grandchildSelectHtml = `<div class='listing-select-wrapper__added' id= 'grandchildren_wrapper'>
                              <div class='listing-select-wrapper__box'>
                                <select class="listing-select-wrapper__box--select" id="grandchild_category" name="category_id">
                                  <option value="---" data-category="---">---</option>
                                  ${insertHTML}
                                </select>
                              </div>
                            </div>`;
    $('.listing-product-detail__category').append(grandchildSelectHtml);
  }

  //親カテゴリーが選択された時の処理(子カテゴリーの表示)
  $("#parent_category").on('change', function(){
    //選択された親カテゴリーの値を取得
    var parentCategory = document.getElementById('parent_category').value;
    //選択された親カテゴリーが"---"(初期設定)のままだとfalse、変わっているとtrue
    if (parentCategory != "---"){
      $.ajax({
        url: 'get_category_children',
        type: 'GET',
        //コントローラーに飛ばす値です。
        data: { parent_name: parentCategory },
        dataType: 'json'
      })
      .done(function(children){
        //まず、既に表示されている子、孫カテゴリーを削除
        $('#children_wrapper').remove();
        $('#grandchildren_wrapper').remove();
        //insertHTMLという変数にカテゴリーのセレクトボックスの選択肢を入れる。(一番最初の段落で設けた変数)
        var insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        //2段落目で設定した子カテゴリーのビューの呼び出し
        appendChildrenBox(insertHTML);
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#children_wrapper').remove();
      $('#grandchildren_wrapper').remove();
    }
  });

  //子カテゴリーが選択された時の処理(孫カテゴリーの表示)
  $('.listing-product-detail__category').on('change', '#child_category', function(){
    var childId = $('#child_category option:selected').data('category');
    if (childId != "---"){
      $.ajax({
        url: 'get_category_grandchildren',
        type: 'GET',
        data: { child_id: childId },
        dataType: 'json'
      })
      .done(function(grandchildren){
        if(grandchildren.length != 0) {
          $('#grandchildren_wrapper').remove();
          $('#size_wrapper').remove();
          $('#brand_wrapper').remove();
          var insertHTML = '';
          grandchildren.forEach(function(grandchild){
            insertHTML += appendOption(grandchild);
          });
          appendGrandchildrenBox(insertHTML);
        }
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#grandchildren_wrapper').remove();
      $('#size_wrapper').remove();
      $('#brand_wrapper').remove();
    }
  });
});

はい。言いたいことはわかります。私、エスパーですから。
そんな皆さんに私から頑張れという便利な言葉を送ります。

ここにつきましてはあまりに長いので、コードにコメントアウトで処理の説明をしています
イマイチわかりにくかったら各自調べてもらえればと思います。

だらだらと長い記事を最後まで読んでいただき、ありがとうございました。
○○キャンプの受講生はLGTM必須で。

参考とさせていただいたサイト

https://qiita.com/ATORA1992/items/bd824f5097caeee09678
@ATORA1992様(とてもわかりやすい記事でした。ありがとうございました!!)

7
3
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
7
3