Created
April 28, 2023 11:18
-
-
Save ghn/fe0360072bf436f5be4bf4fb762a0d66 to your computer and use it in GitHub Desktop.
Rails customs form Builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # frozen_string_literal: true | |
| # Inspiration: https://brandnewbox.com/notes/2021/03/form-builders-in-ruby/ | |
| # | |
| # Example: | |
| # | |
| # f.input :field, options | |
| # | |
| # possible options: | |
| # - as: symbol (:boolean, :data, :float, :radio, :select, :string, :text, :file, :time) | |
| # - collection: array of values | |
| # - label: string | |
| # - hint: string | |
| # - size: symbol (:sm, :md) | |
| # - input_html: hash | |
| class AppFormBuilder < ActionView::Helpers::FormBuilder | |
| delegate :tag, :options_for_select, :select, :safe_join, to: :@template | |
| def input(method, options = {}) | |
| @form_options = options | |
| input_type = guess_input_type(method, options) | |
| case input_type | |
| when :boolean then boolean_input(method, options) | |
| when :date, :datetime then string_input(method, options, :date) | |
| when :float then string_input(method, options, :number_float) | |
| when :file then file_input(method, options) | |
| when :integer then string_input(method, options, :number_integer) | |
| when :radio then radio_buttons_input(method, options) | |
| when :range then range_input(method, options) | |
| when :select then select_input(method, options) | |
| when :string then string_input(method, options, :text) | |
| when :text then text_input(method, options) | |
| when :time then string_input(method, options, :time) | |
| else raise "Unknown input type: #{input_type}" | |
| end | |
| end | |
| def submit(value = nil, options = {}) | |
| super(value, merge_input_options({class: "btn btn-success"}, options)) | |
| end | |
| private def guess_input_type(method, options) | |
| if options[:as].present? | |
| options[:as] | |
| elsif options[:collection].present? | |
| :select | |
| elsif options[:range].present? | |
| :range | |
| else | |
| object_type_for_method(method) | |
| end | |
| end | |
| private def form_group(method, options = {}, &block) | |
| tag.div class: "form-group mt-4" do | |
| safe_join [ | |
| block.call, | |
| error_text(method), | |
| hint_text(options[:hint]) | |
| ].compact | |
| end | |
| end | |
| private def hint_text(text) | |
| return if text.nil? | |
| tag.small text, class: "form-text text-muted" | |
| end | |
| private def error_text(method) | |
| return unless has_error?(method) | |
| tag.div(@object.errors[method].join("<br />").html_safe, class: "invalid-feedback d-block") | |
| end | |
| private def object_type_for_method(method) | |
| if @object.respond_to?(:column_for_attribute) && @object.has_attribute?(method) | |
| @object.column_for_attribute(method).try(:type) | |
| elsif attribute_attachment?(method) | |
| :file | |
| else | |
| :string | |
| end | |
| end | |
| private def attribute_attachment?(method) | |
| @object.class.respond_to?(:reflect_on_all_attachments) && | |
| @object.class.reflect_on_all_attachments.find { |a| a.name == method }.present? | |
| end | |
| private def has_error?(method) | |
| return false unless @object.respond_to?(:errors) | |
| @object.errors.key?(method) | |
| end | |
| # Inputs and helpers | |
| private def file_input(method, options) | |
| label = label(method, options[:label], class: "font-weight-bold") unless options[:label] == false | |
| size = size_class(options[:size]) | |
| form_group(method, options) do | |
| safe_join [ | |
| label, | |
| tag.div(class: "custom-file #{size} d-block") do | |
| safe_join [ | |
| file_field( | |
| method, | |
| merge_input_options({class: "custom-file-input"}, options[:input_html]) | |
| ), | |
| tag.label(I18n.t("shared.placeholders.file"), | |
| class: "custom-file-label", | |
| data: {browse: I18n.t("shared.buttons.browse")}) | |
| ].compact | |
| end | |
| ].compact | |
| end | |
| end | |
| private def range_input(method, options) | |
| range = options[:range] | |
| label = label(method, options[:label], class: "font-weight-bold") unless options[:label] == false | |
| field_options = {class: "custom-range"} | |
| field_options[:min] = range.first | |
| field_options[:max] = range.last | |
| field = range_field(method, merge_input_options(field_options, options[:input_html])) | |
| marker_labels = options[:markers] || [] | |
| markers = tag.div(class: "slide-values") do | |
| safe_join [ | |
| tag.div { range.to_a.map { tag.div(class: "slide-marker") }.join.html_safe }, | |
| tag.div(class: "text-muted") { marker_labels.map { |label| tag.div(label) }.join.html_safe } | |
| ] | |
| end | |
| form_group(method, options) do | |
| safe_join [label, field, markers].compact | |
| end | |
| end | |
| private def string_input(method, options, type) | |
| label = label(method, options[:label], class: "font-weight-bold") unless options[:label] == false | |
| size = size_class(options[:size]) | |
| field = string_field( | |
| method, | |
| merge_input_options({class: "form-control #{size} #{"is-invalid" if has_error?(method)}"}, options[:input_html]), | |
| type | |
| ) | |
| if options[:text_append] | |
| form_group(method, options) do | |
| safe_join [ | |
| label, | |
| tag.div(class: "input-group #{size}") do | |
| safe_join [ | |
| field, | |
| tag.div(class: "input-group-append") do | |
| tag.span(options[:text_append], class: "input-group-text") | |
| end | |
| ].compact | |
| end | |
| ] | |
| end | |
| else | |
| form_group(method, options) do | |
| safe_join [label, field].compact | |
| end | |
| end | |
| end | |
| private def text_input(method, options) | |
| form_group(method, options) do | |
| safe_join [ | |
| (label(method, options[:label], class: "font-weight-bold") unless options[:label] == false), | |
| text_area(method, merge_input_options({class: "form-control", rows: 4}, options[:input_html])) | |
| ].compact | |
| end | |
| end | |
| private def size_class(size) | |
| raise "Unknown size: #{size}" if size.present? && %i[sm md].exclude?(size) | |
| size.nil? ? "" : "size-#{size}" | |
| end | |
| private def boolean_input(method, options = {}) | |
| form_group(method, options) do | |
| tag.div(class: "custom-control custom-checkbox") do | |
| safe_join [ | |
| check_box(method, merge_input_options({class: "custom-control-input"}, options[:input_html])), | |
| label(method, options[:label], class: "custom-control-label") | |
| ] | |
| end | |
| end | |
| end | |
| private def select_input(method, options = {}) | |
| classes = ["custom-select", "form-control", "d-block", size_class(options[:size])].compact | |
| classes << "is-invalid" if has_error?(method) | |
| collection_input(method, options) do | |
| select( | |
| @object.class.name.underscore, | |
| method, | |
| options_for_select(options[:collection], @object[method]), | |
| {}, | |
| merge_input_options( | |
| {class: classes.join(" ")}, | |
| options[:input_html] | |
| ) | |
| ) | |
| end | |
| end | |
| private def radio_buttons_input(method, options = {}) | |
| extra_options = merge_input_options( | |
| {class: "custom-control-input #{"is-invalid" if has_error?(method)}"}, | |
| options[:input_html] | |
| ) | |
| form_group(method, options) do | |
| safe_join [ | |
| (label(method, options[:label], class: "font-weight-bold") unless options[:label] == false), | |
| options[:collection].map do |label, value| | |
| tag.div(class: "custom-control custom-radio") do | |
| safe_join [ | |
| radio_button(method, value, extra_options), | |
| label("#{method}_#{value}", label, class: "custom-control-label") | |
| ] | |
| end | |
| end | |
| ].compact | |
| end | |
| end | |
| private def collection_input(method, options, &block) | |
| form_group(method, options) do | |
| safe_join [ | |
| (label(method, options[:label], class: "font-weight-bold") unless options[:label] == false), | |
| block.call | |
| ].compact | |
| end | |
| end | |
| private def string_field(method, options = {}, type = :text) | |
| case type | |
| when :date | |
| pickr_options = merge_input_options({dateFormat: "Y-m-d"}, options[:pickr_options]) | |
| options.delete(:pickr_options) | |
| date_field(method, merge_input_options(options, {data: {flatpickr: pickr_options}})) | |
| when :number_float then number_field(method, merge_input_options(options, {step: "any"})) | |
| when :number_integer then number_field(method, options) | |
| when :text | |
| case method.to_s | |
| when /email/ then email_field(method, options) | |
| when /password/ then password_field(method, options) | |
| else | |
| text_field(method, options) | |
| end | |
| when :time then time_field(method, merge_input_options(options, {value: @object[method]&.strftime("%H:%M")})) | |
| else raise "Unknown string field type: #{type}" | |
| end | |
| end | |
| private def merge_input_options(options, user_options) | |
| user_options.nil? ? options : options.merge(user_options) | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <%= f.input :email %> | |
| <%= f.input :password %> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment