How does reject_if: :all_blank for accepts_nested_attributes_for work when working with doubly nested associations?

ⅰ亾dé卋堺 提交于 2019-12-22 06:56:35

问题


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?


回答1:


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?



回答2:


This is still an issue in Rails 4.2.4, so I figured I would share what I've learned. To promote getting a fix in Rails, see this issue and this pull request.

I based my fix on that pull request. In your case, it would look something like this (just to be clear, the three dots are just to skip over other code):

class Book < ActiveRecord::Base
  ...
  accepts_nested_attributes_for :parts, 
    reject_if: proc { |attributes| deep_blank?(attributes) }
  ...
    def self.deep_blank?(hash)
    hash.each do |key, value|
      next if key == '_destroy'
      any_blank = value.is_a?(Hash) ? deep_blank?(value) : value.blank?
      return false unless any_blank
    end
    true
  end
end


来源:https://stackoverflow.com/questions/19415205/how-does-reject-if-all-blank-for-accepts-nested-attributes-for-work-when-worki

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!