I’ve built a custom form builder base for our application: ApplicationFormBuilder < ActionView::Helpers::FormBuilder
by subclassing ActionView’s FormBuilder as documented by Rails. I’ve added some custom options to the subclass. In particular, I’ll focus on a simple animations: true
option that will add predefined css classes appropriately to the various inputs to add micro-animations to invalid fields.
I got tripped up with a form using fields_for
. Ideally, we can set the options (animations: true
) once on the parent form object and have it apply to the entire form. However, the implementation of this method is very specific about passing only :builder
and :namespace
from the parent options
. So form inputs generated by fields_for
are not inheriting the :animations
option.
Workarounds include:
- Require passing the same options to
fields_for
. This can add a lot of unnecessary bookkeeping especially for dynamic forms that aren’t fully specified by a couple of view partials. - (I’m currently doing this) Override
fields_for
to pass in additional options. This is mostly fine, but requires duplicating a little bit of code that puts this solution at risk to any future changes to the duplicated code:
# Override default to pass :animations option to fields_for
def fields_for(record_name, record_object = nil, fields_options = {}, &block)
# copied from Rails FormBuilder#fields_for to account for variable method signature support
# rubocop:disable Style/ParallelAssignment
fields_options, record_object = record_object, nil if fields_options.nil? &&
record_object.is_a?(Hash) && record_object.extractable_options?
# rubocop:enable Style/ParallelAssignment
field_options = fields_options.reverse_merge(animations: true) if options[:animations]
super(record_name, record_object, field_options, &block)
end
In a sense, the more general problem is that the variable signature of the fields_for
method and similar make subclass overrides challenging as any override must duplicate the argument handling before being able to safely work with the arguments. But I don’t have a proposal for that!
Here, if we defined, say, inheritable_options
to be an array of keys that we want to pass to nested fields, then we could add
fields_options.reverse_merge!(options.slice(*inheritable_options))
to the source of fields_for
as a general solution that makes it easier to build and maintain feature-rich form builders without brittle method overrides.