class TailwindFormBuilder < ActionView::Helpers::FormBuilder class_attribute :text_field_helpers, default: field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field] # leans on the FormBuilder class_attribute `field_helpers` # you'll want to add a method for each of the specific helpers listed here if you want to style them TEXT_FIELD_STYLE = "bg-gray-200 rounded py-2 px-4 text-bluetang font-semibold leading-tight focus:outline-none focus:bg-white".freeze SELECT_FIELD_STYLE = "block bg-gray-200 text-gray-700 py-2 px-4 rounded leading-tight focus:outline-none focus:bg-white".freeze SUBMIT_BUTTON_STYLE = "cursor-pointer shadow bg-bronze focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded hover:bg-copper".freeze text_field_helpers.each do |field_method| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{field_method}(method, options = {}) if options.delete(:tailwindified) super else text_like_field(#{field_method.inspect}, method, options) end end RUBY_EVAL end def submit(value = nil, options = {}) custom_opts, opts = partition_custom_opts(options) classes = apply_style_classes(SUBMIT_BUTTON_STYLE, custom_opts) super(value, {class: classes}.merge(opts)) end def select(method, choices = nil, options = {}, html_options = {}, &block) custom_opts, opts = partition_custom_opts(options) classes = apply_style_classes(SELECT_FIELD_STYLE, custom_opts, method) labels = labels(method, custom_opts[:label], options) field = super(method, choices, opts, html_options.merge({class: classes}), &block) labels + field end def file_field(method, options = {}) options[:class] = Array(options[:class]) << "block w-full px-1 py-[1px] text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" # You can add more classes as needed for padding, margin, etc., e.g., p-2.5 super(method, options) end private def text_like_field(field_method, object_method, options = {}) custom_opts, opts = partition_custom_opts(options) classes = apply_style_classes(TEXT_FIELD_STYLE, custom_opts, object_method) field = send(field_method, object_method, { class: classes, title: errors_for(object_method)&.join(" ") }.compact.merge(opts).merge({tailwindified: true})) labels = labels(object_method, custom_opts[:label], options) labels + field end def labels(object_method, label_options, field_options) label = tailwind_label(object_method, label_options, field_options) error_label = error_label(object_method, field_options) @template.content_tag("div", label + error_label, {class: "flex flex-col items-start"}) end def tailwind_label(object_method, label_options, field_options) text, label_opts = if label_options.present? [label_options[:text], label_options.except(:text)] else [object_method.to_s.titleize, {}] end label_classes = label_opts[:class] || "block text-platinum font-bold md:text-right mb-1 md:mb-0 pr-4" label_classes += " text-yellow-800 dark:text-yellow-400" if field_options[:disabled] label(object_method, text, { class: label_classes }.merge(label_opts.except(:class))) end def error_label(object_method, options) if errors_for(object_method).present? error_message = @object.errors[object_method].collect(&:titleize).join(", ") tailwind_label(object_method, {text: error_message, class: " font-bold text-red-500"}, options) end end def border_color_classes(object_method) if errors_for(object_method).present? " border-2 border-red-400 focus:border-rose-200" else " border border-platinum focus:border-yellow-700" end end def apply_style_classes(classes, custom_opts, object_method = nil) classes + border_color_classes(object_method) + " #{custom_opts[:class]}" end CUSTOM_OPTS = [:label, :class].freeze def partition_custom_opts(opts) opts.partition { |k, v| CUSTOM_OPTS.include?(k) }.map(&:to_h) end def errors_for(object_method) return unless @object.present? && object_method.present? @object.errors[object_method] end end