Skip to content

Instantly share code, notes, and snippets.

@osbre
Created January 26, 2026 07:23
Show Gist options
  • Select an option

  • Save osbre/167bbfc3f90529397e397eb687e02bec to your computer and use it in GitHub Desktop.

Select an option

Save osbre/167bbfc3f90529397e397eb687e02bec to your computer and use it in GitHub Desktop.
class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
LABEL_CLASS = "block text-sm/6 font-medium text-gray-900 dark:text-white".freeze
INPUT_CLASS = "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500".freeze
TEXTAREA_CLASS = "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500".freeze
SELECT_CLASS = "col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500".freeze
CHECKBOX_CLASS = "col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:indeterminate:border-indigo-500 dark:indeterminate:bg-indigo-500 dark:focus-visible:outline-indigo-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto".freeze
RADIO_CLASS = "relative size-4 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 dark:border-white/10 dark:bg-white/5 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:focus-visible:outline-indigo-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:before:bg-white/20 forced-colors:appearance-auto forced-colors:before:hidden [&:not(:checked)]:before:hidden".freeze
SUBMIT_CLASS = "rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:focus-visible:outline-indigo-500".freeze
SECONDARY_BUTTON_CLASS = "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20".freeze
INPUT_HELPERS = %i[
text_field email_field password_field search_field telephone_field phone_field url_field
number_field date_field datetime_field datetime_local_field month_field week_field time_field
color_field range_field
].freeze
INPUT_HELPERS.each do |helper|
define_method(helper) do |method, options = {}|
super(method, with_default_class(options, INPUT_CLASS))
end
end
def label(method, text = nil, options = {}, &block)
super(method, text, with_default_class(options, LABEL_CLASS), &block)
end
def text_area(method, options = {})
super(method, with_default_class(options, TEXTAREA_CLASS))
end
def select(method, choices = nil, options = {}, html_options = {}, &block)
html_options = with_default_class(html_options, SELECT_CLASS)
select_html = super(method, choices, options, html_options, &block)
return select_html if html_options[:multiple]
select_with_icon(select_html)
end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
html_options = with_default_class(html_options, SELECT_CLASS)
select_html = super(method, collection, value_method, text_method, options, html_options)
return select_html if html_options[:multiple]
select_with_icon(select_html)
end
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
super(method, with_default_class(options, CHECKBOX_CLASS), checked_value, unchecked_value)
end
def radio_button(method, tag_value, options = {})
super(method, tag_value, with_default_class(options, RADIO_CLASS))
end
def submit(value = nil, options = {})
super(value, with_default_class(options, SUBMIT_CLASS))
end
def secondary_button(value = nil, options = {})
options = with_default_class(options, SECONDARY_BUTTON_CLASS)
options[:type] ||= "button"
@template.button_tag(value, options)
end
def checkbox_field(method, label:, description: nil, checked: nil, checked_value: "1", unchecked_value: "0", **options)
options[:checked] = checked unless checked.nil?
checkbox = check_box(method, options, checked_value, unchecked_value)
label_tag = @template.label(@object_name, method, label, class: "font-medium text-gray-900 dark:text-white")
description_tag = description ? @template.tag.p(description, class: "text-gray-500 dark:text-gray-400") : nil
text_content = @template.safe_join([ label_tag, description_tag ].compact)
checkbox_container = @template.content_tag(:div, class: "flex h-6 shrink-0 items-center") do
@template.content_tag(:div, class: "group grid size-4 grid-cols-1") do
check_icon = @template.tag.path(d: "M3 8L6 11L11 3.5", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "opacity-0 group-has-[:checked]:opacity-100")
indeterminate_icon = @template.tag.path(d: "M3 7H11", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "opacity-0 group-has-[:indeterminate]:opacity-100")
icon = @template.content_tag(:svg, @template.safe_join([ check_icon, indeterminate_icon ]), viewBox: "0 0 14 14", fill: "none", class: "pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white")
@template.safe_join([ checkbox, icon ])
end
end
text_container = @template.content_tag(:div, text_content, class: "text-sm/6")
@template.content_tag(:div, class: "flex items-start gap-3") do
@template.safe_join([ checkbox_container, text_container ])
end
end
private
def with_default_class(options, default_class)
options = options.dup
options[:class] = [ default_class, options[:class] ].compact.join(" ")
options
end
def select_with_icon(select_html)
icon = @template.content_tag(
:svg,
@template.tag.path(
d: "M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z",
"clip-rule": "evenodd",
"fill-rule": "evenodd"
),
viewBox: "0 0 16 16",
fill: "currentColor",
"data-slot": "icon",
"aria-hidden": "true",
class: "pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4 dark:text-gray-400"
)
@template.content_tag(:div, class: "grid grid-cols-1") do
@template.safe_join([ select_html, icon ])
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment