7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-13

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

##下準備

長いので見たい人だけ展開してください

※コードは載せますがここでは特に説明しません。
※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;
}

下準備ここまで。

##いざ、実装
では早速やっていきましょう。
※メインは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

  • 条件式を追加
  • 「"---"を選択した時」 → 下位の要素を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

レコードも無事できました。
スクリーンショット 2020-03-14 1.41.12.png
スクリーンショット 2020-03-14 1.41.24.png

というわけで、
完成です!

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

以上です。

##参考にさせていただいた記事
多階層カテゴリでancestryを使ったら便利すぎた

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?