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