みほうです。
3回目のスプリントレビューが終わり、必須機能についてはほぼ実装できました。
さて今回は前記事で予告していたancestryについての記事です。
カテゴリボックスは出品機能実装の中でも特に苦戦したところです。
備忘録として書いていきます。
##開発環境
Rails 5.2.3
Sequel Pro(MySQLデータベース)
親カテゴリボックスの作成
メルカリのコピーサイトにおいて、出品画面のカテゴリボックスについては3つあります。
以下3つのカテゴリボックスのうち、一番上のボックスを親ボックス
、中央のボックスを子ボックス
、一番下のボックスを孫ボックス
とします。
このうち一番上の親ボックス
についてはAjax通信なしで実装できます。
出品画面はitemsコントローラーのnewアクションで作成しているため、その中にインスタンス変数を定義します。
(app/controllers/items_controller.rb)
def new
@parents = Category.all.order("id ASC").limit(13)
# Categoriesテーブルの上から13個のレコードを取り出す
end
そしてViewでcollection_selectを用いてデータベースの情報からセレクトボックスを作ることで親ボックスを作成することができます。
(app/views/new.html.haml)
= f.collection_select :category_ids, @parents, :id, :name,{prompt: "---"}, class: "select-default", id: "parent-form", name: 'item[category_ids][]'
親ボックスについてはこの形でいいとして、中央の子ボックス
、一番下の孫ボックス
には
数々のカテゴリ名が入っています。最終的には1200のカテゴリが登録されます。これを条件毎にセレクトボックスを作るようにすると大変な労力です。
Railsではこのようなセレクトボックスの作成を助ける「ancestry」というgemがあります。
gem「ancestry」について
gem「ancestry」
を用いるとデータベースに登録したレコードの階層化ができます。
参考記事のようにGemfileに書いてbundle installとコマンドを打つと導入できます。
(Gemfile)
gem 'ancestry'
さらに階層化を実装したいテーブルのモデルに以下の記述を追加します。
class [階層化したいテーブル] < ApplicationRecord
has_ancestry #この記述を追加することで階層化できる
end
階層化したいデータについてはdb/seeds.rbに書きます。
rake db:seedとターミナルにコマンドを打つことでデータベースに登録ができます。
lady = Category.create(name: "レディース")
men = Category.create(name: "メンズ")
:
men_jacket = men.children.create(name: "ジャケット/アウター")
:
men_jacket.children.create([{name: "テーラードジャケット"}])
ancestryを入れるとchildrenと記述することで直前の要素の子要素として扱うことができます。
つまり親(メンズ) 子(ジャケット/アウター) 孫(テーラードジャケット)の関係性をgemが作ってくれます。
データベースには以下のように登録されます。
# Categoriesテーブル
|id|name |ancestry|
|1 |レディース|Null|
|2 |メンズ |Null|
:
|30|ジャケット/アウター|2|
:
|287|テーラードジャケット|2/30|
先ほどの親ボックスについてはancestryの値がNullのデータを上から13個取り出していました。では子、孫ボックスについてはどうやってセレクトボックスを作り、入力された値によって中身を変えるのか?
それについてはAjax通信
を使います。
カテゴリボックス作成で役立ったAjax通信
子、孫ボックスについてはAjax通信を使って作成します。
親ボックス同様にインスタンス変数を定義して、その中にchildrenメソッドを入れて作成できないかと考える人もいるかもしれません。
結論から言えば「できません」
なぜならActiveRecord::Relationエラー
、すなわちchildrenメソッドは配列型のデータでは使用できないためです。
そのためjsファイル内でAjax通信の指示を書いて、親ボックスで選択されたidをコントローラーに送る必要があります。
そこでjsファイルを作成し、コードを書きました。
コントローラーではCategoryテーブルのid検索用のsearchメソッド
を定義してルーティングも設定し、respond_to
メソッドを使ってjson形式で行いたい処理を書きました。
(app/javascripts/category_form.js)
$("#parent-form").on("change",function(){
// 親ボックスのidを取得してそのidをAjax通信でコントローラーへ送る
var parentValue = document.getElementById("parent-form").value;
// ("parent-form")は親ボックスのid属性
$.ajax({
url: '/items/search',
type: "GET",
data: {
parent_id: parentValue // 親ボックスの値をparent_idという変数にする。
},
dataType: 'json'
//json形式を指定
})
(app/controllers/items_controller.rb)
def search
respond_to do |format|
format.html
format.json do
@children = Category.find(params[:parent_id]).children
#親ボックスのidから子ボックスのidの配列を作成してインスタンス変数で定義
end
end
end
加えてjbuilderファイルを作成して@childrenの中身を使いやすくします。
(app/views/items/search.json.jbuilder)
json.array! @children do |child|
json.id child.id
json.name child.name
end
そしてAjax通信が成功した(done)ときに追加したいビューにjbuilderファイルで定義した変数を入れることでセレクトボックスの選択肢ができます。
セレクトボックスやdone内のソースコードについては考えてください。
ここにたどり着くまでにメンターに質問を行なったこともあり2日くらいかかりました。
あと孫のセレクトボックスも同じ要領で作ることができます。
ただしsearchメソッド内で親ボックスのidをAjax通信で受け取った場合とそうでない場合とで条件分岐する必要があります。
成功条件としては以下のようになります。
- 親ボックスの値を選んだとき子ボックスが非同期で出現する。
- 子ボックスの値を選んだとき孫ボックスが非同期で出現する。
- 孫ボックスの値を選んだときサイズとブランドのボックスが出現する。
- 親ボックスの中身を違う値にすると孫・サイズ・ブランドのボックスが消えて子ボックスの中身が変わっている。
- 子ボックスの中身を違う値にするとサイズ・ブランドのボックスが消えて孫ボックスの中身が変わっている。
こんな感じになっていれば出品機能のカテゴリボックスについてはOKです。
ancestryを使うと親のidから子供のidを取り出してくれるので非常に便利なgemだと思いました。
あとがき
出品機能、メンバーの協力なしには実装が難しかったです。有難かった。
特にAjax通信においてはなかなか実装できず、メンバーに助けを求めて調べてもらったことが幾度かありました。
あと出品画面に表示される画像については実際のメルカリでは最大10枚まで表示されますが、自分たちのコピーサイトでは1枚だけとしました。
複数枚表示に挑戦してみたのですが思うようにいかず、妥協することにしました。
今週は月曜日にメンバーとテストコードを作成し、火曜日以降は発表資料・最終課題説明文を作っていきます。
6/21の最終課題発表が楽しみです。
6/23 追記
最終課題発表会 無事終了しました。
最高のチームです。
個人ブログに所感を書いてみました
参考記事
[多階層カテゴリでancestryを使ったら便利すぎた]
(https://qiita.com/Sotq_17/items/120256209993fb05ebac)
[Railsで木構造を扱うには]
(https://techracho.bpsinc.jp/hira/2018_03_15/53872)
[jbuilderの書き方]
(https://qiita.com/ryouzi/items/06cb0d4aa7b6527b3645)
[GitHub(ancestry)]
(https://github.com/stefankroes/ancestry)