volpe’s diary

フリーランスじゃなくなったプログラマ volpe が日々便利だなぁと感じたことを中心に綴るブログです

Rails で nested_form に ActiveModel::Base な Formオブジェクトを適用する

railsnested_form をActiveModel::Base な Formオブジェクト(DBに紐づかない Model)に適用してみたのでメモしておく。

想定するケース

  • 他サービスに可変個のデータを送るため、UIから nested_form を用いてデータを受け取りつつ、DBには保存したくないような場合
  • ネストされる側の Model は validation 等の便利機能は使いたいため、 ActiveModel::Model を mixin する

f:id:volpe0104:20190826231852p:plain

対応方針

単にネストしたフォームに ActiveModel::Base な Formオブジェクトを適用するだけであれば、こちら のような対応で可能だが、 nested_form を用いたネストしたフォームの動的追加・削除を実現するにはさらなるメソッドの追加が必要だった。

そこで、 nested_form が必要とする ActiveRecord::Base が持つ一部の処理を擬似的に再現する module を定義し、 ネストする側とネストされる側の Model にそれぞれ mixin する方法で実現した。

  • ネストする側が mixin する module : ActiveModelNestedFormAcceptable
  • ネストされる側が mixin する module : ActiveModelNestable

f:id:volpe0104:20190826232414p:plain

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 を適用する

class TemporaryObject
  include ActiveModel::Model
  include ActiveModelNestable

              :

  # validate など普通に書く
end

あとは通常通り nested_form を使うコードを書けば動くはず。

参考