Module: RubyUIConverter::FormBuilder

Defined in:
lib/ruby_ui_converter/form_builder.rb

Overview

Translates Rails form-builder field calls (‘form.text_field :name, …`) found inside a `form_with` / `form_for` block into RubyUI form components (`Input`, `Textarea`, `Checkbox`, `FormFieldLabel`, `Button`), following the convention of building `name`/`id` as `“model”` and `value` as `model.attr`.

Only active when ‘ruby_ui?` is on and the enclosing form has a determinable model, so the name/value can be reconstructed; otherwise the calls are left untouched (and the block keeps its `|form|` builder variable).

Constant Summary collapse

INPUT_TYPES =

form field method -> input type (nil = no explicit type, like text_field)

{
  "text_field" => nil,
  "email_field" => "email",
  "password_field" => "password",
  "number_field" => "number",
  "telephone_field" => "tel",
  "phone_field" => "tel",
  "url_field" => "url",
  "search_field" => "search",
  "color_field" => "color",
  "range_field" => "range",
  "date_field" => "date",
  "datetime_field" => "datetime-local",
  "datetime_local_field" => "datetime-local",
  "time_field" => "time",
  "month_field" => "month",
  "week_field" => "week",
  "file_field" => "file"
}.freeze
TEXTAREA_METHODS =
%w[text_area textarea].freeze
CHECKBOX_METHODS =
%w[check_box checkbox].freeze

Class Method Summary collapse

Class Method Details

.attr_name(arg) ⇒ Object

“:name” / “"name"” -> “name”



238
239
240
241
242
# File 'lib/ruby_ui_converter/form_builder.rb', line 238

def attr_name(arg)
  return nil unless arg

  arg.strip.sub(/\A:/, "").gsub(/\A["']|["']\z/, "")[/\A\w+\z/]
end

.build(method, args, form) ⇒ Object

Returns a line (or array of lines) for the field, or nil when unmappable. Input/textarea/checkbox additionally get a FormFieldError reading the attribute’s backend errors, like the RubyUI form convention.



147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/ruby_ui_converter/form_builder.rb', line 147

def build(method, args, form)
  if INPUT_TYPES.key?(method)
    with_error(input_field(method, args, form), args, form)
  elsif TEXTAREA_METHODS.include?(method)
    with_error(textarea_field(args, form), args, form)
  elsif CHECKBOX_METHODS.include?(method)
    with_error(checkbox_field(args, form), args, form)
  elsif method == "label"
    label_field(args, form)
  elsif method == "submit"
    submit_button(args)
  end
end

.checkbox_field(args, form) ⇒ Object



201
202
203
204
205
206
207
208
# File 'lib/ruby_ui_converter/form_builder.rb', line 201

def checkbox_field(args, form)
  attr = attr_name(args[0])
  return nil unless attr

  parts = [%(value: "1"), name_and_id(form, attr), "checked: #{form[:model]}.#{attr}?"]
  parts.concat(args[1..] || [])
  "Checkbox(#{parts.join(", ")})"
end

.emit_collection_select(args, form, builder) ⇒ Object

form.collection_select :category_id, Category.all, :id, :name ->

NativeSelect(name:, id:) do
  Category.all.each do |option|
    NativeSelectOption(value: option.id, selected: model.category_id == option.id) { option.name }
  end
end
FormFieldError { ... }

(extra options/html_options beyond the four positionals are not carried over)



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/ruby_ui_converter/form_builder.rb', line 121

def emit_collection_select(args, form, builder)
  attr = attr_name(args[0])
  value_method = attr_name(args[2])
  text_method = attr_name(args[3])
  return false unless attr && args[1] && value_method && text_method

  collection = args[1].strip
  builder.line("NativeSelect(#{name_and_id(form, attr)}) do")
  builder.indent
  builder.line("#{collection}.each do |option|")
  builder.indent
  builder.line(
    "NativeSelectOption(value: option.#{value_method}, " \
    "selected: #{form[:model]}.#{attr} == option.#{value_method}) { option.#{text_method} }"
  )
  builder.dedent
  builder.line("end")
  builder.dedent
  builder.line("end")
  builder.line(error_line(form, attr))
  true
end

.error_line(form, attr) ⇒ Object

FormFieldError { product.errors.to_sentence.upcase_first }



168
169
170
# File 'lib/ruby_ui_converter/form_builder.rb', line 168

def error_line(form, attr)
  "FormFieldError { #{form[:model]}.errors[:#{attr}].to_sentence.upcase_first }"
end

.field_value(form, attr) ⇒ Object

HTML attribute values are strings; calling #to_s keeps Phlex happy for non-string columns (decimal/BigDecimal, integer, date, nil, …) which it would otherwise reject as invalid attribute values.



197
198
199
# File 'lib/ruby_ui_converter/form_builder.rb', line 197

def field_value(form, attr)
  "#{form[:model]}.#{attr}.to_s"
end

.form_field?(code, form) ⇒ Boolean

True when the code is a mappable form field call (so it should not be inlined as ‘{ … }` but emitted through the field translation).

Returns:

  • (Boolean)


84
85
86
87
88
89
# File 'lib/ruby_ui_converter/form_builder.rb', line 84

def form_field?(code, form)
  return false unless form

  method = code[/\A#{Regexp.escape(form[:var])}\.(\w+)/, 1]
  method && mappable_methods.include?(method)
end

.form_scope(header) ⇒ Object

Parse a ‘form_with`/`form_for` block header into a form scope (model:, param:) or nil when it isn’t a model-bound form we can map.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/ruby_ui_converter/form_builder.rb', line 47

def form_scope(header)
  return nil unless header =~ /\A(form_with|form_for)\b/

  var = header[/\bdo\s*\|\s*(\w+)\s*\|/, 1]
  return nil unless var

  model = model_expression(header)
  return nil unless model

  param = model.sub(/\A@/, "")
  return nil unless param =~ /\A\w+\z/

  { var: var, model: model, param: param }
end

.humanize(attr) ⇒ Object



248
249
250
# File 'lib/ruby_ui_converter/form_builder.rb', line 248

def humanize(attr)
  attr.tr("_", " ").capitalize
end

.input_field(method, args, form) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/ruby_ui_converter/form_builder.rb', line 172

def input_field(method, args, form)
  attr = attr_name(args[0])
  return nil unless attr

  parts = []
  if (type = INPUT_TYPES[method])
    parts << %(type: "#{type}")
  end
  parts << name_and_id(form, attr)
  parts << "value: #{field_value(form, attr)}"
  parts.concat(args[1..] || [])
  "Input(#{parts.join(", ")})"
end

.label_field(args, form) ⇒ Object



210
211
212
213
214
215
216
# File 'lib/ruby_ui_converter/form_builder.rb', line 210

def label_field(args, form)
  attr = attr_name(args[0])
  return nil unless attr

  text = string_arg?(args[1]) ? args[1].strip : %("#{humanize(attr)}")
  %(FormFieldLabel(for: "#{form[:param]}[#{attr}]") { #{text} })
end

.mappable_methodsObject

Every builder method this module knows how to translate.



41
42
43
# File 'lib/ruby_ui_converter/form_builder.rb', line 41

def mappable_methods
  INPUT_TYPES.keys + TEXTAREA_METHODS + CHECKBOX_METHODS + %w[label submit collection_select]
end

.model_expression(header) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/ruby_ui_converter/form_builder.rb', line 62

def model_expression(header)
  if (model = header[/\bmodel:\s*([^,)]+)/, 1])
    return model.strip
  end

  return nil unless header =~ /\Aform_for\b/

  rest = RailsHelpers.strip_parens(header.sub(/\Aform_for\b/, "").sub(/\bdo\b.*\z/m, "").strip)
  RailsHelpers.split_args(rest).first&.strip
end

.name_and_id(form, attr) ⇒ Object

“product” + “name” -> name: “product”, id: “product



232
233
234
235
# File 'lib/ruby_ui_converter/form_builder.rb', line 232

def name_and_id(form, attr)
  key = %("#{form[:param]}[#{attr}]")
  "name: #{key}, id: #{key}"
end

.needs_block_var?(var, codes) ⇒ Boolean

True when the children contain a ‘form.<var>` call this module won’t map, so the block variable must be kept.

Returns:

  • (Boolean)


75
76
77
78
79
80
# File 'lib/ruby_ui_converter/form_builder.rb', line 75

def needs_block_var?(var, codes)
  codes.any? do |code|
    method = code[/\A#{Regexp.escape(var)}\.(\w+)/, 1]
    method && !mappable_methods.include?(method)
  end
end

.string_arg?(arg) ⇒ Boolean

Returns:

  • (Boolean)


244
245
246
# File 'lib/ruby_ui_converter/form_builder.rb', line 244

def string_arg?(arg)
  arg && arg.strip.start_with?('"', "'")
end

.submit_button(args) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/ruby_ui_converter/form_builder.rb', line 218

def submit_button(args)
  if string_arg?(args[0])
    text = args[0].strip
    opts = args[1..] || []
  else
    text = '"Save"'
    opts = args
  end

  call = opts.empty? ? %(Button(type: "submit")) : %(Button(type: "submit", #{opts.join(", ")}))
  "#{call} { #{text} }"
end

.textarea_field(args, form) ⇒ Object



186
187
188
189
190
191
192
# File 'lib/ruby_ui_converter/form_builder.rb', line 186

def textarea_field(args, form)
  attr = attr_name(args[0])
  return nil unless attr

  parts = [name_and_id(form, attr)].concat(args[1..] || [])
  "Textarea(#{parts.join(", ")}) { #{field_value(form, attr)} }"
end

.transform(code, transformer, builder) ⇒ Object

Emit a form field call as a RubyUI component. Returns true when handled.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ruby_ui_converter/form_builder.rb', line 92

def transform(code, transformer, builder)
  form = transformer.current_form
  return false unless form

  method = code[/\A#{Regexp.escape(form[:var])}\.(\w+)/, 1]
  return false unless method

  rest = code.sub(/\A#{Regexp.escape(form[:var])}\.\w+\s*/, "")
  args = RailsHelpers.split_args(RailsHelpers.strip_parens(rest))

  # collection_select expands into a NativeSelect with a loop, so it emits
  # its (indented) block directly rather than returning flat lines.
  return emit_collection_select(args, form, builder) if method == "collection_select"

  lines = build(method, args, form)
  return false unless lines

  Array(lines).each { |line| builder.line(line) }
  true
end

.with_error(component, args, form) ⇒ Object



161
162
163
164
165
# File 'lib/ruby_ui_converter/form_builder.rb', line 161

def with_error(component, args, form)
  return nil unless component

  [component, error_line(form, attr_name(args[0]))]
end