Skip to content

Instantly share code, notes, and snippets.

@Agowan
Last active April 5, 2016 20:50
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Agowan/7665554 to your computer and use it in GitHub Desktop.
Save Agowan/7665554 to your computer and use it in GitHub Desktop.
A simple way to use has_many between form objects using Virtus.The problem I had was to get validations working with fields_for in the view, but still have the flexibility and full control using virtus instead of active record models.And I added a way of checking for sanitised args in rails 4.
# encoding: utf-8
class AlbumForm < BaseForm
has_many :songs, class_name: 'SongForm'
validates :songs, form_collection: true
end
# encoding: utf-8
# The goal with this base form is to keep the child classes
# as clean as possible
class BaseForm
include Virtus.model
include ActiveModel::Validations
include HasManyAssociation
include ActiveModel::ForbiddenAttributesProtection
def initialize(*args, &block)
sanitize_args args
super
end
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
def persist!
raise "Implement me!"
end
protected
def sanitize_args(args)
args.each do |value|
sanitize_for_mass_assignment(value)
end
end
end
# encoding: utf-8
# This could be needed if you whant to support :_destroy via the view
# Example of using destroy is rails cast #403
module Destroyable
extend ActiveSupport::Concern
# It's used in conjuction with fields_for.
# See ActionView::Helpers::FormHelper::fields_for for more info.
def _destroy
marked_for_destruction?
end
def marked_for_destruction?
@marked_for_destruction
end
def mark_for_destruction
@marked_for_destruction = true
end
end
# encoding: utf-8
class FormCollectionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, collection)
collection.each do |form|
validate_single_form(attribute, record, form)
end
end
def validate_single_form(name, record, form)
unless valid = form.valid?
form.errors.each do |attribute, message|
attribute = "#{name}.#{attribute}"
record.errors[attribute] << message
record.errors[attribute].uniq!
end
end
valid
end
end
# encoding: utf-8
module HasManyAssociation
extend ActiveSupport::Concern
protected
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
end
module ClassMethods
def has_many(name, options = {})
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
klass = extract_class_name(options) || calculate_class_name(name)
attribute name, Array[klass]
# This is a requirent in order to get fields_for working in the view.
define_method "#{name}_attributes=" do |attributes|
send("#{name}=",[]) unless send(name)
attributes.each do |k,v|
obj = klass.new(v)
send(name) << obj if obj.persisted? || !has_destroy_flag?(v)
end
end
end
def calculate_class_name(name)
"#{name.singularizee}_form".classify.constantize
end
def extract_class_name(options)
options[:class_name] and options[:class_name].constantize
end
def association_class(name)
if association = attribute_set.detect{ |a| a.name == name }
association.type.member_type
end
end
end
end
# encoding: utf-8
class SongForm < BaseForm
include Destroyable
attribute :name, String
validates :name, presence: true
end
@Agowan
Copy link
Author

Agowan commented Jan 6, 2014

If you are looking at Ryan Bates episode about dynamic forms beware that the following will not work.

f.object.send(association).klass.new

Instead use the function association_class that's defined in HasManyAssociation

f.object.class.association_class(association).new

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