Skip to content

Instantly share code, notes, and snippets.

@ghn
Created April 28, 2023 11:18
Show Gist options
  • Select an option

  • Save ghn/fe0360072bf436f5be4bf4fb762a0d66 to your computer and use it in GitHub Desktop.

Select an option

Save ghn/fe0360072bf436f5be4bf4fb762a0d66 to your computer and use it in GitHub Desktop.
Rails customs form Builder
# 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
<%= f.input :email %>
<%= f.input :password %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment