rails嵌套表单的注意事项
accepts_nested_attributes_for与xxx_attributes=方法
类方法accepts_nested_attributes_for主要做的就是为指定的accept的关联对象,定义"#{association_name}_attributes="方法
# activerecord-5.2.1/lib/active_record/nested_attributes.rb
def accepts_nested_attributes_for(*attr_names)
options = { allow_destroy: false, update_only: false }
options.update(attr_names.extract_options!)
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
if reflection = _reflect_on_association(association_name)
reflection.autosave = true
define_autosave_validation_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
self.nested_attributes_options = nested_attributes_options
type = (reflection.collection? ? :collection : :one_to_one)
generate_association_writer(association_name, type)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
private
def generate_association_writer(association_name, type)
generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
silence_redefinition_of_method :#{association_name}_attributes=
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
eoruby
end
定义得的"#{association_name}_attributes=",会调用"assign_nested_attributes_for_#{type}_association",使嵌套提交的"#{association_name}_attributes"能够build到关联对象上,从而最后save关联对象时能够一并save掉
# activerecord-5.2.1/lib/active_record/nested_attributes.rb
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
# ...
if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
# ...
elsif attributes["id"].present?
# ...
elsif !reject_new_record?(association_name, attributes)
# ...
end
end
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
# ...
attributes_collection.each do |attributes|
# ...
if attributes["id"].blank?
# ...
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
# ...
else
# ...
end
end
end
使用嵌套表单fields_for顺便提交对关联对象的修改,一般会配套使用accepts_nested_attributes_for。
如果没有accepts_nested_attributes_for所定义的"#{association_name}_attributes="方法,则fields_for产生的表单项的name只会是survey[questions][content],而非survey[questions_attributes][0][content](假设Survey.has_many :questions)。这样的params只能由后端另外写代码来自行处理。
# actionview-5.2.1/lib/action_view/helpers/form_helper.rb
def fields_for(record_name, record_object = nil, fields_options = {}, &block)
fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
fields_options[:builder] ||= options[:builder]
fields_options[:namespace] = options[:namespace]
fields_options[:parent_builder] = self
case record_name
when String, Symbol
if nested_attributes_association?(record_name)
return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
end
else
record_object = record_name.is_a?(Array) ? record_name.last : record_name
record_name = model_name_from_record_or_class(record_object).param_key
end
# ...
end
def nested_attributes_association?(association_name)
@object.respond_to?("#{association_name}_attributes=")
end
在controller过滤表单参数时,也是使用"#{association_name}_attributes" => [...](注意有_attributes结尾)
fields_for的hidden id
一般嵌套表单都会给每条关联记录生成一个值为该记录id的hidden input tag,使顺便提交对关联对时能更新到对应关联记录或新增关联记录
如果调用field_for时指定了include_id: false,导致生成的嵌套表单没有这个hidden input,或页面上(不知何故真的)没有了这个hidden,或controller过滤参数时没有放过:id,则提交时总会对应不到原记录,而当成新纪录来插入(见上文的attributes["id"].blank?和existing_records.detect)
# actionview-5.2.1/lib/action_view/helpers/form_helper.rb
def fields_for_with_nested_attributes(association_name, association, options, block)
# ...
if association.respond_to?(:to_ary)
explicit_child_index = options[:child_index]
output = ActiveSupport::SafeBuffer.new
association.each do |child|
# ...
output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
end
output
elsif association
fields_for_nested_model(name, association, options, block)
end
end
def fields_for_nested_model(name, object, fields_options, block)
# ...
emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
options.fetch(:include_id, true)
}
@template.fields_for(name, object, fields_options) do |f|
output = @template.capture(f, &block)
output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
output
end
end