Improve form structure, add optional tooltip help icons to form fields
This commit is contained in:
@@ -334,6 +334,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* New form field structure components */
|
||||
.form-field__header {
|
||||
@apply flex items-center justify-between gap-2;
|
||||
}
|
||||
|
||||
.form-field__body {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.form-field__actions {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
@apply block text-xs text-secondary peer-disabled:text-subdued;
|
||||
}
|
||||
@@ -347,10 +360,6 @@
|
||||
@apply transition-opacity duration-300;
|
||||
@apply placeholder:text-subdued;
|
||||
|
||||
&select {
|
||||
@apply pr-8;
|
||||
}
|
||||
|
||||
@variant theme-dark {
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
@@ -358,6 +367,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.form-field__input {
|
||||
@apply pr-10 appearance-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right -0.15rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
.form-field__radio {
|
||||
@apply text-primary;
|
||||
@@ -425,7 +442,5 @@
|
||||
@variant theme-dark {
|
||||
fill: var(--color-white);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +1,38 @@
|
||||
class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
# Fields that visually inherit from "text field"
|
||||
class_attribute :text_field_helpers, default: field_helpers - [ :label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field ]
|
||||
|
||||
# Wraps "text" inputs with custom structure + base styles
|
||||
text_field_helpers.each do |selector|
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{selector}(method, options = {})
|
||||
merged_options = { class: "form-field__input" }.merge(options)
|
||||
label = build_label(method, options)
|
||||
field = super(method, merged_options)
|
||||
|
||||
build_styled_field(label, field, merged_options)
|
||||
form_options = options.slice(:label, :label_tooltip, :inline, :container_class, :required)
|
||||
html_options = options.except(:label, :label_tooltip, :inline, :container_class)
|
||||
|
||||
build_field(method, form_options, html_options) do |merged_options|
|
||||
super(method, merged_options)
|
||||
end
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
|
||||
def radio_button(method, tag_value, options = {})
|
||||
merged_options = { class: "form-field__radio" }.merge(options)
|
||||
|
||||
super(method, tag_value, merged_options)
|
||||
end
|
||||
|
||||
def select(method, choices, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, choices, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
field_options = normalize_options(options, html_options)
|
||||
|
||||
build_field(method, field_options, html_options) do |merged_html_options|
|
||||
super(method, choices, options, merged_html_options)
|
||||
end
|
||||
end
|
||||
|
||||
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, collection, value_method, text_method, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
field_options = normalize_options(options, html_options)
|
||||
|
||||
build_field(method, field_options, html_options) do |merged_html_options|
|
||||
super(method, collection, value_method, text_method, options, merged_html_options)
|
||||
end
|
||||
end
|
||||
|
||||
def money_field(amount_method, options = {})
|
||||
@@ -48,22 +44,15 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
}
|
||||
end
|
||||
|
||||
# A custom styled "toggle" switch input. Underlying input is a `check_box` (uses same API)
|
||||
def toggle(method, options = {}, checked_value = "1", unchecked_value = "0")
|
||||
if object
|
||||
id = "#{object.id}_#{object_name}_#{method}"
|
||||
name = "#{object_name}[#{method}]"
|
||||
checked = object.send(method)
|
||||
else
|
||||
id = "#{method}_toggle_id"
|
||||
name = method
|
||||
checked = options[:checked]
|
||||
end
|
||||
field_id = field_id(method)
|
||||
field_name = field_name(method)
|
||||
checked = object ? object.send(method) : options[:checked]
|
||||
|
||||
@template.render(
|
||||
ToggleComponent.new(
|
||||
id: id,
|
||||
name: name,
|
||||
id: field_id,
|
||||
name: field_name,
|
||||
checked: checked,
|
||||
disabled: options[:disabled],
|
||||
checked_value: checked_value,
|
||||
@@ -74,7 +63,6 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
end
|
||||
|
||||
def submit(value = nil, options = {})
|
||||
# Rails superclass logic to extract the submit text
|
||||
value, options = nil, value if value.is_a?(Hash)
|
||||
value ||= submit_default_value
|
||||
|
||||
@@ -88,16 +76,39 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
end
|
||||
|
||||
private
|
||||
def build_styled_field(label, field, options, remove_padding_right: false)
|
||||
if options[:inline]
|
||||
label + field
|
||||
else
|
||||
@template.tag.div class: [ "form-field", options[:container_class], ("pr-0" if remove_padding_right) ] do
|
||||
label + field
|
||||
def build_field(method, options = {}, html_options = {}, &block)
|
||||
if options[:inline] || options[:label] == false
|
||||
return yield({ class: "form-field__input" }.merge(html_options))
|
||||
end
|
||||
|
||||
label_element = build_label(method, options)
|
||||
field_element = yield({ class: "form-field__input" }.merge(html_options))
|
||||
|
||||
container_classes = ["form-field", options[:container_class]].compact
|
||||
|
||||
@template.tag.div class: container_classes do
|
||||
if options[:label_tooltip]
|
||||
@template.tag.div(class: "form-field__header") do
|
||||
label_element +
|
||||
@template.tag.div(class: "form-field__actions") do
|
||||
build_tooltip(options[:label_tooltip])
|
||||
end
|
||||
end +
|
||||
@template.tag.div(class: "form-field__body") do
|
||||
field_element
|
||||
end
|
||||
else
|
||||
@template.tag.div(class: "form-field__body") do
|
||||
label_element + field_element
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_options(options, html_options)
|
||||
options.merge(required: options[:required] || html_options[:required])
|
||||
end
|
||||
|
||||
def build_label(method, options)
|
||||
return "".html_safe unless options[:label]
|
||||
|
||||
@@ -113,4 +124,15 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
return label(method, class: "form-field__label") if label_text == true
|
||||
label(method, label_text, class: "form-field__label")
|
||||
end
|
||||
end
|
||||
|
||||
def build_tooltip(tooltip_text)
|
||||
return nil unless tooltip_text
|
||||
|
||||
@template.tag.div(data: { controller: "tooltip" }) do
|
||||
@template.safe_join([
|
||||
@template.icon("help-circle", size: "sm", color: "default", class: "cursor-help"),
|
||||
@template.tag.div(tooltip_text, role: "tooltip", data: { tooltip_target: "tooltip" }, class: "tooltip bg-gray-700 text-sm p-2 rounded w-64 text-white")
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,62 +1,85 @@
|
||||
<%# locals: (form:, amount_method:, currency_method:, **options) %>
|
||||
|
||||
<% currency_value = if options[:currency_value_override].present?
|
||||
options[:currency_value_override]
|
||||
elsif form.object && form.object.respond_to?(currency_method)
|
||||
form.object.public_send(currency_method)
|
||||
end
|
||||
options[:currency_value_override]
|
||||
elsif form.object && form.object.respond_to?(currency_method)
|
||||
form.object.public_send(currency_method)
|
||||
end
|
||||
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
|
||||
|
||||
<div class="form-field pr-0 <%= options[:container_class] %>" data-controller="money-field">
|
||||
<%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
|
||||
<%= options[:label] || t(".label") %>
|
||||
<% if options[:required] %>
|
||||
<span class="text-red-500">*</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center grow gap-1">
|
||||
<span class="text-subdued text-sm" data-money-field-target="symbol">
|
||||
<%= currency.symbol %>
|
||||
</span>
|
||||
|
||||
<%= form.number_field amount_method,
|
||||
class: "form-field__input",
|
||||
inline: true,
|
||||
placeholder: "100",
|
||||
value: if options[:value]
|
||||
sprintf("%.#{currency.default_precision}f", options[:value])
|
||||
elsif form.object && form.object.respond_to?(amount_method)
|
||||
val = form.object.public_send(amount_method)
|
||||
sprintf("%.#{currency.default_precision}f", val) if val.present?
|
||||
end,
|
||||
min: options[:min] || -99999999999999,
|
||||
max: options[:max] || 99999999999999,
|
||||
step: currency.step,
|
||||
disabled: options[:disabled],
|
||||
data: {
|
||||
"money-field-target": "amount",
|
||||
"auto-submit-form-target": ("auto" if options[:auto_submit])
|
||||
}.compact,
|
||||
required: options[:required] %>
|
||||
</div>
|
||||
|
||||
<% unless options[:hide_currency] %>
|
||||
<div>
|
||||
<%= form.select currency_method,
|
||||
Money::Currency.as_options.map(&:iso_code),
|
||||
{ inline: true, selected: currency.iso_code },
|
||||
{
|
||||
class: "w-fit pr-5 disabled:text-subdued form-field__input",
|
||||
disabled: options[:disable_currency],
|
||||
data: {
|
||||
"money-field-target": "currency",
|
||||
action: "change->money-field#handleCurrencyChange",
|
||||
"auto-submit-form-target": ("auto" if options[:auto_submit])
|
||||
}.compact
|
||||
} %>
|
||||
<div class="form-field <%= options[:container_class] %>" data-controller="money-field">
|
||||
<% if options[:label_tooltip] %>
|
||||
<div class="form-field__header">
|
||||
<%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
|
||||
<%= options[:label] || t(".label") %>
|
||||
<% if options[:required] %>
|
||||
<span class="text-red-500 ml-0.5">*</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="form-field__actions">
|
||||
<div data-controller="tooltip">
|
||||
<%= icon "help-circle", size: "sm", color: "default", class: "cursor-help" %>
|
||||
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-700 text-sm p-2 rounded w-64 text-white">
|
||||
<%= options[:label_tooltip] %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-field__body">
|
||||
<% unless options[:label_tooltip] %>
|
||||
<%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
|
||||
<%= options[:label] || t(".label") %>
|
||||
<% if options[:required] %>
|
||||
<span class="text-red-500 ml-0.5">*</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center grow gap-1">
|
||||
<span class="text-subdued text-sm" data-money-field-target="symbol">
|
||||
<%= currency.symbol %>
|
||||
</span>
|
||||
|
||||
<%= form.number_field amount_method,
|
||||
class: "form-field__input",
|
||||
inline: true,
|
||||
placeholder: "100",
|
||||
value: if options[:value]
|
||||
sprintf("%.#{currency.default_precision}f", options[:value])
|
||||
elsif form.object && form.object.respond_to?(amount_method)
|
||||
val = form.object.public_send(amount_method)
|
||||
sprintf("%.#{currency.default_precision}f", val) if val.present?
|
||||
end,
|
||||
min: options[:min] || -99999999999999,
|
||||
max: options[:max] || 99999999999999,
|
||||
step: currency.step,
|
||||
disabled: options[:disabled],
|
||||
data: {
|
||||
"money-field-target": "amount",
|
||||
"auto-submit-form-target": ("auto" if options[:auto_submit])
|
||||
}.compact,
|
||||
required: options[:required] %>
|
||||
</div>
|
||||
|
||||
<% unless options[:hide_currency] %>
|
||||
<div>
|
||||
<%= form.select currency_method,
|
||||
Money::Currency.as_options.map(&:iso_code),
|
||||
{ inline: true, selected: currency.iso_code },
|
||||
{
|
||||
class: "w-fit pr-5 disabled:text-subdued form-field__input",
|
||||
disabled: options[:disable_currency],
|
||||
data: {
|
||||
"money-field-target": "currency",
|
||||
action: "change->money-field#handleCurrencyChange",
|
||||
"auto-submit-form-target": ("auto" if options[:auto_submit])
|
||||
}.compact
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user