# frozen_string_literal: true require "hanami/view" require_relative "values" module Hanami module Helpers module FormHelper # A range of convenient methods for building the fields within an HTML form, integrating with # request params and template locals to populate the fields with appropriate values. # # @see FormHelper#form_for # # @api public # @since 2.1.0 class FormBuilder # Set of HTTP methods that are understood by web browsers # # @since 2.1.0 # @api private BROWSER_METHODS = %w[GET POST].freeze private_constant :BROWSER_METHODS # Set of HTTP methods that should NOT generate CSRF token # # @since 2.1.0 # @api private EXCLUDED_CSRF_METHODS = %w[GET].freeze private_constant :EXCLUDED_CSRF_METHODS # Separator for accept attribute of file input # # @since 2.1.0 # @api private # # @see #file_input ACCEPT_SEPARATOR = "," private_constant :ACCEPT_SEPARATOR # Default value for unchecked check box # # @since 2.1.0 # @api private # # @see #check_box DEFAULT_UNCHECKED_VALUE = "0" private_constant :DEFAULT_UNCHECKED_VALUE # Default value for checked check box # # @since 2.1.0 # @api private # # @see #check_box DEFAULT_CHECKED_VALUE = "1" private_constant :DEFAULT_CHECKED_VALUE # Input name separator # # @since 2.1.0 # @api private INPUT_NAME_SEPARATOR = "." private_constant :INPUT_NAME_SEPARATOR # Empty string # # @since 2.1.0 # @api private # # @see #password_field EMPTY_STRING = "" private_constant :EMPTY_STRING include Hanami::View::Helpers::EscapeHelper include Hanami::View::Helpers::TagHelper # @api private # @since 2.1.0 attr_reader :base_name private :base_name # @api private # @since 2.1.0 attr_reader :values private :values # @api private # @since 2.1.0 attr_reader :inflector private :inflector # @api private # @since 2.1.0 attr_reader :form_attributes private :form_attributes # Returns a new form builder. # # @param inflector [Dry::Inflector] the app inflector # @param base_name [String, nil] the base name to use for all fields in the form # @param values [Hanami::Helpers::FormHelper::Values] the values for the form # # @return [self] # # @see Hanami::Helpers::FormHelper#form_for # # @api private # @since 2.1.0 def initialize(inflector:, form_attributes:, base_name: nil, values: Values.new) @base_name = base_name @values = values @form_attributes = form_attributes @inflector = inflector end # @api private # @since 2.1.0 def call(content, **attributes) attributes["accept-charset"] ||= DEFAULT_CHARSET method_override, original_form_method = _form_method(attributes) csrf_token, token = _csrf_token(values, attributes) tag.form(**attributes) do (+"").tap { |inner| inner << input(type: "hidden", name: "_method", value: original_form_method) if method_override inner << input(type: "hidden", name: "_csrf_token", value: token) if csrf_token inner << content }.html_safe end end # Applies the base input name to all fields within the given block. # # This can be helpful when generating a set of nested fields. # # This is a convenience only. You can achieve the same result by including the base name at # the beginning of each input name. # # @param name [String] the base name to be used for all fields in the block # @yieldparam [FormBuilder] the form builder for the nested fields # # @example Basic usage # <% f.fields_for "address" do |fa| %> # <%= fa.text_field "street" %> # <%= fa.text_field "suburb" %> # <% end %> # # # A convenience for: # # <%= f.text_field "address.street" %> # # <%= f.text_field "address.suburb" %> # # => # # # # @example Multiple levels of nesting # <% f.fields_for "address" do |fa| %> # <%= fa.text_field "street" %> # # <% fa.fields_for "location" do |fl| %> # <%= fl.text_field "city" %> # <% end %> # <% end %> # # => # # # # @api public # @since 2.1.0 def fields_for(name, *yield_args) new_base_name = [base_name, name.to_s].compact.join(INPUT_NAME_SEPARATOR) builder = self.class.new( base_name: new_base_name, values: values, form_attributes: form_attributes, inflector: inflector ) yield(builder, *yield_args) end # Yields to the given block for each element in the matching collection value, and applies # the base input name to all fields within the block. # # Use this whenever generating form fields for an collection of nested fields. # # @param name [String] the input name, also used as the base input name for all fields # within the block # @yieldparam [FormBuilder] the form builder for the nested fields # @yieldparam [Integer] the index of the iteration over the collection, starting from zero # @yieldparam [Object] the value of the element from the collection # # @example Basic usage # <% f.fields_for_collection("addresses") do |fa| %> # <%= fa.text_field("street") %> # <% end %> # # => # # # # @example Yielding index and value # <% f.fields_for_collection("bill.addresses") do |fa, i, address| %> #
# Address id: <%= address.id %> # <%= fa.label("street") %> # <%= fa.text_field("street", data: {index: i.to_s}) %> #
# <% end %> # # => #
# Address id: 23 # # #
#
# Address id: 42 # # #
# # @api public # @since 2.1.0 def fields_for_collection(name, &block) _value(name).each_with_index do |value, index| fields_for("#{name}.#{index}", index, value, &block) end end # Returns a label tag. # # @return [String] the tag # # @overload label(field_name, **attributes) # Returns a label tag for the given field name, with a humanized version of the field name # as the tag's content. # # @param field_name [String] the field name # @param attributes [Hash] the tag attributes # # @example # <%= f.label("book.extended_title") %> # # => # # @example HTML attributes # <%= f.label("book.title", class: "form-label") %> # # => # # @overload label(content, **attributes) # Returns a label tag for the field name given as `for:`, with the given content string as # the tag's content. # # @param content [String] the tag's content # @param for [String] the field name # @param attributes [Hash] the tag attributes # # @example # <%= f.label("Title", for: "book.extended_title") %> # # => # # f.label("book.extended_title", for: "ext-title") # # => # # @overload label(field_name, **attributes, &block) # Returns a label tag for the given field name, with the return value of the given block # as the tag's content. # # @param field_name [String] the field name # @param attributes [Hash] the tag attributes # @yieldreturn [String] the tag content # # @example # <%= f.label for: "book.free_shipping" do %> # Free shipping # * # <% end %> # # # => # # # @api public # @since 2.1.0 def label(content = nil, **attributes, &block) for_attribute_given = attributes.key?(:for) attributes[:for] = _input_id(attributes[:for] || content) if content && !for_attribute_given content = inflector.humanize(content.to_s.split(INPUT_NAME_SEPARATOR).last) end tag.label(content, **attributes, &block) end # @overload fieldset(**attributes, &block) # Returns a fieldset tag. # # @param attributes [Hash] the tag's HTML attributes # @yieldreturn [String] the tag's content # # @return [String] the tag # # @example # <%= f.fieldset do %> # <%= f.legend("Author") %> # <%= f.label("author.name") %> # <%= f.text_field("author.name") %> # <% end %> # # # => #
# Author # # #
# # @since 2.1.0 # @api public def fieldset(...) # This is here only for documentation purposes tag.fieldset(...) end # Returns the tags for a check box. # # When editing a resource, the form automatically assigns the `checked` HTML attribute for # the check box tag. # # Returns a hidden input tag in preceding the check box input tag. This ensures that # unchecked values are submitted with the form. # # @param name [String] the input name # @param attributes [Hash] the HTML attributes for the check box tag # @option attributes [String] :checked_value (defaults to "1") # @option attributes [String] :unchecked_value (defaults to "0") # # @return [String] the tags # # @example Basic usage # f.check_box("delivery.free_shipping") # # # => # # # # @example HTML Attributes # f.check_box("delivery.free_shipping", class: "form-check-input") # # => # # # # @example Specifying checked and unchecked values # f.check_box("delivery.free_shipping", checked_value: "true", unchecked_value: "false") # # => # # # # @example Automatic "checked" attribute # # Given the request params: # # {delivery: {free_shipping: "1"}} # f.check_box("delivery.free_shipping") # # => # # # # @example Forcing the "checked" attribute # # Given the request params: # # {delivery: {free_shipping: "0"}} # f.check_box("deliver.free_shipping", checked: "checked") # # => # # # # @example Multiple check boxes for an array of values # f.check_box("book.languages", name: "book[languages][]", value: "italian", id: nil) # f.check_box("book.languages", name: "book[languages][]", value: "english", id: nil) # # => # # # # @example Automatic "checked" attribute for an array of values # # Given the request params: # # {book: {languages: ["italian"]}} # f.check_box("book.languages", name: "book[languages][]", value: "italian", id: nil) # f.check_box("book.languages", name: "book[languages][]", value: "english", id: nil) # # => # # # # @api public # @since 2.1.0 def check_box(name, **attributes) (+"").tap { |output| output << _hidden_field_for_check_box(name, attributes).to_s output << input(**_attributes_for_check_box(name, attributes)) }.html_safe end # Returns a color input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.color_field("user.background") # => # # @example HTML Attributes # f.color_field("user.background", class: "form-control") # => # # @api public # @since 2.1.0 def color_field(name, **attributes) input(**_attributes(:color, name, attributes)) end # Returns a date input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.date_field("user.birth_date") # # => # # @example HTML Attributes # f.date_field("user.birth_date", class: "form-control") # => # # @api public # @since 2.1.0 def date_field(name, **attributes) input(**_attributes(:date, name, attributes)) end # Returns a datetime input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.datetime_field("delivery.delivered_at") # => # # @example HTML Attributes # f.datetime_field("delivery.delivered_at", class: "form-control") # => # # @api public # @since 2.1.0 def datetime_field(name, **attributes) input(**_attributes(:datetime, name, attributes)) end # Returns a datetime-local input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.datetime_local_field("delivery.delivered_at") # => # # @example HTML Attributes # f.datetime_local_field("delivery.delivered_at", class: "form-control") # => # # @api public # @since 2.1.0 def datetime_local_field(name, **attributes) input(**_attributes(:"datetime-local", name, attributes)) end # Returns a time input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.time_field("book.release_hour") # => # # @example HTML Attributes # f.time_field("book.release_hour", class: "form-control") # => # # @api public # @since 2.1.0 def time_field(name, **attributes) input(**_attributes(:time, name, attributes)) end # Returns a month input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.month_field("book.release_month") # => # # @example HTML Attributes # f.month_field("book.release_month", class: "form-control") # => # # @api public # @since 2.1.0 def month_field(name, **attributes) input(**_attributes(:month, name, attributes)) end # Returns a week input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.week_field("book.release_week") # => # # @example HTML Attributes # f.week_field("book.release_week", class: "form-control") # => # # @api public # @since 2.1.0 def week_field(name, **attributes) input(**_attributes(:week, name, attributes)) end # Returns an email input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.email_field("user.email") # => # # @example HTML Attributes # f.email_field("user.email", class: "form-control") # => # # @api public # @since 2.1.0 def email_field(name, **attributes) input(**_attributes(:email, name, attributes)) end # Returns a URL input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.url_field("user.website") # => # # @example HTML Attributes # f.url_field("user.website", class: "form-control") # => # # @api public # @since 2.1.0 def url_field(name, **attributes) attributes[:value] = sanitize_url(attributes.fetch(:value) { _value(name) }) input(**_attributes(:url, name, attributes)) end # Returns a telephone input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example # f.tel_field("user.telephone") # => # # @example HTML Attributes # f.tel_field("user.telephone", class: "form-control") # => # # @api public # @since 2.1.0 def tel_field(name, **attributes) input(**_attributes(:tel, name, attributes)) end # Returns a hidden input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example # f.hidden_field("delivery.customer_id") # => # # @api public # @since 2.1.0 def hidden_field(name, **attributes) input(**_attributes(:hidden, name, attributes)) end # Returns a file input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # @option attributes [String, Array] :accept Optional set of accepted MIME Types # @option attributes [Boolean] :multiple allow multiple file upload # # @return [String] the tag # # @example Basic usage # f.file_field("user.avatar") # => # # @example HTML Attributes # f.file_field("user.avatar", class: "avatar-upload") # => # # @example Accepted MIME Types # f.file_field("user.resume", accept: "application/pdf,application/ms-word") # => # # f.file_field("user.resume", accept: ["application/pdf", "application/ms-word"]) # => # # @example Accept multiple file uploads # f.file_field("user.resume", multiple: true) # => # # @api public # @since 2.1.0 def file_field(name, **attributes) form_attributes[:enctype] = "multipart/form-data" attributes[:accept] = Array(attributes[:accept]).join(ACCEPT_SEPARATOR) if attributes.key?(:accept) attributes = {type: :file, name: _input_name(name), id: _input_id(name), **attributes} input(**attributes) end # Returns a number input tag. # # For this tag, you can make use of the `max`, `min`, and `step` HTML attributes. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.number_field("book.percent_read") # => # # @example Advanced attributes # f.number_field("book.percent_read", min: 1, max: 100, step: 1) # => # # @api public # @since 2.1.0 def number_field(name, **attributes) input(**_attributes(:number, name, attributes)) end # Returns a range input tag. # # For this tag, you can make use of the `max`, `min`, and `step` HTML attributes. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.range_field("book.discount_percentage") # => # # @example Advanced attributes # f.range_field("book.discount_percentage", min: 1, max: 1'0, step: 1) # => # # @api public # @since 2.1.0 def range_field(name, **attributes) input(**_attributes(:range, name, attributes)) end # Returns a textarea tag. # # @param name [String] the input name # @param content [String] the content of the textarea # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.text_area("user.hobby") # => # # f.text_area "user.hobby", "Football" # => # # # @example HTML attributes # f.text_area "user.hobby", class: "form-control" # => # # @api public # @since 2.1.0 def text_area(name, content = nil, **attributes) if content.respond_to?(:to_hash) attributes = content content = nil end attributes = {name: _input_name(name), id: _input_id(name), **attributes} tag.textarea(content || _value(name), **attributes) end # Returns a text input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.text_field("user.first_name") # => # # @example HTML Attributes # f.text_field("user.first_name", class: "form-control") # => # # @api public # @since 2.1.0 def text_field(name, **attributes) input(**_attributes(:text, name, attributes)) end alias_method :input_text, :text_field # Returns a search input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.search_field("search.q") # => # # @example HTML Attributes # f.search_field("search.q", class: "form-control") # => # # @api public # @since 2.1.0 def search_field(name, **attributes) input(**_attributes(:search, name, attributes)) end # Returns a radio input tag. # # When editing a resource, the form automatically assigns the `checked` HTML attribute for # the tag. # # @param name [String] the input name # @param value [String] the input value # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.radio_button("book.category", "Fiction") # f.radio_button("book.category", "Non-Fiction") # # => # # # # @example HTML Attributes # f.radio_button("book.category", "Fiction", class: "form-check") # f.radio_button("book.category", "Non-Fiction", class: "form-check") # # => # # # # @example Automatic checked value # # Given the request params: # # {book: {category: "Non-Fiction"}} # f.radio_button("book.category", "Fiction") # f.radio_button("book.category", "Non-Fiction") # # => # # # # @api public # @since 2.1.0 def radio_button(name, value, **attributes) attributes = {type: :radio, name: _input_name(name), value: value, **attributes} attributes[:checked] = true if _value(name).to_s == value.to_s input(**attributes) end # Returns a password input tag. # # @param name [String] the input name # @param attributes [Hash] the tag's HTML attributes # # @return [String] the tag # # @example Basic usage # f.password_field("signup.password") # => # # @api public # @since 2.1.0 def password_field(name, **attributes) attrs = {type: :password, name: _input_name(name), id: _input_id(name), value: nil, **attributes} attrs[:value] = EMPTY_STRING if attrs[:value].nil? input(**attrs) end # Returns a select input tag containing option tags for the given values. # # The values should be an enumerable of pairs of content (the displayed text for the option) # and value (the value for the option) strings. # # When editing a resource, automatically assigns the `selected` HTML attribute for any # option tags matching the resource's values. # # @param name [String] the input name # @param values [Hash] a Hash to generate `