I have my model setup as below. Everything works fine except blank part records are allowed even if all part and chapter fields are blank.
class Book < ActiveRecord::Base
has_many :parts, inverse_of: :book
accepts_nested_attributes_for :parts, reject_if: :all_blank
end
class Part < ActiveRecord::Base
belongs_to :book, inverse_of: :parts
has_many :chapters, inverse_of: :part
accepts_nested_attributes_for :chapters, reject_if: :all_blank
end
class Chapter < ActiveRecord::Base
belongs_to :part, inverse_of: :chapters
end
Spelunking the code, :all_blank
gets replaced with proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
. So, I use that instead of :all_blank
and add in some debugging. Looks like what is happening is the part's chapters attribute is responding to blank?
with false
because it is an instantiated hash object, even though all it contains is another hash that only contains blank values:
chapters_attributes: !ruby/hash:ActionController::Parameters
'0': !ruby/hash:ActionController::Parameters
title: ''
text: ''
Is it just not meant to work this way?
I've found a workaround:
accepts_nested_attributes_for :parts, reject_if: proc { |attributes|
attributes.all? do |key, value|
key == '_destroy' || value.blank? ||
(value.is_a?(Hash) && value.all? { |key2, value2| value2.all? { |key3, value3| key3 == '_destroy' || value3.blank? } })
end
}
But I was hoping I was missing a better way to handle this.
Update 1: I tried redefining blank?
for Hash
but that causes probs.
class Hash
def blank?
:empty? || all? { |k,v| v.blank? }
end
end
Update 2: This makes :all_blank
work as I was expecting it to, but it is ugly and not well-tested.
module ActiveRecord::NestedAttributes::ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |k, v| k == '_destroy' || v.valueless? } }
end
class Object
alias_method :valueless?, :blank?
end
class Hash
def valueless?
blank? || all? { |k, v| v.valueless? }
end
end
Update 3: Doh! Update 1 had a typo in it. This version does seem to work.
class Hash
def blank?
empty? || all? { |k,v| v.blank? }
end
end
Does this have too much potential for unintended consequences to be a viable option? If this is a good option, where in my app should this code live?
When using :all_blank
with accepts_nested_attributes_for
, it will check each individual attribute to see if it is blank.
# From the api documentation
REJECT_ALL_BLANK_PROC = proc do |attributes|
attributes.all? { |key, value| key == "_destroy" || value.blank? }
end
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
The attribute of the nested association will be a hash that contains the attributes of the association. The check to see if the attribute is blank will return false
because the hash is not empty - it contains a key for each attribute of the association. This behavior will cause the reject_if: :all_blank
to return false
because of the nested association.
To work around this, you can add your own method to application_record.rb like so:
# Add an instance method to application_record.rb / active_record.rb
def all_blank?(attributes)
attributes.all? do |key, value|
key == '_destroy' || value.blank? ||
value.is_a?(Hash) && all_blank?(value)
end
end
# Then modify your model book.rb to call that method
accepts_nested_attributes_for :parts, reject_if: :all_blank?
Methods in previous answers will not work in this case -
book = Book.create({
parts_attributes: {
name: '',
chapters_attributes: {[
{ name: '', _destroy: false},
{ id: '', name: '', _destroy: false }
]}
}
})
Here we are providing an array of blank values for chapters with either fields which are blank or _destroy. If you need to reject these values too then you can use this method -
def all_blank?(attributes)
attributes.all? do |key, value|
key == '_destroy' || value.blank? ||
value.is_a?(Hash) && all_blank?(value) ||
value.is_a?(Array) && value.all? { |val| all_blank?(val) }
end
end
Here in addition to previous conditions, we have added a line, which checks if all elements in array are blank then reject it as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With