diff --git a/Gemfile b/Gemfile
index b14f471..6a278f9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,8 +7,9 @@ unless ENV['TRAVIS']
gem 'yard', require: false
end
-gem 'lotus-utils', github: 'lotus/utils', branch: 'master' # FIXME use a stable branch
-gem 'lotus-view', github: 'lotus/view', branch: '0.3.x'
+gem 'lotus-utils', github: 'lotus/utils', branch: 'master' # FIXME use a stable branch
+gem 'lotus-controller', github: 'lotus/controller', branch: 'master' # FIXME use a stable branch
+gem 'lotus-view', github: 'lotus/view', branch: '0.3.x'
gem 'simplecov', require: false
gem 'coveralls', require: false
diff --git a/lib/lotus/helpers.rb b/lib/lotus/helpers.rb
index c5ea520..a369d65 100644
--- a/lib/lotus/helpers.rb
+++ b/lib/lotus/helpers.rb
@@ -2,6 +2,7 @@
require 'lotus/helpers/html_helper'
require 'lotus/helpers/escape_helper'
require 'lotus/helpers/routing_helper'
+require 'lotus/helpers/form_helper'
module Lotus
# View helpers for Ruby applications
@@ -21,6 +22,7 @@ def self.included(base)
include Lotus::Helpers::HtmlHelper
include Lotus::Helpers::EscapeHelper
include Lotus::Helpers::RoutingHelper
+ include Lotus::Helpers::FormHelper
end
end
end
diff --git a/lib/lotus/helpers/form_helper.rb b/lib/lotus/helpers/form_helper.rb
new file mode 100644
index 0000000..c2d9126
--- /dev/null
+++ b/lib/lotus/helpers/form_helper.rb
@@ -0,0 +1,249 @@
+require 'lotus/helpers/form_helper/form_builder'
+
+module Lotus
+ module Helpers
+ # Form builder
+ #
+ # By including Lotus::Helpers::FormHelper it will inject one public method: form_for.
+ # This is a HTML5 form builder.
+ #
+ # To understand the general HTML5 builder syntax of this framework, please
+ # consider to have a look at Lotus::Helpers::HtmlHelper documentation.
+ #
+ # This builder is independent from any template engine.
+ # This was hard to achieve without a compromise: the form helper should be
+ # used in one output block in a template or as a method in a view (see the examples below).
+ #
+ # Features:
+ #
+ # * Support for complex markup without the need of concatenation
+ # * Auto closing HTML5 tags
+ # * Support for view local variables
+ # * Method override support (PUT/PATCH/DELETE HTTP verbs aren't understood by browsers)
+ # * Automatic generation of HTML attributes for inputs: id, name, value
+ # * Allow to override HTML attributes
+ # * Extract values from request params and fill value attributes
+ # * Automatic selection of current value for radio button and select inputs
+ # * Infinite nested fields
+ #
+ # Supported tags and inputs:
+ #
+ # * color_field
+ # * date_field
+ # * datetime_field
+ # * datetime_local_field
+ # * email_field
+ # * hidden_field
+ # * file_field
+ # * fields_for
+ # * form_for
+ # * label
+ # * text_field
+ # * radio_button
+ # * select
+ # * submit
+ #
+ # @since x.x.x
+ #
+ # @see Lotus::Helpers::FormHelper#form_for
+ # @see Lotus::Helpers::HtmlHelper
+ #
+ # @example One output block (template)
+ # <%=
+ # form_for :book, routes.books_path do
+ # text_field :title
+ #
+ # submit 'Create'
+ # end
+ # %>
+ #
+ # @example Method (view)
+ # require 'lotus/helpers'
+ #
+ # class MyView
+ # include Lotus::Helpers::FormHelper
+ #
+ # def my_form
+ # form_for :book, routes.books_path do
+ # text_field :title
+ # end
+ # end
+ # end
+ #
+ # # Corresponding template:
+ # #
+ # # <%= my_form %>
+ module FormHelper
+ # Default HTTP method for form
+ #
+ # @since x.x.x
+ # @api private
+ DEFAULT_METHOD = 'POST'.freeze
+
+ # Instantiate a HTML5 form builder
+ #
+ # @param name [Symbol] the toplevel name of the form, it's used to generate
+ # input names, ids, and to lookup params to fill values.
+ # @param url [String] the form action URL
+ # @param attributes [Hash] HTML attributes to pass to the form tag
+ # @param blk [Proc] A block that describes the contents of the form
+ #
+ # @return [Lotus::Helpers::FormHelper::FormBuilder] the form builder
+ #
+ # @since x.x.x
+ #
+ # @see Lotus::Helpers::FormHelper
+ # @see Lotus::Helpers::FormHelper::FormBuilder
+ #
+ # @example Basic usage
+ # <%=
+ # form_for :book, routes.books_path, class: 'form-horizontal' do
+ # div do
+ # label :title
+ # text_field :title, class: 'form-control'
+ # end
+ #
+ # submit 'Create'
+ # end
+ # %>
+ #
+ # Output:
+ # #
+ #
+ # @example Method override
+ # <%=
+ # form_for :book, routes.book_path(id: book.id), method: :put do
+ # text_field :title
+ #
+ # submit 'Update'
+ # end
+ # %>
+ #
+ # Output:
+ # #
+ #
+ # @example Nested fields
+ # <%=
+ # form_for :delivery, routes.deliveries_path do
+ # text_field :customer_name
+ #
+ # fields_for :address do
+ # text_field :city
+ # end
+ #
+ # submit 'Create'
+ # end
+ # %>
+ #
+ # Output:
+ # #
+ #
+ # @example Form to create a new resource
+ # <%=
+ # form_for :book, routes.books_path do
+ # text_field :title
+ #
+ # submit 'Create'
+ # end
+ # %>
+ #
+ # Output:
+ # #
+ #
+ # @example Form to update an existing resource
+ # <%=
+ # form_for { book: book }, routes.book_path(id: book.id) do
+ # text_field :title
+ #
+ # submit 'Update'
+ # end
+ # %>
+ #
+ # Output:
+ # #
+ #
+ # @example Share markup betweek new and update forms
+ # module Deliveries
+ # class New
+ # include Lotus::View
+ # include Lotus::Helpers
+ #
+ # def form_values
+ # :delivery
+ # end
+ #
+ # def form_action
+ # routes.deliveries_path
+ # end
+ # end
+ #
+ # class Edit
+ # include Lotus::View
+ # include Lotus::Helpers
+ #
+ # def form_values
+ # { delivery: delivery }
+ # end
+ #
+ # def form_action
+ # routes.delivery_path(id: delivery.id)
+ # end
+ # end
+ # end
+ #
+ # # deliveries/_form.html.erb
+ # <%=
+ # form_for form_values, form_action do
+ # text_field :title
+ #
+ # submit update? ? 'Update' : 'Create'
+ # end
+ # %>
+ #
+ # # deliveries/new.html.erb
+ # <%= render partial: 'deliveries/form' %>
+ #
+ # # deliveries/edit.html.erb
+ # <%= render partial: 'deliveries/form' %>
+ def form_for(name, url, attributes = {}, &blk)
+ name, values = case name
+ when Hash
+ [name.first.first, name]
+ else
+ [name, {}]
+ end
+
+ verb = :patch if values.any?
+ attributes = { action: url, id: "#{ name }-form", method: verb || DEFAULT_METHOD }.merge(attributes)
+
+ FormBuilder.new(name, params, Values.new(values), attributes, &blk)
+ end
+ end
+ end
+end
diff --git a/lib/lotus/helpers/form_helper/form_builder.rb b/lib/lotus/helpers/form_helper/form_builder.rb
new file mode 100644
index 0000000..7d8dc73
--- /dev/null
+++ b/lib/lotus/helpers/form_helper/form_builder.rb
@@ -0,0 +1,629 @@
+require 'lotus/helpers/form_helper/html_node'
+require 'lotus/helpers/html_helper/html_builder'
+require 'lotus/utils/string'
+
+module Lotus
+ module Helpers
+ module FormHelper
+ # FIXME Don't inherit from OpenStruct
+ # TODO unify values with params
+ require 'ostruct'
+ class Values < OpenStruct
+ GET_SEPARATOR = '.'.freeze
+
+ def get(key)
+ key, *keys = key.to_s.split(GET_SEPARATOR)
+ result = self[key]
+
+ Array(keys).each do |k|
+ break if result.nil?
+
+ result = if result.respond_to?(k)
+ result.public_send(k)
+ else
+ nil
+ end
+ end
+
+ result
+ end
+
+ def update?
+ to_h.keys.count > 0
+ end
+ end
+
+ # Form builder
+ #
+ # @since x.x.x
+ #
+ # @see Lotus::Helpers::HtmlHelper::HtmlBuilder
+ class FormBuilder < ::Lotus::Helpers::HtmlHelper::HtmlBuilder
+ # Set of HTTP methods that are understood by web browsers
+ #
+ # @since x.x.x
+ # @api private
+ BROWSER_METHODS = ['GET', 'POST'].freeze
+
+ # Checked attribute value
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::FormHelper::FormBuilder#radio_button
+ CHECKED = 'checked'.freeze
+
+ # Selected attribute value for option
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::FormHelper::FormBuilder#select
+ SELECTED = 'selected'.freeze
+
+ # Separator for accept attribute of file input
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::FormHelper::FormBuilder#file_input
+ ACCEPT_SEPARATOR = ','.freeze
+
+ # Replacement for input id interpolation
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::FormHelper::FormBuilder#_input_id
+ INPUT_ID_REPLACEMENT = '-\k'.freeze
+
+ # Replacement for input value interpolation
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::FormHelper::FormBuilder#_value
+ INPUT_VALUE_REPLACEMENT = '.\k'.freeze
+
+ # ENCTYPE_MULTIPART = 'multipart/form-data'.freeze
+
+ self.html_node = ::Lotus::Helpers::FormHelper::HtmlNode
+
+ # Instantiate a form builder
+ #
+ # @param name [Symbol] the toplevel name of the form, it's used to generate
+ # input names, ids, and to lookup params to fill values.
+ # @param params [Lotus::Action::Params] the params of the request
+ # @param values [Hash] A set of values
+ # @param attributes [Hash] HTML attributes to pass to the form tag
+ # @param blk [Proc] A block that describes the contents of the form
+ #
+ # @return [Lotus::Helpers::FormHelper::FormBuilder] the form builder
+ #
+ # @since x.x.x
+ def initialize(name, params, values, attributes = {}, &blk)
+ super()
+
+ @name = name
+ @params = params
+ @values = values
+ @attributes = attributes
+ @blk = blk
+ end
+
+ # Resolves all the nodes and generates the markup
+ #
+ # @return [Lotus::Utils::Escape::SafeString] the output
+ #
+ # @since x.x.x
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @example Basic usage
+ # <%=
+ # # ...
+ # label :extended_title
+ # %>
+ #
+ # # Output:
+ # #
+ #
+ # @example Custom content
+ # <%=
+ # # ...
+ # label 'Title', for: :extended_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 = Utils::String.new(content).titleize
+
+ super(content, attributes)
+ end
+
+ # Color input
+ #
+ # @param name [Symbol] the input name
+ # @param attributes [Hash] HTML attributes to pass to the input tag
+ #
+ # @since x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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
+
+ # Text input
+ #
+ # @param name [Symbol] the input name
+ # @param attributes [Hash] HTML attributes to pass to the input tag
+ #
+ # @since x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @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
+
+ # 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 x.x.x
+ #
+ # @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 x.x.x
+ #
+ # @example Basic usage
+ # <%=
+ # # ...
+ # submit 'Create'
+ # %>
+ #
+ # # Output:
+ # #
+ def submit(content, attributes = {})
+ attributes = { type: :submit }.merge(attributes)
+ button(content, attributes)
+ end
+
+ protected
+ def update?
+ @values.update?
+ end
+
+ # A set of options to pass to the sub form helpers.
+ #
+ # @api private
+ # @since x.x.x
+ def options
+ Hash[form_name: @name, params: @params, values: @values, verb: @verb]
+ end
+
+ private
+ # Check the current builder is top-level
+ #
+ # @api private
+ # @since x.x.x
+ def toplevel?
+ @attributes.any?
+ end
+
+ # Prepare for method override
+ #
+ # @api private
+ # @since x.x.x
+ def _method_override!
+ verb = (@attributes.fetch(:method) { DEFAULT_METHOD }).to_s.upcase
+
+ if BROWSER_METHODS.include?(verb)
+ @attributes[:method] = verb
+ else
+ @attributes[:method] = DEFAULT_METHOD
+ @verb = verb
+ end
+ end
+
+ # Return a set of default HTML attributes
+ #
+ # @api private
+ # @since x.x.x
+ 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 x.x.x
+ def _input_name(name)
+ "#{ @name }[#{ name }]"
+ end
+
+ # Input id HTML attribute
+ #
+ # @api private
+ # @since x.x.x
+ 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 x.x.x
+ def _value(name)
+ name = _input_name(name).gsub(/\[(?[[:word:]]*)\]/, INPUT_VALUE_REPLACEMENT)
+ @values.get(name) || @params.get(name)
+ end
+
+ # Input for HTML attribute
+ #
+ # @api private
+ # @since x.x.x
+ def _for(content, name)
+ _input_id(name || content)
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/lotus/helpers/form_helper/html_node.rb b/lib/lotus/helpers/form_helper/html_node.rb
new file mode 100644
index 0000000..972120b
--- /dev/null
+++ b/lib/lotus/helpers/form_helper/html_node.rb
@@ -0,0 +1,63 @@
+require 'lotus/helpers/html_helper/html_node'
+
+module Lotus
+ module Helpers
+ module FormHelper
+ # HTML form node
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::HtmlHelper::HtmlNode
+ class HtmlNode < ::Lotus::Helpers::HtmlHelper::HtmlNode
+ # Initialize a new HTML form node
+ #
+ # @param name [Symbol,String] the name of the tag
+ # @param content [String,Proc,Lotus::Helpers::HtmlHelper::FormBuilder,NilClass] the optional content
+ # @param attributes [Hash,NilClass] the optional tag attributes
+ # @param options [Hash] a set of data
+ #
+ # @return [Lotus::Helpers::FormHelper::HtmlNode]
+ #
+ # @since x.x.x
+ # @api private
+ def initialize(name, content, attributes, options)
+ super
+ @builder = FormBuilder.new(
+ options.fetch(:form_name),
+ options.fetch(:params),
+ options.fetch(:values)
+ )
+ @verb = options.fetch(:verb, nil)
+ end
+
+ private
+ # Resolve the (nested) content
+ #
+ # @return [String] the content
+ #
+ # @since x.x.x
+ # @api private
+ #
+ # @see Lotus::Helpers::HtmlHelper::HtmlNode#content
+ def content
+ _method_override!
+ super
+ end
+
+ # Inject a hidden field to make Method Override possible
+ #
+ # @since x.x.x
+ # @api private
+ def _method_override!
+ return if @verb.nil?
+
+ verb = @verb
+ @builder.resolve do
+ input(type: :hidden, name: :_method, value: verb)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/lotus/helpers/html_helper/html_builder.rb b/lib/lotus/helpers/html_helper/html_builder.rb
index 17214d8..ce6c942 100644
--- a/lib/lotus/helpers/html_helper/html_builder.rb
+++ b/lib/lotus/helpers/html_helper/html_builder.rb
@@ -1,4 +1,5 @@
require 'lotus/utils' # RUBY_VERSION >= '2.2'
+require 'lotus/utils/class_attribute'
require 'lotus/utils/escape'
require 'lotus/helpers/html_helper/empty_html_node'
require 'lotus/helpers/html_helper/html_node'
@@ -149,7 +150,7 @@ class HtmlBuilder
CONTENT_TAGS.each do |tag|
class_eval %{
def #{ tag }(content = nil, attributes = nil, &blk)
- @nodes << HtmlNode.new(:#{ tag }, blk || content, attributes || content)
+ @nodes << self.class.html_node.new(:#{ tag }, blk || content, attributes || content, options)
self
end
}
@@ -164,6 +165,11 @@ def #{ tag }(attributes = nil)
}
end
+ include Utils::ClassAttribute
+
+ class_attribute :html_node
+ self.html_node = ::Lotus::Helpers::HtmlHelper::HtmlNode
+
# Initialize a new builder
#
# @return [Lotus::Helpers::HtmlHelper::HtmlBuilder] the builder
@@ -174,6 +180,9 @@ def initialize
@nodes = []
end
+ def options
+ end
+
# Define a custom tag
#
# @param name [Symbol,String] the name of the tag
@@ -217,7 +226,7 @@ def initialize
# # hello
# #
def tag(name, content = nil, attributes = nil, &blk)
- @nodes << HtmlNode.new(name, blk || content, attributes || content)
+ @nodes << HtmlNode.new(name, blk || content, attributes || content, options)
self
end
diff --git a/lib/lotus/helpers/html_helper/html_node.rb b/lib/lotus/helpers/html_helper/html_node.rb
index 28eea35..cee2a69 100644
--- a/lib/lotus/helpers/html_helper/html_node.rb
+++ b/lib/lotus/helpers/html_helper/html_node.rb
@@ -15,9 +15,10 @@ class HtmlNode < EmptyHtmlNode
# @param name [Symbol,String] the name of the tag
# @param content [String,Proc,Lotus::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content
# @param attributes [Hash,NilClass] the optional tag attributes
+ # @param options [Hash] a optional set of data
#
# @return [Lotus::Helpers::HtmlHelper::HtmlNode]
- def initialize(name, content, attributes)
+ def initialize(name, content, attributes, options = {})
@builder = HtmlBuilder.new
@name = name
@content = case content
diff --git a/test/fixtures.rb b/test/fixtures.rb
index e8c1fb7..687948e 100644
--- a/test/fixtures.rb
+++ b/test/fixtures.rb
@@ -1,4 +1,5 @@
require 'lotus/view'
+require 'lotus/controller'
require 'lotus/helpers/html_helper'
require 'lotus/helpers/escape_helper'
@@ -195,11 +196,55 @@ def details
end
end
+class FormHelperView
+ include Lotus::Helpers::FormHelper
+ attr_reader :params
+
+ def initialize(params)
+ @params = Lotus::Action::Params.new(params)
+ end
+end
+
+class Address
+ attr_reader :street
+
+ def initialize(attributes = {})
+ @street = attributes[:street]
+ end
+end
+
+class Delivery
+ attr_reader :id, :customer_id, :address
+
+ def initialize(attributes = {})
+ @id = attributes[:id]
+ @customer_id = attributes[:customer_id]
+ @address = attributes[:address]
+ end
+end
+
+class DeliveryParams < Lotus::Action::Params
+ param :delivery do
+ param :customer_id, type: Integer, presence: true
+ param :address do
+ param :street, type: String, presence: true
+ end
+ end
+end
+
module FullStack
class Routes
def self.path(name)
"/#{ name }"
end
+
+ def self.deliveries_path
+ '/deliveries'
+ end
+
+ def self.delivery_path(attrs = {})
+ "/deliveries/#{ attrs.fetch(:id) }"
+ end
end
module Views
@@ -214,6 +259,34 @@ def routing_helper_path
end
end
end
+
+ module Deliveries
+ class New
+ include TestView
+ template 'deliveries/new'
+
+ def form_values
+ :delivery
+ end
+
+ def form_action
+ routes.deliveries_path
+ end
+ end
+
+ class Edit
+ include TestView
+ template 'deliveries/edit'
+
+ def form_values
+ Hash[delivery: delivery]
+ end
+
+ def form_action
+ routes.delivery_path(id: delivery.id)
+ end
+ end
+ end
end
end
diff --git a/test/fixtures/templates/albums/new.html.erb b/test/fixtures/templates/albums/new.html.erb
new file mode 100644
index 0000000..1cb5582
--- /dev/null
+++ b/test/fixtures/templates/albums/new.html.erb
@@ -0,0 +1 @@
+<%= album_form %>
diff --git a/test/fixtures/templates/deliveries/_form.html.erb b/test/fixtures/templates/deliveries/_form.html.erb
new file mode 100644
index 0000000..496c6da
--- /dev/null
+++ b/test/fixtures/templates/deliveries/_form.html.erb
@@ -0,0 +1,23 @@
+<%=
+ form_for(form_values, form_action, class: 'form-horizontal') do
+ div class: 'form-group' do
+ label :customer
+ input_text :customer, class: 'form-control', placeholder: 'Customer', name: nil
+
+ hidden_field :customer_id
+ end
+
+ fieldset do
+ legend 'Address'
+
+ fields_for :address do
+ div class: 'form-group' do
+ label :street
+ input_text :street, class: 'form-control', placeholder: 'Street'
+ end
+ end
+ end
+
+ submit update? ? 'Update' : 'Create', class: 'btn btn-default'
+ end
+%>
diff --git a/test/fixtures/templates/deliveries/edit.html.erb b/test/fixtures/templates/deliveries/edit.html.erb
new file mode 100644
index 0000000..c6f94b7
--- /dev/null
+++ b/test/fixtures/templates/deliveries/edit.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'deliveries/form' %>
diff --git a/test/fixtures/templates/deliveries/new.html.erb b/test/fixtures/templates/deliveries/new.html.erb
new file mode 100644
index 0000000..c6f94b7
--- /dev/null
+++ b/test/fixtures/templates/deliveries/new.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'deliveries/form' %>
diff --git a/test/form_helper_test.rb b/test/form_helper_test.rb
new file mode 100644
index 0000000..80c6050
--- /dev/null
+++ b/test/form_helper_test.rb
@@ -0,0 +1,758 @@
+require 'test_helper'
+
+describe Lotus::Helpers::FormHelper do
+ let(:view) { FormHelperView.new(params) }
+ let(:params) { Hash[] }
+ let(:action) { '/books' }
+
+ #
+ # FORM
+ #
+
+ describe '#form_for' do
+ it 'renders' do
+ actual = view.form_for(:book, action).to_s
+ actual.must_equal %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action, id: 'books').to_s
+ actual.must_equal %()
+ end
+
+ it "allows to override 'method' attribute (get)" do
+ actual = view.form_for(:book, action, method: 'get').to_s
+ actual.must_equal %()
+ end
+
+ it "allows to override 'method' attribute (:get)" do
+ actual = view.form_for(:book, action, method: :get).to_s
+ actual.must_equal %()
+ end
+
+ it "allows to override 'method' attribute (GET)" do
+ actual = view.form_for(:book, action, method: 'GET').to_s
+ actual.must_equal %()
+ end
+
+ [:patch, :put, :delete].each do |verb|
+ it "allows to override 'method' attribute (#{ verb })" do
+ actual = view.form_for(:book, action, method: verb) do
+ text_field :title
+ end.to_s
+
+ actual.must_equal %()
+ end
+ end
+
+ it "allows to override 'action' attribute" do
+ actual = view.form_for(:book, action, action: '/b').to_s
+ actual.must_equal %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action, class: 'form-horizonal').to_s
+ actual.must_equal %()
+ end
+ end
+
+ #
+ # NESTED FIELDS
+ #
+
+ describe '#fields_for' do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ fields_for :categories do
+ text_field :name
+
+ fields_for :subcategories do
+ text_field :name
+ end
+
+ text_field :name2
+ end
+
+ text_field :title
+ end.to_s
+
+ actual.must_equal %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { title: 'TDD', categories: { name: 'foo', name2: 'bar', subcategories: { name: 'sub' } } }] }
+
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ fields_for :categories do
+ text_field :name
+
+ fields_for :subcategories do
+ text_field :name
+ end
+
+ text_field :name2
+ end
+
+ text_field :title
+ end.to_s
+
+ actual.must_equal %()
+ end
+ end
+ end
+
+ #
+ # INPUT FIELDS
+ #
+
+ # describe '#check_box' do
+ # it 'renders' do
+ # actual = view.form_for(:book, action) do
+ # check_box :free_shipping
+ # end.to_s
+
+ # actual.must_include %()
+ # end
+ # end
+
+ describe "#color_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ color_field :cover
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action) do
+ color_field :cover, id: 'b-cover'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'name' attribute" do
+ actual = view.form_for(:book, action) do
+ color_field :cover, name: 'cover'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ color_field :cover, value: '#ffffff'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action) do
+ color_field :cover, class: 'form-control'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { cover: value }] }
+ let(:value) { '#d3397e' }
+
+ it "renders with value" do
+ actual = view.form_for(:book, action) do
+ color_field :cover
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ color_field :cover, value: '#000000'
+ end.to_s
+
+ actual.must_include %()
+ end
+ end
+ end
+
+ describe "#date_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date, id: 'release-date'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'name' attribute" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date, name: 'release_date'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date, value: '2015-02-19'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date, class: 'form-control'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { release_date: value }] }
+ let(:value) { '2014-06-23' }
+
+ it "renders with value" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ date_field :release_date, value: '2015-03-23'
+ end.to_s
+
+ actual.must_include %()
+ end
+ end
+ end
+
+ describe "#datetime_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at, id: 'published-timestamp'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'name' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at, name: 'book[published][timestamp]'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at, value: '2015-02-19T12:50:36Z'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at, class: 'form-control'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { published_at: value }] }
+ let(:value) { '2015-02-19T12:56:31Z' }
+
+ it "renders with value" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_field :published_at, value: '2015-02-19T12:50:36Z'
+ end.to_s
+
+ actual.must_include %()
+ end
+ end
+ end
+
+ describe "#datetime_local_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at, id: 'local-release-timestamp'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'name' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at, name: 'book[release-timestamp]'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at, value: '2015-02-19T14:01:28+01:00'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at, class: 'form-control'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { released_at: value }] }
+ let(:value) { '2015-02-19T14:11:19+01:00' }
+
+ it "renders with value" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ datetime_local_field :released_at, value: '2015-02-19T14:01:28+01:00'
+ end.to_s
+
+ actual.must_include %()
+ end
+ end
+ end
+
+ describe "#email_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'id' attribute" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, id: 'publisher-email'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'name' attribute" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, name: 'book[email]'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, value: 'publisher@example.org'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify 'multiple' attribute" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, multiple: true
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to specify HTML attributes" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, class: 'form-control'
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ describe "with filled params" do
+ let(:params) { Hash[book: { publisher_email: value }] }
+ let(:value) { 'maria@publisher.org' }
+
+ it "renders with value" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "allows to override 'value' attribute" do
+ actual = view.form_for(:book, action) do
+ email_field :publisher_email, value: 'publisher@example.org'
+ end.to_s
+
+ actual.must_include %()
+ end
+ end
+ end
+
+ describe "#file_field" do
+ it "renders" do
+ actual = view.form_for(:book, action) do
+ file_field :image_cover
+ end.to_s
+
+ actual.must_include %()
+ end
+
+ it "sets 'enctype' attribute to the form"
+ # it "sets 'enctype' attribute to the form" do
+ # actual = view.form_for(:book, action) do
+ # file_field :image_cover
+ # end.to_s
+
+ # actual.must_include %()
+ end
+ end
+
+ describe 'after a failed form submission' do
+ before do
+ @params = DeliveryParams.new({ delivery: { address: { street: '5th Ave' }}})
+ @params.valid? # trigger validations
+
+ @actual = FullStack::Views::Deliveries::New.render(format: :html, params: @params)
+ end
+
+ it 'renders the form with previous values' do
+ @actual.must_include %()
+ end
+ end
+ end
+
+ describe 'form to update a resource' do
+ describe 'first page load' do
+ before do
+ @address = Address.new(street: '5th Ave')
+ @delivery = Delivery.new(id: 1, customer_id: 23, address: @address)
+ @params = DeliveryParams.new({})
+ @actual = FullStack::Views::Deliveries::Edit.render(format: :html, delivery: @delivery, params: @params)
+ end
+
+ it 'renders the form' do
+ @actual.must_include %()
+ end
+ end
+ end
+end