Skip to content

Instantly share code, notes, and snippets.

@bkildow
Last active January 15, 2017 16:01
Show Gist options
  • Save bkildow/0c1bd0db8d1e4acdbb1c to your computer and use it in GitHub Desktop.
Save bkildow/0c1bd0db8d1e4acdbb1c to your computer and use it in GitHub Desktop.
Reform 1.2.6 nested form (with support for cocoon gem)
# app/forms/graduation_form.rb
require 'concerns/nested_form'
class GraduationForm < Reform::Form
# Needed for correct behavior of virtual attributes, see https://github.com/apotonick/reform/issues/75
reform_2_0!
model :response
collection :minor_responses, populate_if_empty: MinorResponse do
include Reform::NestedForm
property :minor_name
# only save this if minor_name isn't blank
reject_if_blank :minor_name
end
end
# app/forms/concerns/nested_form.rb
# Helps facilitates nested forms. loosely based on: https://github.com/apotonick/reform/issues/86#issuecomment-73918238
# Adds the ability to exclude saving if a particular field is blank, and helper methods to make cocoon gem work.
module Reform
module NestedForm
extend ActiveSupport::Concern
included do
property :id, virtual: true
property :_destroy, virtual: true
@reject_field = []
end
def sync_hash(options)
if fields._destroy == '1' || reject_fields?
model.mark_for_destruction
end
super(options)
end
def new_record?
model.new_record?
end
def marked_for_destruction?
model.marked_for_destruction?
end
def reject_fields?
self.class.reject_field.any? { |f| fields[f].blank? }
end
class_methods do
def reject_if_blank(field)
@reject_field << field
end
def reject_field
@reject_field
end
end
end
end
# app/models/response.rb
class Response < ActiveRecord::Base
# autosave: true is needed to get mark_for_destruction
has_many :minor_responses, dependent: :destroy, autosave: true
end
@vala
Copy link

vala commented Feb 17, 2015

Hi @bkildow, Maybe what you've done with the rejection part of your script can be accomplished with validations. I don't see the difference between the reject_if_blank :minor_name line and a validates :minor_name, presence: true

@bkildow
Copy link
Author

bkildow commented Mar 3, 2015

In my use case, I want the field to show up, but not be required. When saving, I don't want to create the associated record if it is blank.

@tbrooke
Copy link

tbrooke commented Apr 16, 2015

@bkildow thanks for this just started refactoring an App that makes heavy use of cocoon to include reform - How Cocoon and reform would interact concerned me but evidently it is do able

@audionerd
Copy link

@bkildow This worked for me with Trailblazer contracts, although I was getting undefined method 'build' for #<Disposable::Twin::Collection:...>

I found a workaround by overriding Cocoon::ViewHelpers#create_object_on_association. It expects the form collection to be ActiveRecord, with a build method, but Disposable::Twin::Collection doesn't have build. So I ask the model for the raw collection's buildinstead.

# via https://github.com/nathanvda/cocoon/blob/be59abd99027b0cce25dc4246c86d60b51c5e6f2/lib/cocoon/view_helpers.rb#L133-L136
module Cocoon
  module ViewHelpers
    def create_object_on_association(f, association, instance, force_non_association_create)
      if instance.class.name == "Mongoid::Relations::Metadata" || force_non_association_create
        create_object_with_conditions(instance)
      else
        assoc_obj = nil

        if instance.collection?

          if f.object.respond_to?(:model) && f.object.send(association).is_a?(Disposable::Twin::Collection)
            # HACK! Add Disposable::Twin::Collection support
            assoc_obj = f.object.model.send(association).build
            f.object.model.send(association).delete(assoc_obj)

          else
            # assume ActiveRecord or compatible
            assoc_obj = f.object.send(association).build
            f.object.send(association).delete(assoc_obj)
          end
        else
          assoc_obj = f.object.send("build_#{association}")
          f.object.send(association).delete
        end

        assoc_obj = assoc_obj.dup if assoc_obj.frozen?

        assoc_obj
      end
    end
  end
end

Also, if you get undefined method 'reflect_on_association', be sure in your contract you include Reform::Form::ActiveModel::ModelReflections.

@audionerd
Copy link

One more Trailblazer note: sync_hash is no longer part of the Reform API. So I commented out sync_hash and instead followed the instructions in the Trailblazer book re: "Removing Collection Items".

So the collection's skip_if looks like:

def skip_item?(fragment, options)
  # don't process if it's getting destroyed!
  if fragment["_destroy"] == "1"
    items.delete(item.find { |x| x.id.to_s == fragment["id"] })
    return true
  end
end

@lucaspiller
Copy link

Here is an update that works with Reform 2.2.1:

https://gist.github.com/lucaspiller/615f09bb525a14163921fd56b4b8e611

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment