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
-
.attr_name(arg) ⇒ Object
“:name” / “"name"” -> “name”.
-
.build(method, args, form) ⇒ Object
Returns a line (or array of lines) for the field, or nil when unmappable.
- .checkbox_field(args, form) ⇒ Object
-
.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).
-
.error_line(form, attr) ⇒ Object
FormFieldError { product.errors.to_sentence.upcase_first }.
-
.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.
-
.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).
-
.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.
- .humanize(attr) ⇒ Object
- .input_field(method, args, form) ⇒ Object
- .label_field(args, form) ⇒ Object
-
.mappable_methods ⇒ Object
Every builder method this module knows how to translate.
- .model_expression(header) ⇒ Object
- .name_and_id(form, attr) ⇒ Object
-
.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.
- .string_arg?(arg) ⇒ Boolean
- .submit_button(args) ⇒ Object
- .textarea_field(args, form) ⇒ Object
-
.transform(code, transformer, builder) ⇒ Object
Emit a form field call as a RubyUI component.
- .with_error(component, args, form) ⇒ Object
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" (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).
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_methods ⇒ Object
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
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.
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
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 (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 |