Last active
April 5, 2016 20:50
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
class AlbumForm < BaseForm | |
has_many :songs, class_name: 'SongForm' | |
validates :songs, form_collection: true | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
class SongForm < BaseForm | |
include Destroyable | |
attribute :name, String | |
validates :name, presence: true | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you are looking at Ryan Bates episode about dynamic forms beware that the following will not work.
Instead use the function association_class that's defined in HasManyAssociation