-
Notifications
You must be signed in to change notification settings - Fork 104
Description
Hi,
One of the great feature of StoreModel is its compatibility with Rails built in features like accepts_nested_attributes_for.
One bummer is that it won't work with basic gem like https://github.com/nathanvda/cocoon where you can easily have let's say multiple phone numbers with an Add / Trash button because they expect to have an ActiveRecord::Relation but they get a simple Array.
I'm sharing my workaround here but it would be very great to a better/deeper implementation.
Demo usage
class Member < ApplicationRecord
include StoreModel::NestedAttributes
attribute :contact, Contact.to_type
accepts_nested_attributes_for :contact, allow_destroy: true, update_only: true, reject_if: :all_blankclass Contact < ApplicationStoreModel
attribute :id, :integer
attribute :name, :string
attribute :phones, Phone.to_array_type
accepts_nested_attributes_for :phones, allow_destroy: true, update_only: true, reject_if: :all_blank
endclass Phone < ApplicationStoreModel
attribute :id, :integer
attribute :number, :string
attribute :type_id, :integer
endapp/views/shared/phones/form.html.haml
- model_class = Phone
- partial_fields = "shared/phones/form_fields"
#phones
= model_class.model_name.human(count: 2)
= f.simple_fields_for :phones do |ff|
= render partial_fields, f: ff
.phones.mb-2
= link_to_add_association f, :phones, partial: partial_fields, class: "btn btn-outline-secondary my-2" do
%i.fas.fa-plus
= t("helpers.links.add")app/views/shared/phones/form_fields.html.haml
.nested-fields
= f.input :number
= f.input :type_id
= link_to_remove_association f, class: "btn btn-outline-danger align-self-end" do
%i.fa-solid.fa-trashThat's it for the Demo. Use cocoon as if everything were standard ActiveRecord model.
Solution / Workaround to get this to work
The trick is to override the phones getter to return an extended Array type that respond to expected methods by Cocoon. The simplest way I got around it is to override the attribute class methods to re-define my getters at the same time. Then i created a base class that I make every StoreModel extends of.
config/initializers/store_model.rb
module CocoonCompatibility
extend ActiveSupport::Concern
class ArrayForm < Array
attr_reader :klass
def initialize(klass)
@klass = klass
super()
end
def build
new_item = klass.new
push new_item
new_item
end
end
included do |klass|
##
# Redefine the getter for every StoreModel array type to use the "ArrayForm"
def self.attribute(name, type = nil, **args)
super
return unless type.is_a? StoreModel::Types::Many
define_method name do
values = super()
return values if values.is_a? CocoonCompatibility::ArrayForm
array = CocoonCompatibility::ArrayForm.new(type.model_klass)
array.push(*values)
array
end
end
def self.reflect_on_association(association)
return nil unless public_method_defined? association
ActiveRecord::Reflection::HasManyReflection.new(association, nil, {}, OpenStruct.new) # rubocop:disable Style/OpenStructUse
end
def new_record?
id.nil?
end
def mark_for_destruction
@marked_for_destruction = true
end
def marked_for_destruction?
@marked_for_destruction || false
end
def _destroy
marked_for_destruction?
end
end
end
class ApplicationStoreModel
include StoreModel::Model
include StoreModel::NestedAttributes
include Draper::Decoratable # If you use the draper gem, this works.
include CocoonCompatibility
end