Module: NestedFormsHelper

Defined in:
app/helpers/nested_forms_helper.rb

Instance Method Summary collapse

Instance Method Details

#button_to_stimulus_nested_form(name, options = {}) ⇒ Object

Stimulus-based nested forms using stimulus-components/rails-nested-form
Stimulus-based nested forms — preferred for new code



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'app/helpers/nested_forms_helper.rb', line 112

def button_to_stimulus_nested_form(name, options = {})
  css_classes = []
  if options[:class].present?
    css_classes << options[:class]
  else
    css_classes += %w[btn btn-outline-primary]
  end

  button_label = if block_given?
                   yield
                 elsif options[:skip_icon].to_b
                   name
                 else
                   fa_icon('circle-plus', text: name)
                 end

  # Use the controller name from options or default to 'nested-form'
  controller_name = options[:controller] || 'nested-form'

  data_hsh = {
    action: "#{controller_name}#add"
  }.merge(options[:data] || {})

  button_tag button_label.html_safe,
             id: options[:id],
             type: 'button',
             class: css_classes.compact.uniq.join(' '),
             style: options[:style],
             data: data_hsh
end

#generate_html(form_builder, method, options = {}) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
# File 'app/helpers/nested_forms_helper.rb', line 4

def generate_html(form_builder, method, options = {})
  options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new
  options[:partial] ||= method.to_s.singularize
  options[:form_builder_local] ||= :f
  options[:partial_locals] ||= {}
  child_index = options[:child_index] || 'NEW_RECORD'
  builder_method = form_builder.respond_to?(:simple_fields_for) ? :simple_fields_for : :fields_for
  form_builder.send(builder_method, method, options[:object], child_index: child_index) do |f|
    render(partial: options[:partial],
           locals: { options[:form_builder_local] => f }.merge(options[:partial_locals]))
  end
end

Legacy nested forms (to be migrated to Stimulus).
Pass use_stimulus_append: true to omit .new-nested-form and use .rma-add-items-nested-trigger
(rma-add-items Stimulus controller). Pass stimulus_nested_trigger_class: 'my-class' for a
page-specific trigger (rma-receive-items, etc.); that controller must handle the click.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/helpers/nested_forms_helper.rb', line 60

def link_to_new_nested_form(name, form_builder, method, options = {}, instance_defaults = nil)
  instance_defaults ||= options[:defaults] || {}
  options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new(instance_defaults)
  options[:partial] ||= method.to_s.singularize
  options[:form_builder_local] ||= :f
  options[:element_id] ||= method.to_s
  options[:partial_locals] ||= {}
  html = generate_html(form_builder,
                       method,
                       object: options[:object],
                       partial: options[:partial],
                       form_builder_local: options[:form_builder_local],
                       partial_locals: options[:partial_locals],
                       child_index: options[:child_index])
  use_stimulus_append = options[:use_stimulus_append]
  stimulus_trigger_class = options[:stimulus_nested_trigger_class].to_s.presence
  css_classes = []
  if options[:class].present?
    css_classes << options[:class]
  else
    css_classes += %w[btn btn-outline-primary]
  end
  css_classes << if stimulus_trigger_class
                   stimulus_trigger_class
                 elsif use_stimulus_append
                   'rma-add-items-nested-trigger'
                 else
                   'new-nested-form'
                 end

  link_label = if block_given?
                 yield
               elsif options[:skip_icon].to_b
                 name
               else
                 fa_icon('circle-plus', text: name)
               end
  data_hsh = {
    'element-id': options[:element_id],
    partial: h(html.to_s),
    trigger: options[:trigger]
  }.merge(options[:data] || {})

  link_to link_label.html_safe, '#',
          id: options[:id],
          class: css_classes.compact.uniq.join(' '),
          style: options[:style],
          data: data_hsh
end

#nested_form_delete_icon(f, selector, label_text = nil, **opts) ⇒ Object

Keyword options (all optional): :icon, :icon_options (passed to fa_icon), :link_class, :title, :aria_label

Non-persisted branch: +icon_args+ is built from +icon_base+ (+icon_options+ dup). If both +label_text+
and +:text+ in +icon_options+ are present, +label_text+ wins (written into +icon_args[:text]+) and
Rails.logger.warn records the conflict.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/helpers/nested_forms_helper.rb', line 22

def nested_form_delete_icon(f, selector, label_text = nil, **opts)
  icon_name = (opts[:icon] || 'trash').to_s
  icon_base = (opts[:icon_options] || {}).dup
  extra_link_class = opts[:link_class].presence
  title = opts[:title]
  aria_label = opts[:aria_label]

  if f.object.present? && f.object.persisted?
    f.label '_destroy', class: 'p-0 m-0' do
      inner = fa_icon(icon_name, **icon_base) + f.check_box('_destroy')
      inner += tag.span(label_text, class: 'ms-1') if label_text.present?
      inner
    end
  else
    icon_args = icon_base.dup
    if label_text.present? && (icon_args.key?(:text) || icon_args.key?('text'))
      Rails.logger.warn(
        '[nested_form_delete_icon] label_text and icon_options[:text] were both set; ' \
        "label_text wins and replaces icon_args[:text]. label_text=#{label_text.inspect}, " \
        "icon_options=#{opts[:icon_options].inspect}, icon_base keys=#{icon_base.keys.inspect}"
      )
    end
    if label_text.present?
      icon_args.delete('text')
      icon_args[:text] = label_text
    end
    link_classes = ['btn', 'trash-remove', 'p-0', 'm-0', extra_link_class].compact.join(' ')
    link_opts = { class: link_classes, 'data-class-to-remove': selector }
    link_opts[:title] = title if title.present?
    link_opts[:'aria-label'] = aria_label if aria_label.present?
    link_to fa_icon(icon_name, **icon_args), '#', **link_opts
  end
end