マルチステップ(ウィザード形式の)フォームで役立つかもしれないTips

activerecordインスタンスnew的なメソッドが欲しい。

  • モデルのインスタンスparamsで渡ってきた値を使って一時的にattributesを設定しておきたい & nested modelbuildもやって欲しい。
  • Viewにインスタンス変数を渡す際、一時的attributesを上書きしたいだけなので、update_attributes!は使えない。
    • なぜ一時的かというと、長大なマルチステップフォームだから。最終画面まで保存しちゃだめなの。

Railsでフォームを使ったウィザード形式の画面遷移を実装する際はだいたい以下のように実装してる場合が多い。

class HogehogesController < ApplicationController
  def new
    @customer = Customer.new
  end

  def update
    @customer = Customer.new(params[:customer])
    if @customer.save
    ~~~~
  end  
end

要するに画面毎にCustomernewして、@customerを生成しているのだけど、
newを通してフォームから渡ってきたparams、つまりnested_attributesを渡すと、
nested modelも勝手にbuildしてくれる。

ちなみにフォームから渡ってくるparamsはこんな感じ。

{
  "customer"=>{
    "customer_favorites_attributes"=>{
      "url"=> "fooo"
    }
  }
}

ここまでは良いのだけど、問題はCustomerが既に存在する場合。
単純に「入力」→「保存」だけの画面遷移なら、newしてる部分をCustomer.find(:id)
やらに変えれば良い。
ただ、マルチステップなフォームになった場合、途中で保存されると困るケースがある。
さらにいうとnested_attributesが渡ってくるフォームの場合。
nested modelを個別にbuildとかしたくない。

そこで期待する挙動は以下の様な形

class HogehogesController < ApplicationController
  def new
    @customer = current_or_guest_customer
  end

  def update
    # new の場合と同じ用に nanikaメソッドが 一時的に attributes を上書きしてくれて、
    # かつ nested model の build もやってくれる!
    @customer = current_or_guest_customer.nanika(params[:customer])
    if @customer.save
    ~~~~
  end  
end

前提

class Customer < ActiveRecord::Base
  has_many :favorites, class_name: "CustomerFavorite"
end

class CustomerFavorite < ActiveRecord::Base
  belongs_to :customer
end
= simple_form_for @customer, :method => :post do |f|
  = f.simple_fields_for :favorites do |favorite_form|
    = favorite_form.input :url
  • マルチステップ(入力、入力、確認画面、入力、確認画面、完了画面)フォーム

調査

期待する挙動がnewのそれに近いものでいいのだから、
インスタンス作るときに使うcreatenewメソッド見ればいいんじゃ?
ということでAPI参照〜。

これかな?
http://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create

def create(attributes = nil, &block)
  if attributes.is_a?(Array)
    attributes.collect { |attr| create(attr, &block) }
  else
    object = new(attributes, &block)
    object.save
    object
  end
end

new使っとる・・
でもどこのnewなのこれ・・・
とりあえずnewを検索検索・・
newだけ入れると候補多すぎ・・・地道に見ていく・・
このAPIもう少し絞込みの手段が欲しい・・

これっぽい
http://api.rubyonrails.org/classes/ActiveModel/Model.html#method-c-new

def initialize(params={})
  params.each do |attr, value|
    self.public_send("#{attr}=", value)
  end if params

  super()
end

ほほーpublic_sendとやらを使っているようだ。
ruby自体のObjectクラスに実装されているsendメソッドと同様に、
レシーバの持つメソッドを、シンボルや文字列で呼び出せるメソッドみたい。
(レシーバって呼び方がいまいちしっくりこないのだけど、レシーバってなんでレシーバって呼ぶんだろう。)
sendメソッドとの違いは、privateなメソッドを呼べるかどうか。
public_sendprivateなメソッドを呼び出せない。

脱線してしまったけど、とにかくこのpublic_sendmethodでselfのもつpublicなmethodを呼び出すわけですな。(◯ー大柴かっ)
ということは、selfは元々~~~~_attributes=ってメソッドを持っていることになる。
今回の前提だと、Customerにはcustomer_favorites_attributes=メソッドがあるはず。

methodsメソッドを使って調べてみる。

@customer = Customer.new
@customer.methods.grep(/customer_favorites_attributes/)
=> [
    [0] customer_favorites_attributes=(attributes) Customer
]

はいありました!

なるほど、普段何気なく使ってるnested_attributesを受け入れる仕組みが、
初めからactiverecordオブジェクトには備わっているのか〜!
それで Customer.new(params[:customer]) みたいなことが簡単にできるんだ。
めちゃくちゃ便利!

実践

実際に試してみるとbuildもやってくれてる!

@customer.favorites
=> nil

class Customer < ActiveRecord::Base
  def nanika(params = {})
    params.each do |attr, value|
      self.public_send("#{attr}=", value)
    end if params
  end
end

@customer.nanika(params)

@customer.favorites
=> [
  ・・・ 
]

普通にもっと楽なやり方ありそうだけど・・。

誰が(どこで)モデルの属性値ごとに_attributes=メソッドを生やしてるの?

メタプログラミングの領域っぽくて、それっぽい感じの場所は見つけたけど確信が得られず・・

以下それっぽいの。

_attributes=buildもやってくれてるっぽいけど、具体的にどう実装されてるんだろう・・・。

参考

WEB+DB PRESS Vol.74

WEB+DB PRESS Vol.74

パーフェクトRuby (PERFECT SERIES 6)

パーフェクトRuby (PERFECT SERIES 6)