From a6e3833fd030a9edd392d19e24d0d7069ee8e72a Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Wed, 1 Jan 2025 16:26:58 +0100 Subject: [PATCH] Extend custom FormBuilder to DRY in forms Closes #8 Closes #45 --- app/assets/stylesheets/application.css | 6 ++++ app/helpers/application_helper.rb | 48 ++++++++++++++++++++++---- app/views/units/_form.html.erb | 16 ++++----- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index ff93f0c..f481a09 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -159,6 +159,12 @@ textarea:hover { border-color: var(--color-blue); outline: solid 1px var(--color-blue); } +input:invalid, +select:invalid, +textarea:invalid { + border-color: var(--color-red); + outline: solid 1px var(--color-red); +} select:hover { cursor: pointer; } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 28aa7c5..a307908 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -56,24 +56,64 @@ module ApplicationHelper end class TabularFormBuilder < ActionView::Helpers::FormBuilder + def initialize(...) + super(...) + @default_options.merge!(@options.slice(:form)) + end + + [:text_field, :password_field, :text_area].each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(method, options = {}) + options[:maxlength] ||= object.class.type_for_attribute(method).limit + if object.errors.include?(method) + options[:pattern] = except_pattern(object.public_send(method), options[:pattern]) + end + super + end + RUBY_EVAL + end + + def number_field(method, options = {}) + value = object.public_send(method) + if value.is_a? BigDecimal + options[:value] = value.to_scientific + type = object.class.type_for_attribute(method) + options[:step] ||= BigDecimal(10).power(-type.scale) + options[:max] ||= BigDecimal(10).power(type.precision - type.scale) - options[:step] + options[:min] = options[:min] == :step ? options[:step] : options[:min] || -options[:max] + end + super + end + + def button(value = nil, options = {}, &block) + # button does not use #objectify_options + options.merge!(@options.slice(:form)) + super + end + private def submit_default_value svg_name = object ? (object.persisted? ? 'update' : 'plus-circle-outline') : '' @template.svg_tag("pictograms/#{svg_name}") + super end + + def except_pattern(value, pattern = nil) + "(?!^#{Regexp.escape(value)}$)" + (pattern || ".*") + end end def tabular_fields_for(record_name, record_object = nil, options = {}, &block) # skip_default_ids causes turbo to generate unique ID for element with [autofocus]. # Otherwise IDs are not unique when multiple forms are open and the first input gets focus. + record_object, options = nil, record_object if record_object.is_a? Hash options.merge! builder: TabularFormBuilder, skip_default_ids: true render_errors(record_name) fields_for(record_name, record_object, **options, &block) end def tabular_form_with(**options, &block) - options.merge! builder: TabularFormBuilder + options = options.deep_merge builder: TabularFormBuilder, html: {autocomplete: 'off'} form_with(**options, &block) end @@ -151,10 +191,4 @@ module ApplicationHelper def disabled_attributes(disabled) disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} end - - def number_attributes(type) - step = BigDecimal(10).power(-type.scale) - max = BigDecimal(10).power(type.precision - type.scale) - step - {min: -max, max: max, step: step} - end end diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index 59cbff1..d298d63 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -1,26 +1,22 @@ -<%= tabular_fields_for @unit do |form| %> +<%= tabular_fields_for @unit, form: form_tag do |form| %> <%- tag.tr id: row, class: "form", onkeydown: "processKey(event)", data: {link: link, form: form_tag, hidden_row: hidden_row} do %> - <%= form.text_field :symbol, form: form_tag, required: true, autofocus: true, size: 12, - maxlength: @unit.class.type_for_attribute(:symbol).limit, autocomplete: "off" %> + <%= form.text_field :symbol, required: true, autofocus: true, size: 12 %> - <%= form.text_area :description, form: form_tag, cols: 30, rows: 1, escape: false, - maxlength: @unit.class.type_for_attribute(:description).limit, autocomplete: "off" %> + <%= form.text_area :description, cols: 30, rows: 1, escape: false %> <% unless @unit.base.nil? %> - <%= form.hidden_field :base_id, form: form_tag %> - <%= form.number_field :multiplier, form: form_tag, required: true, - size: 10, autocomplete: "off", - **number_attributes(@unit.class.type_for_attribute(:multiplier)) %> + <%= form.hidden_field :base_id %> + <%= form.number_field :multiplier, required: true, size: 10 %> <% end %> - <%= form.button form: form_tag %> + <%= form.button %> <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous', name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %>