# frozen_string_literal: true
module Shoelace
module FormHelper
class ShoelaceInputField < ActionView::Helpers::Tags::TextField #:nodoc:
attr_reader :field_type
def initialize(field_type, *args)
super(*args)
@field_type = field_type
end
def render(&block)
options = @options.stringify_keys
value = options.fetch("value") { value_before_type_cast }
options["value"] = value if value.present?
options["size"] = options["maxlength"] unless options.key?("size")
options["type"] ||= field_type
options["class"] ||= [options["class"], Shoelace.invalid_input_class_name].compact.join(" ") if @object.respond_to?(:errors) && @object.errors[@method_name].present?
add_default_name_and_id(options)
@template_object.content_tag('sl-input', '', options, &block)
end
end
class ShoelaceColorPicker < ActionView::Helpers::Tags::ColorField #:nodoc:
RGB_VALUE_REGEX = /#[0-9a-fA-F]{6}/
def field_type; nil; end
def tag(tag_name, *args, &block)
tag_name.to_s == 'input' ? content_tag('sl-color-picker', '', *args, &block) : super
end
private
def validate_color_string(string)
string.downcase if RGB_VALUE_REGEX.match?(string)
end
end
class ShoelaceRange < ActionView::Helpers::Tags::NumberField #:nodoc:
def field_type; nil; end
def tag(tag_name, *args, &block)
tag_name.to_s == 'input' ? content_tag('sl-range', '', *args, &block) : super
end
end
class ShoelaceSwitch < ActionView::Helpers::Tags::TextField #:nodoc:
def field_type; nil; end
def render(&block)
options = @options.stringify_keys
options["value"] = options.fetch("value") { value_before_type_cast }
add_default_name_and_id(options)
@template_object.content_tag('sl-switch', @method_name.to_s.humanize, options, &block)
end
end
class ShoelaceTextArea < ActionView::Helpers::Tags::TextArea #:nodoc:
def render(&block)
options = @options.stringify_keys
options["value"] = options.fetch("value") { value_before_type_cast }
add_default_name_and_id(options)
@template_object.content_tag("sl-textarea", '', options, &block)
end
end
class ShoelaceSelect < ActionView::Helpers::Tags::Select #:nodoc:
def grouped_options_for_select(grouped_options, options)
@template_object.grouped_sl_options_for_select(grouped_options, options)
end
def options_for_select(container, options = nil)
@template_object.sl_options_for_select(container, options)
end
def select_content_tag(option_tags, _options, html_options)
html_options = html_options.stringify_keys
html_options['value'] ||= value
add_default_name_and_id(html_options)
@template_object.content_tag("sl-select", option_tags, html_options)
end
end
class ShoelaceCollectionSelect < ActionView::Helpers::Tags::CollectionSelect #:nodoc:
def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
@template_object.sl_options_from_collection_for_select(collection, value_method, text_method, selected)
end
def select_content_tag(option_tags, _options, html_options)
html_options = html_options.stringify_keys
html_options['value'] ||= value
add_default_name_and_id(html_options)
@template_object.content_tag("sl-select", option_tags, html_options)
end
end
class ShoelaceGroupedCollectionSelect < ActionView::Helpers::Tags::GroupedCollectionSelect #:nodoc:
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
@template_object.sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key)
end
def select_content_tag(option_tags, _options, html_options)
html_options = html_options.stringify_keys
html_options['value'] ||= value
add_default_name_and_id(html_options)
@template_object.content_tag("sl-select", option_tags, html_options)
end
end
class ShoelaceCheckBox < ActionView::Helpers::Tags::CheckBox #:nodoc:
def render(&block)
options = @options.stringify_keys
options["value"] = @checked_value
options["checked"] = true if input_checked?(options)
if options["multiple"]
add_default_name_and_id_for_value(@checked_value, options)
options.delete("multiple")
else
add_default_name_and_id(options)
end
include_hidden = options.delete("include_hidden") { true }
sl_checkbox_tag = if block_given?
@template_object.content_tag('sl-checkbox', '', options, &block)
else
@template_object.content_tag('sl-checkbox', @method_name.to_s.humanize, options)
end
if include_hidden
hidden_field_for_checkbox(options) + sl_checkbox_tag
else
sl_checkbox_tag
end
end
end
class ShoelaceRadioButton < ActionView::Helpers::Tags::RadioButton #:nodoc:
def render(&block)
options = @options.stringify_keys
options["value"] = @tag_value
add_default_name_and_id_for_value(@tag_value, options)
options.delete("name")
@template_object.content_tag('sl-radio', '', options.except("type"), &block)
end
end
class ShoelaceCollectionRadioButtons < ActionView::Helpers::Tags::CollectionRadioButtons #:nodoc:
class RadioButtonBuilder < Builder # :nodoc:
def label(*)
text
end
def radio_button(extra_html_options = {}, &block)
html_options = extra_html_options.merge(@input_html_options)
html_options[:skip_default_ids] = false
@template_object.sl_radio_button(@object_name, @method_name, @value, html_options, &block)
end
end
def render(&block)
render_collection_for(RadioButtonBuilder, &block)
end
private
def render_collection(&block)
html_options = @html_options.stringify_keys
html_options["value"] = value
add_default_name_and_id(html_options)
@template_object.content_tag('sl-radio-group', html_options.with_defaults(label: @method_name.humanize)) { super(&block) }
end
def hidden_field
''.html_safe
end
def render_component(builder)
builder.radio_button { builder.label }
end
end
class ShoelaceFormBuilder < ActionView::Helpers::FormBuilder #:nodoc:
{
email: :email,
number: :number,
password: :password,
search: :search,
telephone: :tel,
phone: :tel,
text: :text,
url: :url
}.each do |field_type, field_class|
# def email_field(method, **options, &block)
# ShoelaceInputField.new(:email, object_name, method, @template, options.with_defaults(label: method.to_s.humanize)).render(&block)
# end
eval <<-RUBY, nil, __FILE__, __LINE__ + 1
def #{field_type}_field(method, **options, &block)
ShoelaceInputField.new(:#{field_class}, object_name, method, @template, options.with_defaults(object: @object, label: method.to_s.humanize)).render(&block)
end
RUBY
end
def color_field(method, **options)
ShoelaceColorPicker.new(object_name, method, @template, options.with_defaults(object: @object)).render
end
alias color_picker color_field
def range_field(method, **options)
ShoelaceRange.new(object_name, method, @template, options.with_defaults(object: @object, label: method.to_s.humanize)).render
end
alias range range_field
def switch_field(method, **options, &block)
ShoelaceSwitch.new(object_name, method, @template, options.with_defaults(object: @object)).render(&block)
end
alias switch switch_field
def text_area(method, **options, &block)
ShoelaceTextArea.new(object_name, method, @template, options.with_defaults(object: @object, label: method.to_s.humanize, resize: 'auto')).render(&block)
end
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0", &block)
ShoelaceCheckBox.new(object_name, method, @template, checked_value, unchecked_value, options.merge(object: @object)).render(&block)
end
def select(method, choices = nil, options = {}, html_options = {}, &block)
ShoelaceSelect.new(object_name, method, @template, choices, options.with_defaults(object: @object), html_options.with_defaults(label: method.to_s.humanize), &block).render
end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
ShoelaceCollectionSelect.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object), html_options.with_defaults(label: method.to_s.humanize), &block).render
end
def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
ShoelaceGroupedCollectionSelect.new(object_name, method, @template, collection, group_method, group_label_method, option_key_method, option_value_method, options.with_defaults(object: @object), html_options.with_defaults(label: method.to_s.humanize)).render
end
def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
ShoelaceCollectionRadioButtons.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object), html_options).render(&block)
end
def submit(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash)
@template.sl_submit_tag(value || submit_default_value, **options)
end
end
DEFAULT_FORM_PARAMETERS = { builder: ShoelaceFormBuilder }
DIVIDER_TAG = "".html_safe
private_constant :DEFAULT_FORM_PARAMETERS, :DIVIDER_TAG
def sl_form_for(*args, **options, &block)
form_for(*args, **DEFAULT_FORM_PARAMETERS, **options, &block)
end
def sl_form_with(**args, &block)
form_with(**args, **DEFAULT_FORM_PARAMETERS, &block)
end
# Creates a submit button with the text value as the caption, with the +submit+ attribute.
def sl_submit_tag(value = 'Save changes', **options)
options = options.deep_stringify_keys
tag_options = { "type" => "submit", "variant" => "primary" }.update(options)
set_default_disable_with(value, tag_options)
content_tag('sl-button', value, tag_options)
end
# Creates a shoelace text field; use these text fields to input smaller chunks of text like a username or a search
# query.
#
# For the properties available on this tag, please refer to the official documentation:
# https://shoelace.style/components/input?id=properties
#
def sl_text_field_tag(name, value = nil, **options, &block)
content_tag('sl-input', '', { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys), &block)
end
# Returns a string of ++ tags, like +options_for_select+, but prepends a ++ tag to
# each group.
def grouped_sl_options_for_select(grouped_options, options)
body = "".html_safe
grouped_options.each_with_index do |container, index|
label, values = container
body.safe_concat(DIVIDER_TAG) if index > 0
body.safe_concat(content_tag("small", label)) if label.present?
body.safe_concat(sl_options_for_select(values, options))
end
body
end
# Accepts an enumerable (hash, array, enumerable, your type) and returns a string of +sl-option+ tags. Given
# an enumerable where the elements respond to +first+ and +last+ (such as a two-element array), the “lasts” serve
# as option values and the “firsts” as option text.
def sl_options_for_select(enumerable, options = nil)
return enumerable if String === enumerable
selected, disabled = extract_selected_and_disabled(options).map { |r| Array(r).map(&:to_s) }
enumerable.map do |element|
html_attributes = option_html_attributes(element)
text, value = option_text_and_value(element).map(&:to_s)
html_attributes[:checked] ||= selected.include?(value)
html_attributes[:disabled] ||= disabled.include?(value)
html_attributes[:value] = value
tag_builder.content_tag_string('sl-option', text, html_attributes)
end.join("\n").html_safe
end
def sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
body = "".html_safe
collection.each_with_index do |group, index|
option_tags = sl_options_from_collection_for_select(value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
body.safe_concat(DIVIDER_TAG) if index > 0
body.safe_concat(content_tag("small", value_for_collection(group, group_label_method)))
body.safe_concat(option_tags)
end
body
end
# Returns a string of ++ tags compiled by iterating over the collection and assigning the result of
# a call to the +value_method+ as the option value and the +text_method+ as the option text.
def sl_options_from_collection_for_select(collection, value_method, text_method, selected = nil)
options = collection.map do |element|
[value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
end
selected, disabled = extract_selected_and_disabled(selected)
select_deselect = {
selected: extract_values_from_collection(collection, value_method, selected),
disabled: extract_values_from_collection(collection, value_method, disabled)
}
sl_options_for_select(options, select_deselect)
end
# Returns a ++ tag for accessing a specified attribute (identified by method) on an object assigned to
# the template (identified by object). If the current value of method is +tag_value+ the radio button will be
# checked.
#
# To force the radio button to be checked pass checked: true in the options hash. You may pass HTML options there
# as well.
def sl_radio_button(object_name, method, tag_value, options = {}, &block)
ShoelaceRadioButton.new(object_name, method, self, tag_value, options).render(&block)
end
{
email: :email,
number: :number,
password: :password,
search: :search,
telephone: :tel,
phone: :tel,
url: :url
}.each do |field_type, field_class|
# def sl_email_field_tag(method, **options, &block)
# sl_text_field_tag(name, value, options.merge(type: :email))
# end
eval <<-RUBY, nil, __FILE__, __LINE__ + 1
# Creates a text field of type “#{field_type}”.
def sl_#{field_type}_field_tag(method, **options, &block)
sl_text_field_tag(name, value, options.merge(type: :#{field_class}))
end
RUBY
end
end
FormBuilder = FormHelper::ShoelaceFormBuilder
end