Rails で nested_form に ActiveModel::Base な Formオブジェクトを適用する
rails で nested_form をActiveModel::Base な Formオブジェクト(DBに紐づかない Model)に適用してみたのでメモしておく。
想定するケース
- 他サービスに可変個のデータを送るため、UIから
nested_form
を用いてデータを受け取りつつ、DBには保存したくないような場合 - ネストされる側の Model は validation 等の便利機能は使いたいため、
ActiveModel::Model
を mixin する
対応方針
単にネストしたフォームに ActiveModel::Base な Formオブジェクトを適用するだけであれば、こちら のような対応で可能だが、 nested_form
を用いたネストしたフォームの動的追加・削除を実現するにはさらなるメソッドの追加が必要だった。
そこで、 nested_form
が必要とする ActiveRecord::Base
が持つ一部の処理を擬似的に再現する module を定義し、 ネストする側とネストされる側の Model にそれぞれ mixin する方法で実現した。
- ネストする側が mixin する module :
ActiveModelNestedFormAcceptable
- ネストされる側が mixin する module :
ActiveModelNestable
module の定義
ネストする側の Model が mixin するモジュール
- app/models/concerns/active_model_nested_form_acceptable.rb
accepts_nested_attributes_for
の代わりに、 ActiveModel 向けの関連付けメソッドaccepts_active_model_nested_attributes_for
を定義する。- やってることは、フォームの動的追加の際に呼ばれる
reflect_on_association
でクラス名を取得可能にするのと、フォームのパラメータをネストする側の Model に設定する際に呼ばれる代入演算子のオーバーライド。
module ActiveModelNestedFormAcceptable extend ActiveSupport::Concern included do def self.accepts_active_model_nested_attributes_for(association, klass: nil) klass ||= association.to_s.classify.constantize add_nested_association(association, klass) define_method("#{association}_attributes=") do |attributes| self.instance_variable_set( "@#{association}", attributes .select { |_, attribute| attribute[:_destroy] == 'false' } .map { |_, attribute| klass.new(attribute) } ) end end def self.add_nested_association(association, klass) @nested_attributes ||= {} @nested_attributes[association] = klass end def self.reflect_on_association(association) if @nested_attributes[association] data = { klass: @nested_attributes[association] } OpenStruct.new data else super end end end end
ネストされる側の Model が mix in するモジュール
- app/models/concerns/active_model_nestable.rb
- フォーム削除時に必要なアトリビュート
_destroy
をダミーメソッドとして追加する。
- フォーム削除時に必要なアトリビュート
module ActiveModelNestable def _destroy # NOTE: dummy method for nested_form false end def _destroy=(value) # NOTE: dummy method for nested_form end end
具体的なクラスに適用してみる
では、具体的にネストする側 HogeWebService
クラス、ネストされる側 TemporaryObject
クラスとした場合の例を以下に示す。
ネストする側のクラスに ActiveModelNestedFormAcceptable
を適用する
- app/models/hoge_web_service.rb
accepts_active_model_nested_attributes_for
でフォームオブジェクトを関連付ける
class HogeWebService < ApplicationRecord include ActiveModelNestedFormAcceptable attr_accessor :temporary_objects accepts_active_model_nested_attributes_for :temporary_objects : # インスタンス生成時に temporary_objects に initialize で渡されたパラメータが設定される end
ネストされる側のクラスに ActiveModelNestable
を適用する
- app/models/additional_destination.rb
class TemporaryObject include ActiveModel::Model include ActiveModelNestable : # validate など普通に書く end
あとは通常通り nested_form
を使うコードを書けば動くはず。