require 'lotus/helpers/form_helper/html_node' require 'lotus/helpers/form_helper/values' require 'lotus/helpers/html_helper/html_builder' require 'lotus/utils/string' module Lotus module Helpers module FormHelper # Form builder # # @since 0.2.0 # # @see Lotus::Helpers::HtmlHelper::HtmlBuilder class FormBuilder < ::Lotus::Helpers::HtmlHelper::HtmlBuilder # Set of HTTP methods that are understood by web browsers # # @since 0.2.0 # @api private BROWSER_METHODS = ['GET', 'POST'].freeze # Set of HTTP methods that should NOT generate CSRF token # # @since 0.2.0 # @api private EXCLUDED_CSRF_METHODS = ['GET'].freeze # Checked attribute value # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#radio_button CHECKED = 'checked'.freeze # Selected attribute value for option # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#select SELECTED = 'selected'.freeze # Separator for accept attribute of file input # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#file_input ACCEPT_SEPARATOR = ','.freeze # Replacement for input id interpolation # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#_input_id INPUT_ID_REPLACEMENT = '-\k'.freeze # Replacement for input value interpolation # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#_value INPUT_VALUE_REPLACEMENT = '.\k'.freeze # Default value for unchecked check box # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#check_box DEFAULT_UNCHECKED_VALUE = '0'.freeze # Default value for checked check box # # @since 0.2.0 # @api private # # @see Lotus::Helpers::FormHelper::FormBuilder#check_box DEFAULT_CHECKED_VALUE = '1'.freeze # ENCTYPE_MULTIPART = 'multipart/form-data'.freeze self.html_node = ::Lotus::Helpers::FormHelper::HtmlNode # Instantiate a form builder # # @overload initialize(form, attributes, params, &blk) # Top level form # @param form [Lotus::Helpers:FormHelper::Form] the form # @param attributes [::Hash] a set of HTML attributes # @param params [Lotus::Action::Params] request params # @param blk [Proc] a block that describes the contents of the form # # @overload initialize(form, attributes, params, &blk) # Nested form # @param form [Lotus::Helpers:FormHelper::Form] the form # @param attributes [Lotus::Helpers::FormHelper::Values] user defined # values # @param blk [Proc] a block that describes the contents of the form # # @return [Lotus::Helpers::FormHelper::FormBuilder] the form builder # # @since 0.2.0 # @api private def initialize(form, attributes, context = nil, &blk) super() @context = context @blk = blk # Nested form if @context.nil? && attributes.is_a?(Values) @values = attributes @attributes = {} @name = form else @form = form @name = form.name @values = Values.new(form.values, @context.params) @attributes = attributes @verb_method = verb_method @csrf_token = csrf_token end end # Resolves all the nodes and generates the markup # # @return [Lotus::Utils::Escape::SafeString] the output # # @since 0.2.0 # @api private # # @see Lotus::Helpers::HtmlHelper::HtmlBuilder#to_s # @see http://www.rubydoc.info/gems/lotus-utils/Lotus/Utils/Escape/SafeString def to_s if toplevel? _method_override! form(@blk, @attributes) end super end # Nested fields # # The inputs generated by the wrapped block will be prefixed with the given name # It supports infinite levels of nesting. # # @param name [Symbol] the nested name, it's used to generate input # names, ids, and to lookup params to fill values. # # @since 0.2.0 # # @example Basic usage # <%= # form_for :delivery, routes.deliveries_path do # text_field :customer_name # # fields_for :address do # text_field :street # end # # submit 'Create' # end # %> # # Output: # #
# # # # # # # # # #
# # @example Multiple levels of nesting # <%= # form_for :delivery, routes.deliveries_path do # text_field :customer_name # # fields_for :address do # text_field :street # # fields_for :location do # text_field :city # text_field :country # end # end # # submit 'Create' # end # %> # # Output: # #
# # # # # # # # # # # # # #
def fields_for(name) current_name = @name @name = _input_name(name) yield ensure @name = current_name end # Label tag # # The first param content can be a Symbol that represents # the target field (Eg. :extended_title), or a String # which is used as it is. # # @param content [Symbol,String] the field name or a content string # @param attributes [Hash] HTML attributes to pass to the label tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # label :extended_title # %> # # # Output: # # # # @example Custom content # <%= # # ... # label 'Title', for: :extended_title # %> # # # Output: # # # # @example Custom "for" attribute # <%= # # ... # label :extended_title, for: 'ext-title' # %> # # # Output: # # # # @example Nested fields usage # <%= # # ... # fields_for :address do # label :city # text_field :city # end # %> # # # Output: # # # # def label(content, attributes = {}) attributes = { for: _for(content, attributes.delete(:for)) }.merge(attributes) content = case content when String, Lotus::Utils::String content else Utils::String.new(content).capitalize end super(content, attributes) end # Check box # # It renders a check box input. # # When a form is submitted, browsers don't send the value of unchecked # check boxes. If an user unchecks a check box, their browser won't send # the unchecked value. On the server side the corresponding value is # missing, so the application will assume that the user action never # happened. # # To solve this problem the form renders a hidden field with the # "unchecked value". When the user unchecks the input, the browser will # ignore it, but it will still send the value of the hidden input. See # the examples below. # # When editing a resource, the form automatically assigns the # checked="checked" attribute. # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # @option attributes [String] :checked_value (defaults to "1") # @option attributes [String] :unchecked_value (defaults to "0") # # @since 0.2.0 # # @example Basic usage # <%= # check_box :free_shipping # %> # # # Output: # # # # # # @example Specify (un)checked values # <%= # check_box :free_shipping, checked_value: 'true', unchecked_value: 'false' # %> # # # Output: # # # # # # @example Automatic "checked" attribute # # For this example the params are: # # # # { delivery: { free_shipping: '1' } } # <%= # check_box :free_shipping # %> # # # Output: # # # # # # @example Force "checked" attribute # # For this example the params are: # # # # { delivery: { free_shipping: '0' } } # <%= # check_box :free_shipping, checked: 'checked' # %> # # # Output: # # # # # # @example Multiple check boxes # <%= # check_box :languages, name: 'book[languages][]', value: 'italian', id: nil # check_box :languages, name: 'book[languages][]', value: 'english', id: nil # %> # # # Output: # # # # # # @example Automatic "checked" attribute for multiple check boxes # # For this example the params are: # # # # { book: { languages: ['italian'] } } # <%= # check_box :languages, name: 'book[languages][]', value: 'italian', id: nil # check_box :languages, name: 'book[languages][]', value: 'english', id: nil # %> # # # Output: # # # # def check_box(name, attributes = {}) _hidden_field_for_check_box( name, attributes) input _attributes_for_check_box(name, attributes) end # Color input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # color_field :background # %> # # # Output: # # def color_field(name, attributes = {}) input _attributes(:color, name, attributes) end # Date input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # date_field :birth_date # %> # # # Output: # # def date_field(name, attributes = {}) input _attributes(:date, name, attributes) end # Datetime input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # datetime_field :delivered_at # %> # # # Output: # # def datetime_field(name, attributes = {}) input _attributes(:datetime, name, attributes) end # Datetime Local input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # datetime_local_field :delivered_at # %> # # # Output: # # def datetime_local_field(name, attributes = {}) input _attributes(:'datetime-local', name, attributes) end # Email input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # email_field :email # %> # # # Output: # # def email_field(name, attributes = {}) input _attributes(:email, name, attributes) end # Hidden input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # hidden_field :customer_id # %> # # # Output: # # def hidden_field(name, attributes = {}) input _attributes(:hidden, name, attributes) end # File input # # PLEASE REMEMBER TO ADD enctype: 'multipart/form-data' ATTRIBUTE TO THE FORM # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # @option attributes [String,Array] :accept Optional set of accepted MIME Types # # @since 0.2.0 # # @example Basic usage # <%= # # ... # file_field :avatar # %> # # # Output: # # # # @example Accepted mime types # <%= # # ... # file_field :resume, accept: 'application/pdf,application/ms-word' # %> # # # Output: # # # # @example Accepted mime types (as array) # <%= # # ... # file_field :resume, accept: ['application/pdf', 'application/ms-word'] # %> # # # Output: # # def file_field(name, attributes = {}) attributes[:accept] = Array(attributes[:accept]).join(ACCEPT_SEPARATOR) if attributes.key?(:accept) attributes = { type: :file, name: _input_name(name), id: _input_id(name) }.merge(attributes) input(attributes) end # Number input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the number input # # @example Basic usage # <%= # # ... # number_field :percent_read # %> # # # Output: # # # # You can also make use of the 'max', 'min', and 'step' attributes for # the HTML5 number field. # # @example Advanced attributes # <%= # # ... # number_field :priority, min: 1, max: 10, step: 1 # %> # # # Output: # # def number_field(name, attributes = {}) input _attributes(:number, name, attributes) end # Text-area input # # @param name [Symbol] the input name # @param content [String] the content of the textarea # @param attributes [Hash] HTML attributes to pass to the textarea tag # # @since 0.2.5 # # @example Basic usage # <%= # # ... # text_area :hobby # %> # # # Output: # # # # @example Set content # <%= # # ... # text_area :hobby, 'Football' # %> # # # Output: # # # # @example Set content and HTML attributes # <%= # # ... # text_area :hobby, 'Football', class: 'form-control' # %> # # # Output: # # # # @example Omit content and specify HTML attributes # <%= # # ... # text_area :hobby, class: 'form-control' # %> # # # Output: # # # # @example Force blank value # <%= # # ... # text_area :hobby, '', class: 'form-control' # %> # # # Output: # # 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)}.merge(attributes) textarea(content || _value(name), attributes) end # Text input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # text_field :first_name # %> # # # Output: # # def text_field(name, attributes = {}) input _attributes(:text, name, attributes) end alias_method :input_text, :text_field # Radio input # # If request params have a value that corresponds to the given value, # it automatically sets the checked attribute. # This Lotus::Controller integration happens without any developer intervention. # # @param name [Symbol] the input name # @param value [String] the input value # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # radio_button :category, 'Fiction' # radio_button :category, 'Non-Fiction' # %> # # # Output: # # # # # # @example Automatic checked value # # Given the following params: # # # # book: { # # category: 'Non-Fiction' # # } # # <%= # # ... # radio_button :category, 'Fiction' # radio_button :category, 'Non-Fiction' # %> # # # Output: # # # # def radio_button(name, value, attributes = {}) attributes = { type: :radio, name: _input_name(name), value: value }.merge(attributes) attributes[:checked] = CHECKED if _value(name) == value input(attributes) end # Password input # # @param name [Symbol] the input name # @param attributes [Hash] HTML attributes to pass to the input tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # password_field :password # %> # # # Output: # # def password_field(name, attributes = {}) input({ type: :password, name: _input_name(name), id: _input_id(name), value: nil }.merge(attributes)) end # Select input # # @param name [Symbol] the input name # @param values [Hash] a Hash to generate tags. # Keys correspond to value and values correspond to the content. # @param attributes [Hash] HTML attributes to pass to the input tag # # If request params have a value that corresponds to one of the given values, # it automatically sets the selected attribute on the tag. # This Lotus::Controller integration happens without any developer intervention. # # @since 0.2.0 # # @example Basic usage # <%= # # ... # values = Hash['it' => 'Italy', 'us' => 'United States'] # select :stores, values # %> # # # Output: # # # # @example Automatic selected option # # Given the following params: # # # # book: { # # store: 'it' # # } # # <%= # # ... # values = Hash['it' => 'Italy', 'us' => 'United States'] # select :stores, values # %> # # # Output: # # def select(name, values, attributes = {}) options = attributes.delete(:options) || {} attributes = { name: _input_name(name), id: _input_id(name) }.merge(attributes) super(attributes) do values.each do |value, content| if _value(name) == value option(content, {value: value, selected: SELECTED}.merge(options)) else option(content, {value: value}.merge(options)) end end end end # Submit button # # @param content [String] The content # @param attributes [Hash] HTML attributes to pass to the button tag # # @since 0.2.0 # # @example Basic usage # <%= # # ... # submit 'Create' # %> # # # Output: # # def submit(content, attributes = {}) attributes = { type: :submit }.merge(attributes) button(content, attributes) end protected # A set of options to pass to the sub form helpers. # # @api private # @since 0.2.0 def options Hash[name: @name, values: @values, verb: @verb, csrf_token: @csrf_token] end private # Check the current builder is top-level # # @api private # @since 0.2.0 def toplevel? @attributes.any? end # Prepare for method override # # @api private # @since 0.2.0 def _method_override! if BROWSER_METHODS.include?(@verb_method) @attributes[:method] = @verb_method else @attributes[:method] = DEFAULT_METHOD @verb = @verb_method end end # Return the method from attributes # # @api private def verb_method (@attributes.fetch(:method) { DEFAULT_METHOD }).to_s.upcase end # Return CSRF Protection token from view context # # @api private # @since 0.2.0 def csrf_token @context.csrf_token if @context.respond_to?(:csrf_token) && !EXCLUDED_CSRF_METHODS.include?(@verb_method) end # Return a set of default HTML attributes # # @api private # @since 0.2.0 def _attributes(type, name, attributes) { type: type, name: _input_name(name), id: _input_id(name), value: _value(name) }.merge(attributes) end # Input name HTML attribute # # @api private # @since 0.2.0 def _input_name(name) "#{ @name }[#{ name }]" end # Input id HTML attribute # # @api private # @since 0.2.0 def _input_id(name) name = _input_name(name).gsub(/\[(?[[[:word:]]\-]*)\]/, INPUT_ID_REPLACEMENT) Utils::String.new(name).dasherize end # Input value HTML attribute # # @api private # @since 0.2.0 def _value(name) name = _input_name(name).gsub(/\[(?[[:word:]]*)\]/, INPUT_VALUE_REPLACEMENT) @values.get(name) end # Input for HTML attribute # # @api private # @since 0.2.0 def _for(content, name) case name when String, Lotus::Utils::String name else _input_id(name || content) end end # Hidden field for check box # # @api private # @since 0.2.0 # # @see Lotus::Helpers::FormHelper::FormBuilder#check_box def _hidden_field_for_check_box(name, attributes) if attributes[:value].nil? || !attributes[:unchecked_value].nil? input({ type: :hidden, name: attributes[:name] || _input_name(name), value: attributes.delete(:unchecked_value) || DEFAULT_UNCHECKED_VALUE }) end end # HTML attributes for check box # # @api private # @since 0.2.0 # # @see Lotus::Helpers::FormHelper::FormBuilder#check_box def _attributes_for_check_box(name, attributes) attributes = { type: :checkbox, name: _input_name(name), id: _input_id(name), value: attributes.delete(:checked_value) || DEFAULT_CHECKED_VALUE }.merge(attributes) value = _value(name) attributes[:checked] = CHECKED if value && ( value == attributes[:value] || value.include?(attributes[:value]) ) attributes end end end end end