Browse files

Merge branch 'actions'

  • Loading branch information...
2 parents 35e228e + 22c54a9 commit 769c91b7a24a6fa094b11c436976d5c4bc37d6d9 @justinfrench committed Jan 15, 2012
View
54 README.textile
@@ -40,8 +40,9 @@ One day, I finally had enough, so I opened up my text editor, and wrote a DSL fo
<%= author_form.input :last_name %>
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button %>
+ <%= f.actions do %>
+ <%= f.action :submit, :as => :button %>
+ <%= f.action :cancel, :as => :link %>
<% end %>
<% end %>
@@ -139,27 +140,27 @@ h2. Usage
Forms are really boring to code... you want to get onto the good stuff as fast as possible.
-This renders a set of inputs (one for _most_ columns in the database table, and one for each ActiveRecord @belongs_to@-association), followed by a submit button:
+This renders a set of inputs (one for _most_ columns in the database table, and one for each ActiveRecord @belongs_to@-association), followed by default action buttons (an input submit button):
<pre>
<%= semantic_form_for @user do |f| %>
<%= f.inputs %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
This is a great way to get something up fast, but like scaffolding, it's *not recommended for production*. Don't be so lazy!
-To specify the order of the fields, skip some of the fields or even add in fields that Formtastic couldn't infer. You can pass in a list of field names to @inputs@ and list of button names to @buttons@:
+To specify the order of the fields, skip some of the fields or even add in fields that Formtastic couldn't infer. You can pass in a list of field names to @inputs@ and list of action names to @actions@:
<pre>
<%= semantic_form_for @user do |f| %>
<%= f.inputs :title, :body, :section, :categories, :created_at %>
- <%= f.buttons :commit %>
+ <%= f.actions :submit, :cancel %>
<% end %>
</pre>
-You probably want control over the input type Formtastic uses for each field. You can expand the @inputs@ and @buttons@ to block helper format and use the @:as@ option to specify an exact input type:
+You probably want control over the input type Formtastic uses for each field. You can expand the @inputs@ and @actions@ to block helper format and use the @:as@ option to specify an exact input type:
<pre>
<%= semantic_form_for @post do |f| %>
@@ -170,8 +171,9 @@ You probably want control over the input type Formtastic uses for each field. Yo
<%= f.input :categories %>
<%= f.input :created_at, :as => :string %>
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button %>
+ <%= f.actions do %>
+ <%= f.action :submit, :as => :button %>
+ <%= f.action :cancel, :as => :link %>
<% end %>
<% end %>
</pre>
@@ -191,8 +193,8 @@ If you want to customize the label text, or render some hint text below the fiel
<%= f.input :categories, :required => false %>
<%= f.input :created_at, :as => :string, :label => "Publication Date", :required => false %>
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button %>
+ <%= f.actions do %>
+ <%= f.action(:submit) %>
<% end %>
<% end %>
</pre>
@@ -211,7 +213,7 @@ Nested forms are also supported (don't forget your models need to be setup corre
<%= f.semantic_fields_for :author do |author| %>
<%= author.inputs :first_name, :last_name, :name => "Author" %>
<% end %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
@@ -221,7 +223,7 @@ Or the Formtastic way with the @:for@ option:
<%= semantic_form_for @post do |f| %>
<%= f.inputs :title, :body, :created_at %>
<%= f.inputs :first_name, :last_name, :for => :author, :name => "Author" %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
@@ -231,7 +233,7 @@ When working in has many association, you can even supply @"%i"@ in your fieldse
<%= semantic_form_for @post do |f| %>
<%= f.inputs %>
<%= f.inputs :name => 'Category #%i', :for => :categories %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
@@ -244,7 +246,7 @@ If you have more than one form on the same page, it may lead to HTML invalidatio
<%= f.input :body %> # id="cat_form_post_body"
<%= f.input :created_at %> # id="cat_form_post_created_at"
<% end %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
@@ -258,17 +260,17 @@ Customize HTML attributes for any input using the @:input_html@ option. Typicall
<%= f.input :created_at, :input_html => { :disabled => true } %>
<%= f.input :updated_at, :input_html => { :readonly => true } %>
<% end %>
- <%= f.buttons %>
+ <%= f.actions %>
<% end %>
</pre>
-The same can be done for buttons with the @:button_html@ option:
+The same can be done for actions with the @:button_html@ option:
<pre>
<%= semantic_form_for @post do |f| %>
...
- <%= f.buttons do %>
- <%= f.commit_button :button_html => { :class => "primary", :disable_with => 'Wait...' } %>
+ <%= f.actions do %>
+ <%= f.action :submit, :button_html => { :class => "primary", :disable_with => 'Wait...' } %>
<% end %>
<% end %>
</pre>
@@ -409,6 +411,8 @@ Formtastic supports localized *labels*, *hints*, *legends*, *actions* using the
actions:
create: "Create my %{model}"
update: "Save changes"
+ reset: "Reset form"
+ cancel: "Cancel and go back"
dummie: "Launch!"
</pre>
@@ -421,8 +425,8 @@ Formtastic supports localized *labels*, *hints*, *legends*, *actions* using the
<%= f.input :body %> # => :label => "Write something...", :hint => "Write something inspiring here."
<%= f.input :section %> # => :label => I18n.t('activerecord.attributes.user.section') or 'Section'
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button %> # => "Create my %{model}"
+ <%= f.actions do %>
+ <%= f.action(:submit) %> # => "Create my %{model}"
<% end %>
<% end %>
</pre>
@@ -449,8 +453,8 @@ _Note: Slightly different because Formtastic can't guess how you group fields in
<%= f.input :body, :hint => false %> # => :label => "Write something..."
<%= f.input :section, :label => 'Some section' %> # => :label => 'Some section'
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button :dummie %> # => "Launch!"
+ <%= f.actions do %>
+ <%= f.actions(:submit, :label => :dummie) %> # => "Launch!"
<% end %>
<% end %>
</pre>
@@ -470,8 +474,8 @@ If I18n-lookups is disabled, i.e.:
<%= f.input :body, :label => true %> # => :label => "Write something..."
<%= f.input :section, :label => true %> # => :label => I18n.t('activerecord.attributes.user.section') or 'Section'
<% end %>
- <%= f.buttons do %>
- <%= f.commit_button true %> # => "Update %{model}" (if we are in edit that is...)
+ <%= f.actions do %>
+ <%= f.action :submit, :label => true %> # => "Update %{model}" (if we are in edit that is...)
<% end %>
<% end %>
</pre>
View
19 app/assets/stylesheets/formtastic.css
@@ -18,6 +18,7 @@ This stylesheet forms part of the Formtastic Rails Plugin
.formtastic fieldset,
.formtastic legend,
.formtastic input,
+.formtastic button,
.formtastic textarea,
.formtastic select,
.formtastic p {
@@ -47,6 +48,7 @@ This stylesheet forms part of the Formtastic Rails Plugin
}
.formtastic input,
+.formtastic button,
.formtastic textarea {
font-family:sans-serif;
font-size:inherit;
@@ -80,18 +82,29 @@ This stylesheet forms part of the Formtastic Rails Plugin
}
-/* BUTTONS
+/* BUTTONS & ACTIONS
--------------------------------------------------------------------------------------------------*/
-.formtastic .buttons {
+.formtastic .buttons,
+.formtastic .actions {
overflow:hidden; /* clear containing floats */
padding-left:25%;
}
-.formtastic .button {
+.formtastic .button,
+.formtastic .action {
float:left;
padding-right:0.5em;
}
+.formtastic .button_action button {
+ padding:3px 8px;
+}
+
+.formtastic .link_action a {
+ display:block;
+ padding:3px 0;
+}
+
/* INPUTS
--------------------------------------------------------------------------------------------------*/
View
9 lib/formtastic.rb
@@ -9,6 +9,7 @@ module Formtastic
autoload :HtmlAttributes
autoload :I18n
autoload :Inputs
+ autoload :Actions
autoload :LocalizedString
autoload :Localizer
autoload :Util
@@ -18,7 +19,15 @@ class UnknownInputError < NameError
end
# @private
+ class UnknownActionError < NameError
+ end
+
+ # @private
class PolymorphicInputWithoutCollectionError < ArgumentError
end
+ # @private
+ class UnsupportedMethodForAction < ArgumentError
+ end
+
end
View
11 lib/formtastic/actions.rb
@@ -0,0 +1,11 @@
+module Formtastic
+ module Actions
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+ autoload :Buttonish
+ autoload :InputAction
+ autoload :LinkAction
+ autoload :ButtonAction
+ end
+end
View
156 lib/formtastic/actions/base.rb
@@ -0,0 +1,156 @@
+module Formtastic
+ module Actions
+ module Base
+ include Formtastic::LocalizedString
+
+ attr_accessor :builder, :template, :object, :object_name, :method, :options
+
+ def initialize(builder, template, object, object_name, method, options)
+ @builder = builder
+ @template = template
+ @object = object
+ @object_name = object_name
+ @method = method
+ @options = options.dup
+
+ check_supported_methods!
+ end
+
+ def to_html
+ raise NotImplementedError
+ end
+
+ def wrapper(&block)
+ template.content_tag(:li,
+ template.capture(&block),
+ wrapper_html_options
+ )
+ end
+
+ def wrapper_html_options
+ wrapper_html_options_from_options.merge(default_wrapper_html_options)
+ end
+
+ def wrapper_html_options_from_options
+ options[:wrapper_html] || {}
+ end
+
+ def default_wrapper_html_options
+ {
+ :class => wrapper_class,
+ :id => wrapper_id
+ }
+ end
+
+ def wrapper_class
+ (default_wrapper_classes << wrapper_classes_from_options).join(" ")
+ end
+
+ def default_wrapper_classes
+ ["action", "#{options[:as]}_action"]
+ end
+
+ def wrapper_classes_from_options
+ classes = wrapper_html_options_from_options[:class] || []
+ classes = classes.split(" ") if classes.is_a? String
+ classes
+ end
+
+ def wrapper_html_options_from_options
+ options[:wrapper_html] || {}
+ end
+
+ def wrapper_id
+ wrapper_id_from_options || default_wrapper_id
+ end
+
+ def wrapper_id_from_options
+ wrapper_html_options_from_options[:id]
+ end
+
+ def default_wrapper_id
+ "#{object_name}_#{method}_action"
+ end
+
+ def supported_methods
+ raise NotImplementedError
+ end
+
+ def text
+ text = options[:label]
+ text = (localized_string(i18n_key, text, :action, :model => sanitized_object_name) ||
+ Formtastic::I18n.t(i18n_key, :model => sanitized_object_name)) unless text.is_a?(::String)
+ text
+ end
+
+ def button_html
+ default_button_html.merge(button_html_from_options || {}).merge(extra_button_html_options)
+ end
+
+ def button_html_from_options
+ options[:button_html]
+ end
+
+ def extra_button_html_options
+ {}
+ end
+
+ def default_button_html
+ { :accesskey => accesskey }
+ end
+
+ def accesskey
+ # TODO could be cleaner and separated, remember that nil is an allowed value for all of these
+ return options[:accesskey] if options.key?(:accesskey)
+ return options[:button_html][:accesskey] if options.key?(:button_html) && options[:button_html].key?(:accesskey)
+ # TODO might be different for cancel, etc?
+ return builder.default_commit_button_accesskey
+ end
+
+
+ protected
+
+ def check_supported_methods!
+ raise Formtastic::UnsupportedMethodForAction unless supported_methods.include?(method)
+ end
+
+ def i18n_key
+ return submit_i18n_key if method == :submit
+ method
+ end
+
+ def submit_i18n_key
+ if new_or_persisted_object?
+ key = @object.persisted? ? :update : :create
+ else
+ key = :submit
+ end
+ end
+
+ def new_or_persisted_object?
+ object && (object.respond_to?(:persisted?) || object.respond_to?(:new_record?))
+ end
+
+ def sanitized_object_name
+ if new_or_persisted_object?
+ # Deal with some complications with ActiveRecord::Base.human_name and two name models (eg UserPost)
+ # ActiveRecord::Base.human_name falls back to ActiveRecord::Base.name.humanize ("Userpost")
+ # if there's no i18n, which is pretty crappy. In this circumstance we want to detect this
+ # fall back (human_name == name.humanize) and do our own thing name.underscore.humanize ("User Post")
+ if object.class.model_name.respond_to?(:human)
+ sanitized_object_name = object.class.model_name.human
+ else
+ object_human_name = @object.class.human_name # default is UserPost => "Userpost", but i18n may do better ("User post")
+ crappy_human_name = @object.class.name.humanize # UserPost => "Userpost"
+ decent_human_name = @object.class.name.underscore.humanize # UserPost => "User post"
+ sanitized_object_name = (object_human_name == crappy_human_name) ? decent_human_name : object_human_name
+ end
+ else
+ sanitized_object_name = object_name.to_s.send(builder.label_str_method)
+ end
+ sanitized_object_name
+ end
+
+ end
+ end
+end
View
72 lib/formtastic/actions/button_action.rb
@@ -0,0 +1,72 @@
+# Outputs a `<button type="submit">` or `<button type="reset">` wrapped in the standard `<li>`
+# wrapper. This is an alternative choice for `:submit` and `:reset` actions, which render with
+# `<input type="submit">` and `<input type="reset">` by default.
+#
+# @example Full form context and output
+#
+# <%= semantic_form_for(@post) do |f| %>
+# <%= f.actions do %>
+# <%= f.action :reset, :as => :button %>
+# <%= f.action :submit, :as => :button %>
+# <% end %>
+# <% end %>
+#
+# <form...>
+# <fieldset class="actions">
+# <ol>
+# <li class="action button_action" id="post_submit_action">
+# <button type="reset" value="Reset">
+# </li>
+# <li class="action button_action" id="post_reset_action">
+# <button type="submit" value="Create Post">
+# </li>
+# </ol>
+# </fieldset>
+# </form>
+#
+# @example Specifying a label with a String
+# <%= f.action :submit, :as => :button, :label => "Go" %>
+#
+# @example Pass HTML attributes down to the `<button>`
+# <%= f.action :submit, :as => :button, :button_html => { :class => 'pretty', :accesskey => 'g', :disable_with => "Wait..." } %>
+#
+# @example Access key can also be set as a top-level option
+# <%= f.action :submit, :as => :button, :accesskey => 'g' %>
+#
+# @example Pass HTML attributes down to the `<li>` wrapper (classes are appended to the existing classes)
+# <%= f.action :submit, :as => :button, :wrapper_html => { :class => 'special', :id => 'whatever' } %>
+# <%= f.action :submit, :as => :button, :wrapper_html => { :class => ['extra', 'special'], :id => 'whatever' } %>
+#
+# @option *args :label [String, Symbol]
+# Override the label text with a String or a symbol for an i18n translation key
+#
+# @option *args :button_html [Hash]
+# Override or add to the HTML attributes to be passed down to the `<input>` tag
+#
+# @option *args :wrapper_html [Hash]
+# Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
+#
+# @todo document i18n keys
+# @todo document i18n translation with :label (?)
+module Formtastic
+ module Actions
+ class ButtonAction
+ include Base
+ include Buttonish
+
+ # TODO absolutely horrible hack to work-around Rails < 3.1 missing button_tag, need
+ # to figure out something more appropriate.
+ #
+ # TODO reset_action class?
+ def to_html
+ wrapper do
+ if template.respond_to?(:button_tag)
+ template.button_tag(text, button_html)
+ else
+ template.content_tag(:button, text, button_html)
+ end
+ end
+ end
+ end
+ end
+end
View
17 lib/formtastic/actions/buttonish.rb
@@ -0,0 +1,17 @@
+module Formtastic
+ module Actions
+ module Buttonish
+
+ def supported_methods
+ [:submit, :reset]
+ end
+
+ def extra_button_html_options
+ {
+ :type => method
+ }
+ end
+
+ end
+ end
+end
View
68 lib/formtastic/actions/input_action.rb
@@ -0,0 +1,68 @@
+# Outputs an `<input type="submit">` or `<input type="reset">` wrapped in the standard `<li>`
+# wrapper. This the default for `:submit` and `:reset` actions, but `:as => :button` is also
+# available as an alternative.
+#
+# @example The `:as` can be ommitted, these are functionally equivalent
+# <%= f.action :submit, :as => :input %>
+# <%= f.action :submit %>
+#
+# @example Full form context and output
+#
+# <%= semantic_form_for(@post) do |f| %>
+# <%= f.actions do %>
+# <%= f.action :reset, :as => :input %>
+# <%= f.action :submit, :as => :input %>
+# <% end %>
+# <% end %>
+#
+# <form...>
+# <fieldset class="actions">
+# <ol>
+# <li class="action input_action" id="post_submit_action">
+# <input type="reset" value="Reset">
+# </li>
+# <li class="action input_action" id="post_reset_action">
+# <input type="submit" value="Create Post">
+# </li>
+# </ol>
+# </fieldset>
+# </form>
+#
+# @example Specifying a label with a String
+# <%= f.action :submit, :as => :input, :label => "Go" %>
+#
+# @example Pass HTML attributes down to the `<input>`
+# <%= f.action :submit, :as => :input, :button_html => { :class => 'pretty', :accesskey => 'g', :disable_with => "Wait..." } %>
+#
+# @example Access key can also be set as a top-level option
+# <%= f.action :submit, :as => :input, :accesskey => 'g' %>
+#
+# @example Pass HTML attributes down to the `<li>` wrapper (classes are appended to the existing classes)
+# <%= f.action :submit, :as => :input, :wrapper_html => { :class => 'special', :id => 'whatever' } %>
+# <%= f.action :submit, :as => :input, :wrapper_html => { :class => ['extra', 'special'], :id => 'whatever' } %>
+#
+# @option *args :label [String, Symbol]
+# Override the label text with a String or a symbol for an i18n translation key
+#
+# @option *args :button_html [Hash]
+# Override or add to the HTML attributes to be passed down to the `<input>` tag
+#
+# @option *args :wrapper_html [Hash]
+# Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
+#
+# @todo document i18n keys
+# @todo document i18n translation with :label (?)
+module Formtastic
+ module Actions
+ class InputAction
+ include Base
+ include Buttonish
+
+ def to_html
+ wrapper do
+ builder.submit(text, button_html)
+ end
+ end
+ end
+ end
+end
View
87 lib/formtastic/actions/link_action.rb
@@ -0,0 +1,87 @@
+# Outputs a link wrapped in the standard `<li>` wrapper. This the default for `:cancel` actions.
+# The link's URL defaults to Rails' built-in `:back` macro (the HTTP referrer, or Javascript for the
+# browser's history), but can be altered with the `:url` option.
+#
+# @example The `:as` can be ommitted, these are functionally equivalent
+# <%= f.action :cancel, :as => :link %>
+# <%= f.action :cancel %>
+#
+# @example Full form context and output
+#
+# <%= semantic_form_for(@post) do |f| %>
+# <%= f.actions do %>
+# <%= f.action :submit, :as => :input %>
+# <%= f.action :cancel, :as => :link %>
+# <% end %>
+# <% end %>
+#
+# <form...>
+# <fieldset class="actions">
+# <ol>
+# <li class="action input_action" id="post_submit_action">
+# <input type="reset" value="Reset">
+# </li>
+# <li class="action link_action" id="post_reset_action">
+# <a href="javascript:history.back()">Cancel</a>
+# </li>
+# </ol>
+# </fieldset>
+# </form>
+#
+# @example Modifying the URL for the link
+# <%= f.action :cancel, :as => :link, :url => "http://example.com/path" %>
+# <%= f.action :cancel, :as => :link, :url => "/path" %>
+# <%= f.action :cancel, :as => :link, :url => posts_path %>
+# <%= f.action :cancel, :as => :link, :url => url_for(...) %>
+# <%= f.action :cancel, :as => :link, :url => { :controller => "posts", :action => "index" } %>
+#
+# @example Specifying a label with a String
+# <%= f.action :cancel, :as => :link, :label => "Stop" %>
+#
+# @example Pass HTML attributes down to the `<a>`
+# <%= f.action :cancel, :as => :link, :button_html => { :class => 'pretty', :accesskey => 'x' } %>
+#
+# @example Access key can also be set as a top-level option
+# <%= f.action :cancel, :as => :link, :accesskey => 'x' %>
+#
+# @example Pass HTML attributes down to the `<li>` wrapper (classes are appended to the existing classes)
+# <%= f.action :cancel, :as => :link, :wrapper_html => { :class => 'special', :id => 'whatever' } %>
+# <%= f.action :cancel, :as => :link, :wrapper_html => { :class => ['extra', 'special'], :id => 'whatever' } %>
+#
+# @option *args :label [String, Symbol]
+# Override the label text with a String or a symbol for an i18n translation key
+#
+# @option *args :button_html [Hash]
+# Override or add to the HTML attributes to be passed down to the `<a>` tag
+#
+# @option *args :wrapper_html [Hash]
+# Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
+#
+# @todo document i18n keys
+# @todo document i18n translation with :label (?)
+# @todo :prefix and :suffix options? (can also be done with CSS or subclassing for custom Actions)
+module Formtastic
+ module Actions
+ class LinkAction
+
+ include Base
+
+ def supported_methods
+ [:cancel]
+ end
+
+ # TODO reset_action class?
+ def to_html
+ wrapper do
+ template.link_to(text, url, button_html)
+ end
+ end
+
+ def url
+ return options[:url] if options.key?(:url)
+ :back
+ end
+
+ end
+ end
+end
View
2 lib/formtastic/form_builder.rb
@@ -41,6 +41,8 @@ def self.configure(name, value = nil)
include Formtastic::Helpers::InputHelper
include Formtastic::Helpers::InputsHelper
include Formtastic::Helpers::ButtonsHelper
+ include Formtastic::Helpers::ActionHelper
+ include Formtastic::Helpers::ActionsHelper
include Formtastic::Helpers::ErrorsHelper
# This is a wrapper around Rails' `ActionView::Helpers::FormBuilder#fields_for`, originally
View
2 lib/formtastic/helpers.rb
@@ -2,6 +2,8 @@ module Formtastic
# @private
module Helpers
autoload :ButtonsHelper, 'formtastic/helpers/buttons_helper'
+ autoload :ActionHelper, 'formtastic/helpers/action_helper'
+ autoload :ActionsHelper, 'formtastic/helpers/actions_helper'
autoload :ErrorsHelper, 'formtastic/helpers/errors_helper'
autoload :FieldsetWrapper, 'formtastic/helpers/fieldset_wrapper'
autoload :FileColumnDetection, 'formtastic/helpers/file_column_detection'
View
109 lib/formtastic/helpers/action_helper.rb
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+module Formtastic
+ module Helpers
+ module ActionHelper
+
+ # Renders an action for the form (such as a subit/reset button, or a cancel link).
+ #
+ # Each action is wrapped in an `<li class="action">` tag with other classes added based on the
+ # type of action being rendered, and is intended to be rendered inside a {#buttons}
+ # block which wraps the button in a `fieldset` and `ol`.
+ #
+ # The textual value of the label can be changed from the default through the `:label`
+ # argument or through i18n.
+ #
+ # @example Basic usage
+ # # form
+ # <%= semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions do %>
+ # <%= f.action(:submit) %>
+ # <%= f.action(:reset) %>
+ # <%= f.action(:cancel) %>
+ # <% end %>
+ # <% end %>
+ #
+ # # output
+ # <form ...>
+ # ...
+ # <fieldset class="buttons">
+ # <ol>
+ # <li class="action input_action">
+ # <input name="commit" type="submit" value="Create Post">
+ # </li>
+ # <li class="action input_action">
+ # <input name="commit" type="reset" value="Reset Post">
+ # </li>
+ # <li class="action link_action">
+ # <a href="/posts">Cancel Post</a>
+ # </li>
+ # </ol>
+ # </fieldset>
+ # </form>
+ #
+ # @example Set the value through the `:label` option
+ # <%= f.action :submit, :label => "Go" %>
+ #
+ # @example Pass HTML attributes down to the tag inside the wrapper
+ # <%= f.action :submit, :button_html => { :class => 'pretty', :accesskey => 'g', :disable_with => "Wait..." } %>
+ #
+ # @example Pass HTML attributes down to the `<li>` wrapper
+ # <%= f.action :submit, :wrapper_html => { :class => 'special', :id => 'whatever' } %>
+ #
+ # @option *args :label [String, Symbol]
+ # Override the label text with a String or a symbold for an i18n translation key
+ #
+ # @option *args :button_html [Hash]
+ # Override or add to the HTML attributes to be passed down to the `<input>` tag
+ #
+ # @option *args :wrapper_html [Hash]
+ # Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
+ #
+ # @todo document i18n keys
+ def action(method, options = {})
+ options = options.dup # Allow options to be shared without being tainted by Formtastic
+ options[:as] ||= default_action_type(method, options)
+
+ klass = action_class(options[:as])
+
+ klass.new(self, template, @object, @object_name, method, options).to_html
+ end
+
+ protected
+
+ def default_action_type(method, options = {}) #:nodoc:
+ case method
+ when :submit then :input
+ when :reset then :input
+ when :cancel then :link
+ end
+ end
+
+ def action_class(as)
+ @input_classes_cache ||= {}
+ @input_classes_cache[as] ||= begin
+ begin
+ begin
+ custom_action_class_name(as).constantize
+ rescue NameError
+ standard_action_class_name(as).constantize
+ end
+ rescue NameError
+ raise Formtastic::UnknownActionError
+ end
+ end
+ end
+
+ # :as => :button # => ButtonAction
+ def custom_action_class_name(as)
+ "#{as.to_s.camelize}Action"
+ end
+
+ # :as => :button # => Formtastic::Actions::ButtonAction
+ def standard_action_class_name(as)
+ "Formtastic::Actions::#{as.to_s.camelize}Action"
+ end
+
+ end
+ end
+end
View
168 lib/formtastic/helpers/actions_helper.rb
@@ -0,0 +1,168 @@
+module Formtastic
+ module Helpers
+ # ActionsHelper encapsulates the responsibilties of the {#actions} DSL for acting on
+ # (submitting, cancelling, resetting) forms.
+ #
+ # {#actions} is a block helper used to wrap the form's actions (buttons, links) in a
+ # `<fieldset>` and `<ol>`, with each item in the list containing the markup representing a
+ # single action.
+ #
+ # <%= semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions do %>
+ # <%= f.action(:submit)
+ # <%= f.action(:cancel)
+ # <% end %>
+ # <% end %>
+ #
+ # The HTML output will be something like:
+ #
+ # <form class="formtastic" method="post" action="...">
+ # ...
+ # <fieldset class="actions">
+ # <ol>
+ # <li class="action input_action">
+ # <input type="submit" name="commit" value="Create Post">
+ # </li>
+ # <li class="action input_action">
+ # <a href="/posts">Cancel Post</a>
+ # </li>
+ # </ol>
+ # </fieldset>
+ # </form>
+ #
+ # It's important to note that the `semantic_form_for` and {#actions} blocks wrap the
+ # standard Rails `form_for` helper and form builder, so you have full access to every standard
+ # Rails form helper, with any HTML markup and ERB syntax, allowing you to "break free" from
+ # Formtastic when it doesn't suit to create your own buttons, links and actions:
+ #
+ # <%= semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions do %>
+ # <li class="save">
+ # <%= f.submit "Save" %>
+ # <li>
+ # <li class="cancel-link">
+ # Or <%= link_to "Cancel", posts_url %>
+ # <li>
+ # <% end %>
+ # <% end %>
+ #
+ # There are many other syntax variations and arguments to customize your form. See the
+ # full documentation of {#actions} and {#action} for details.
+ module ActionsHelper
+
+ include Formtastic::Helpers::FieldsetWrapper
+
+ # Creates a fieldset and ol tag wrapping for use around a set of buttons. It can be
+ # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
+ # or with a list of named actions. These two examples are functionally equivalent:
+ #
+ # # With a block:
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <% f.actions do %>
+ # <%= f.action(:submit) %>
+ # <%= f.action(:cancel) %>
+ # <% end %>
+ # <% end %>
+ #
+ # # With a list of fields:
+ # <% semantic_form_for @post do |f| %>
+ # <%= f.actions :submit, :cancel %>
+ # <% end %>
+ #
+ # # Output:
+ # <form ...>
+ # <fieldset class="buttons">
+ # <ol>
+ # <li class="action input_action">
+ # <input type="submit" ...>
+ # </li>
+ # <li class="action link_action">
+ # <a href="...">...</a>
+ # </li>
+ # </ol>
+ # </fieldset>
+ # </form>
+ #
+ # All options except `:name` and `:title` are passed down to the fieldset as HTML
+ # attributes (`id`, `class`, `style`...). If provided, the `:name` or `:title` option is
+ # passed into a `<legend>` inside the `<fieldset>` to name the set of buttons.
+ #
+ # @example Quickly add button(s) to the form, accepting all default values, options and behaviors
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions %>
+ # <% end %>
+ #
+ # @example Specify which named buttons you want, accepting all default values, options and behaviors
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions :commit %>
+ # <% end %>
+ #
+ # @example Specify which named buttons you want, and name the fieldset
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions :commit, :name => "Actions" %>
+ # or
+ # <%= f.actions :commit, :label => "Actions" %>
+ # <% end %>
+ #
+ # @example Get full control over the action options
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions do %>
+ # <%= f.action :label => "Go", :button_html => { :class => "pretty" :disable_with => "Wait..." }, :wrapper_html => { ... }
+ # <% end %>
+ # <% end %>
+ #
+ # @example Make your own actions with standard Rails helpers or HTML
+ # <% semantic_form_for @post do |f| %>
+ # <%= f.actions do %>
+ # <li>
+ # ...
+ # </li>
+ # <% end %>
+ # <% end %>
+ #
+ # @example Add HTML attributes to the fieldset
+ # <% semantic_form_for @post do |f| %>
+ # ...
+ # <%= f.actions :commit, :style => "border:1px;" %>
+ # or
+ # <%= f.actions :style => "border:1px;" do %>
+ # ...
+ # <% end %>
+ # <% end %>
+ #
+ # @option *args :label [String, Symbol]
+ # Optionally specify text for the legend of the fieldset
+ #
+ # @option *args :name [String, Symbol]
+ # Optionally specify text for the legend of the fieldset (alias for `:label`)
+ #
+ # @todo document i18n keys
+ def actions(*args, &block)
+ html_options = args.extract_options!
+ html_options[:class] ||= "actions"
+
+ if block_given?
+ field_set_and_list_wrapping(html_options, &block)
+ else
+ args = default_actions if args.empty?
+ contents = args.map { |action_name| action(action_name) }
+ field_set_and_list_wrapping(html_options, contents)
+ end
+ end
+
+ protected
+
+ def default_actions
+ [:submit]
+ end
+
+ end
+ end
+end
View
8 lib/formtastic/helpers/buttons_helper.rb
@@ -53,6 +53,8 @@ module Helpers
#
# There are many other syntax variations and arguments to customize your form. See the
# full documentation of {#buttons} and {#commit_button} for details.
+ #
+ # @deprecated ButtonsHelper will be removed after 2.1
module ButtonsHelper
include Formtastic::Helpers::FieldsetWrapper
include Formtastic::LocalizedString
@@ -164,7 +166,10 @@ module ButtonsHelper
# Optionally specify text for the legend of the fieldset (alias for `:label`)
#
# @todo document i18n keys
+ # @deprecated f.buttons is deprecated in favor of f.actions and will be removed after 2.1
def buttons(*args, &block)
+ ::ActiveSupport::Deprecation.warn("f.buttons is deprecated in favour of f.actions and will be removed from Formtastic after 2.1. Please see ActionsHelper and InputAction or ButtonAction for more information")
+
html_options = args.extract_options!
html_options[:class] ||= "buttons"
@@ -238,7 +243,10 @@ def buttons(*args, &block)
#
# @todo document i18n keys
# @todo strange that `:accesskey` seems to be supported in the top level args as well as `:button_html`
+ # @deprecated f.commit_button is deprecated in favor of f.actions and will be removed after 2.1
def commit_button(*args)
+ ::ActiveSupport::Deprecation.warn("f.commit_button is deprecated in favour of f.action(:submit) and will be removed from Formtastic after 2.1. Please see ActionsHelper and InputAction or ButtonAction for more information")
+
options = args.extract_options!
text = options.delete(:label) || args.shift
View
4 lib/generators/templates/_form.html.erb
@@ -5,7 +5,7 @@
<%- end -%>
<%% end %>
- <%%= f.buttons do %>
- <%%= f.commit_button %>
+ <%%= f.actions do %>
+ <%%= f.action(:submit, :as => :input) %>
<%% end %>
<%% end %>
View
4 lib/generators/templates/_form.html.haml
@@ -4,5 +4,5 @@
= f.input :<%= attribute.name %>
<%- end -%>
- = f.buttons do
- = f.commit_button
+ = f.actions do
+ = f.action :submit, :as => :input
View
2 lib/locale/en.yml
@@ -5,4 +5,6 @@ en:
:create: 'Create %{model}'
:update: 'Update %{model}'
:submit: 'Submit %{model}'
+ :cancel: 'Cancel %{model}'
+ :reset: 'Reset %{model}'
:required: 'required'
View
22 sample/basic_inputs.html
@@ -177,6 +177,28 @@
</li>
</ol>
</fieldset>
+
+ <fieldset class="actions">
+ <ol>
+ <li class="action input_action">
+ <input id="gem_submit" type="submit" value="Create Thing">
+ </li>
+ </ol>
+ </fieldset>
+
+ <fieldset class="actions">
+ <ol>
+ <li class="action button_action">
+ <button id="gem_submit" type="submit">Create Thing</button>
+ </li>
+ <li class="action button_action">
+ <button id="gem_submit" type="reset">Reset</button>
+ </li>
+ <li class="action link_action">
+ <a href="#">Cancel</button>
+ </li>
+ </ol>
+ </fieldset>
</form>
</body>
</html>
View
63 spec/actions/button_action_spec.rb
@@ -0,0 +1,63 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'ButtonAction', 'when submitting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :button))
+ end)
+ end
+
+ it 'should render a submit type of button' do
+ output_buffer.should have_tag('li.action.button_action button[@type="submit"]')
+ end
+
+end
+
+describe 'ButtonAction', 'when resetting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:reset, :as => :button))
+ end)
+ end
+
+ it 'should render a reset type of button' do
+ output_buffer.should have_tag('li.action.button_action button[@type="reset"]', :text => "Reset Post")
+ end
+
+ it 'should not render a value attribute' do
+ output_buffer.should_not have_tag('li.action.button_action button[@value]')
+ end
+
+end
+
+describe 'InputAction', 'when cancelling' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ it 'should raise an error' do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :button))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+end
View
484 spec/actions/generic_action_spec.rb
@@ -0,0 +1,484 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'InputAction::Base' do
+
+ # Most basic Action class to test Base
+ class ::GenericAction
+ include ::Formtastic::Actions::Base
+
+ def supported_methods
+ [:submit, :reset, :cancel]
+ end
+
+ def to_html
+ wrapper do
+ builder.submit(text, button_html)
+ end
+ end
+ end
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ describe 'wrapping HTML' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic,
+ :wrapper_html => { :foo => 'bah' }
+ ))
+ end)
+ end
+
+ it 'should add the #foo id to the li' do
+ output_buffer.should have_tag('li#post_submit_action')
+ end
+
+ it 'should add the .action and .generic_action classes to the li' do
+ output_buffer.should have_tag('li.action.generic_action')
+ end
+
+ it 'should pass :wrapper_html HTML attributes to the wrapper' do
+ output_buffer.should have_tag('li.action.generic_action[@foo="bah"]')
+ end
+
+ context "when a custom :id is provided" do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic,
+ :wrapper_html => { :id => 'foo_bah_bing' }
+ ))
+ end)
+ end
+
+ it "should use the custom id" do
+ output_buffer.should have_tag('li#foo_bah_bing')
+ end
+
+ end
+
+ context "when a custom class is provided as a string" do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic,
+ :wrapper_html => { :class => 'foo_bah_bing' }
+ ))
+ end)
+ end
+
+ it "should add the custom class strng to the existing classes" do
+ output_buffer.should have_tag('li.action.generic_action.foo_bah_bing')
+ end
+
+ end
+
+ context "when a custom class is provided as an array" do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic,
+ :wrapper_html => { :class => ['foo_bah_bing', 'zing_boo'] }
+ ))
+ end)
+ end
+
+ it "should add the custom class strng to the existing classes" do
+ output_buffer.should have_tag('li.action.generic_action.foo_bah_bing.zing_boo')
+ end
+
+ end
+
+ end
+
+ describe 'button HTML' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic,
+ :button_html => { :foo => 'bah' }
+ ))
+ end)
+ end
+
+ it 'should pass :button_html HTML attributes to the button' do
+ output_buffer.should have_tag('li.action.generic_action input[@foo="bah"]')
+ end
+
+ it 'should respect a default_commit_button_accesskey configuration with nil' do
+ with_config :default_commit_button_accesskey, nil do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ end)
+ output_buffer.should_not have_tag('li.action input[@accesskey]')
+ end
+ end
+
+ it 'should respect a default_commit_button_accesskey configuration with a String' do
+ with_config :default_commit_button_accesskey, 's' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ end)
+ output_buffer.should have_tag('li.action input[@accesskey="s"]')
+ end
+ end
+
+ it 'should respect an accesskey through options over configration' do
+ with_config :default_commit_button_accesskey, 's' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic, :accesskey => 'o'))
+ end)
+ output_buffer.should_not have_tag('li.action input[@accesskey="s"]')
+ output_buffer.should have_tag('li.action input[@accesskey="o"]')
+ end
+ end
+
+ end
+
+ describe 'labelling' do
+
+ describe 'when used without object' do
+
+ describe 'when explicit label is provided' do
+ it 'should render an input with the explicitly specified label' do
+ concat(semantic_form_for(:post, :url => 'http://example.com') do |builder|
+ concat(builder.action(:submit, :as => :generic, :label => "Click!"))
+ concat(builder.action(:reset, :as => :generic, :label => "Reset!"))
+ concat(builder.action(:cancel, :as => :generic, :label => "Cancel!"))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Click!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel!"]')
+ end
+ end
+
+ describe 'when no explicit label is provided' do
+ describe 'when no I18n-localized label is provided' do
+ before do
+ ::I18n.backend.store_translations :en, :formtastic => {
+ :submit => 'Submit %{model}',
+ :reset => 'Reset %{model}',
+ :cancel => 'Cancel %{model}'
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with default I18n-localized label (fallback)' do
+ concat(semantic_form_for(:post, :url => 'http://example.com') do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Submit Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset Post"]')
+ end
+ end
+
+ describe 'when I18n-localized label is provided' do
+
+ before do
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :submit => 'Custom Submit',
+ :reset => 'Custom Reset',
+ :cancel => 'Custom Cancel'
+ }
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with localized label (I18n)' do
+ with_config :i18n_lookups_by_default, true do
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :post => {
+ :submit => 'Custom Submit %{model}',
+ :reset => 'Custom Reset %{model}',
+ :cancel => 'Custom Cancel %{model}'
+ }
+ }
+ }
+ concat(semantic_form_for(:post, :url => 'http://example.com') do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Submit Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel Post"]})
+ end
+ end
+
+ it 'should render an input with anoptional localized label (I18n) - if first is not set' do
+ with_config :i18n_lookups_by_default, true do
+ concat(semantic_form_for(:post, :url => 'http://example.com') do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Submit"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel"]})
+ end
+ end
+
+ end
+ end
+ end
+
+ describe 'when used on a new record' do
+ before do
+ @new_post.stub!(:new_record?).and_return(true)
+ end
+
+ describe 'when explicit label is provided' do
+ it 'should render an input with the explicitly specified label' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic, :label => "Click!"))
+ concat(builder.action(:reset, :as => :generic, :label => "Reset!"))
+ concat(builder.action(:cancel, :as => :generic, :label => "Cancel!"))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Click!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel!"]')
+ end
+ end
+
+ describe 'when no explicit label is provided' do
+ describe 'when no I18n-localized label is provided' do
+ before do
+ ::I18n.backend.store_translations :en, :formtastic => {
+ :create => 'Create %{model}',
+ :reset => 'Reset %{model}',
+ :cancel => 'Cancel %{model}'
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with default I18n-localized label (fallback)' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Create Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel Post"]')
+ end
+ end
+
+ describe 'when I18n-localized label is provided' do
+ before do
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :create => 'Custom Create',
+ :reset => 'Custom Reset',
+ :cancel => 'Custom Cancel'
+ }
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with localized label (I18n)' do
+ with_config :i18n_lookups_by_default, true do
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :post => {
+ :create => 'Custom Create %{model}',
+ :reset => 'Custom Reset %{model}',
+ :cancel => 'Custom Cancel %{model}'
+ }
+ }
+ }
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Create Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel Post"]})
+ end
+ end
+
+ it 'should render an input with anoptional localized label (I18n) - if first is not set' do
+ with_config :i18n_lookups_by_default, true do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Create"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel"]})
+ end
+ end
+
+ end
+ end
+ end
+
+ describe 'when used on an existing record' do
+ before do
+ @new_post.stub!(:persisted?).and_return(true)
+ end
+
+ describe 'when explicit label is provided' do
+ it 'should render an input with the explicitly specified label' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic, :label => "Click!"))
+ concat(builder.action(:reset, :as => :generic, :label => "Reset!"))
+ concat(builder.action(:cancel, :as => :generic, :label => "Cancel!"))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Click!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset!"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel!"]')
+ end
+ end
+
+ describe 'when no explicit label is provided' do
+ describe 'when no I18n-localized label is provided' do
+ before do
+ ::I18n.backend.store_translations :en, :formtastic => {
+ :update => 'Save %{model}',
+ :reset => 'Reset %{model}',
+ :cancel => 'Cancel %{model}'
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with default I18n-localized label (fallback)' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag('li.generic_action input[@value="Save Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Reset Post"]')
+ output_buffer.should have_tag('li.generic_action input[@value="Cancel Post"]')
+ end
+ end
+
+ describe 'when I18n-localized label is provided' do
+ before do
+ ::I18n.backend.reload!
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :update => 'Custom Save',
+ :reset => 'Custom Reset',
+ :cancel => 'Custom Cancel'
+ }
+ }
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ it 'should render an input with localized label (I18n)' do
+ with_config :i18n_lookups_by_default, true do
+ ::I18n.backend.store_translations :en,
+ :formtastic => {
+ :actions => {
+ :post => {
+ :update => 'Custom Save %{model}',
+ :reset => 'Custom Reset %{model}',
+ :cancel => 'Custom Cancel %{model}'
+ }
+ }
+ }
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Save Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset Post"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel Post"]})
+ end
+ end
+
+ it 'should render an input with anoptional localized label (I18n) - if first is not set' do
+ with_config :i18n_lookups_by_default, true do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Save"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Reset"]})
+ output_buffer.should have_tag(%Q{li.generic_action input[@value="Custom Cancel"]})
+ ::I18n.backend.store_translations :en, :formtastic => {}
+ end
+ end
+
+ end
+ end
+ end
+ end
+
+ describe 'when the model is two words' do
+
+ before do
+ output_buffer = ''
+ class ::UserPost
+ extend ActiveModel::Naming if defined?(ActiveModel::Naming)
+ include ActiveModel::Conversion if defined?(ActiveModel::Conversion)
+
+ def id
+ end
+
+ def persisted?
+ end
+
+ # Rails does crappy human_name
+ def self.human_name
+ "User post"
+ end
+ end
+ @new_user_post = ::UserPost.new
+
+ @new_user_post.stub!(:new_record?).and_return(true)
+ concat(semantic_form_for(@new_user_post, :url => '') do |builder|
+ concat(builder.action(:submit, :as => :generic))
+ concat(builder.action(:reset, :as => :generic))
+ concat(builder.action(:cancel, :as => :generic))
+ end)
+ end
+
+ it "should render the string as the value of the button" do
+ output_buffer.should have_tag('li input[@value="Create User post"]')
+ output_buffer.should have_tag('li input[@value="Reset User post"]')
+ output_buffer.should have_tag('li input[@value="Cancel User post"]')
+ end
+
+ end
+
+end
View
59 spec/actions/input_action_spec.rb
@@ -0,0 +1,59 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'InputAction', 'when submitting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :input))
+ end)
+ end
+
+ it 'should render a submit type of input' do
+ output_buffer.should have_tag('li.action.input_action input[@type="submit"]')
+ end
+
+end
+
+describe 'InputAction', 'when resetting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:reset, :as => :input))
+ end)
+ end
+
+ it 'should render a reset type of input' do
+ output_buffer.should have_tag('li.action.input_action input[@type="reset"]')
+ end
+
+end
+
+describe 'InputAction', 'when cancelling' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ it 'should raise an error' do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :input))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+end
View
92 spec/actions/link_action_spec.rb
@@ -0,0 +1,92 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'LinkAction', 'when cancelling' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ context 'without a :url' do
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :link))
+ end)
+ end
+
+ it 'should render a submit type of input' do
+ output_buffer.should have_tag('li.action.link_action a[@href="javascript:history.back()"]')
+ end
+
+ end
+
+ context 'with a :url as String' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :link, :url => "http://foo.bah/baz"))
+ end)
+ end
+
+ it 'should render a submit type of input' do
+ output_buffer.should have_tag('li.action.link_action a[@href="http://foo.bah/baz"]')
+ end
+
+ end
+
+ context 'with a :url as Hash' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :link, :url => { :action => "foo" }))
+ end)
+ end
+
+ it 'should render a submit type of input' do
+ output_buffer.should have_tag('li.action.link_action a[@href="/mock/path"]')
+ end
+
+ end
+
+end
+
+describe 'LinkAction', 'when submitting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ it 'should raise an error' do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :link))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+end
+
+describe 'LinkAction', 'when submitting' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ it 'should raise an error' do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:reset, :as => :link))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+end
View
365 spec/helpers/action_helper_spec.rb
@@ -0,0 +1,365 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'Formtastic::FormBuilder#action' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ after do
+ ::I18n.backend.reload!
+ end
+
+ describe 'arguments and options' do
+
+ it 'should require the first argument (the action method)' do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action()) # no args passed in at all
+ end)
+ }.should raise_error(ArgumentError)
+ end
+
+ describe ':as option' do
+
+ describe 'when not provided' do
+
+ it 'should default to a commit for commit' do
+ concat(semantic_form_for(:project, :url => 'http://test.host') do |builder|
+ concat(builder.action(:submit))
+ end)
+ output_buffer.should have_tag('form li.action.input_action', :count => 1)
+ end
+
+ it 'should default to a button for reset' do
+ concat(semantic_form_for(:project, :url => 'http://test.host') do |builder|
+ concat(builder.action(:reset))
+ end)
+ output_buffer.should have_tag('form li.action.input_action', :count => 1)
+ end
+
+ it 'should default to a link for cancel' do
+ concat(semantic_form_for(:project, :url => 'http://test.host') do |builder|
+ concat(builder.action(:cancel))
+ end)
+ output_buffer.should have_tag('form li.action.link_action', :count => 1)
+ end
+ end
+
+ it 'should call the corresponding action class with .to_html' do
+ [:input, :button, :link].each do |action_style|
+ semantic_form_for(:project, :url => "http://test.host") do |builder|
+ action_instance = mock('Action instance')
+ action_class = "#{action_style.to_s}_action".classify
+ action_constant = "Formtastic::Actions::#{action_class}".constantize
+
+ action_constant.should_receive(:new).and_return(action_instance)
+ action_instance.should_receive(:to_html).and_return("some HTML")
+
+ concat(builder.action(:submit, :as => action_style))
+ end
+ end
+ end
+
+ end
+
+ #describe ':label option' do
+ #
+ # describe 'when provided' do
+ # it 'should be passed down to the label tag' do
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:title, :label => "Kustom"))
+ # end)
+ # output_buffer.should have_tag("form li label", /Kustom/)
+ # end
+ #
+ # it 'should not generate a label if false' do
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:title, :label => false))
+ # end)
+ # output_buffer.should_not have_tag("form li label")
+ # end
+ #
+ # it 'should be dupped if frozen' do
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:title, :label => "Kustom".freeze))
+ # end)
+ # output_buffer.should have_tag("form li label", /Kustom/)
+ # end
+ # end
+ #
+ # describe 'when not provided' do
+ # describe 'when localized label is provided' do
+ # describe 'and object is given' do
+ # describe 'and label_str_method not :humanize' do
+ # it 'should render a label with localized text and not apply the label_str_method' do
+ # with_config :label_str_method, :reverse do
+ # @localized_label_text = 'Localized title'
+ # @new_post.stub!(:meta_description)
+ # ::I18n.backend.store_translations :en,
+ # :formtastic => {
+ # :labels => {
+ # :meta_description => @localized_label_text
+ # }
+ # }
+ #
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:meta_description))
+ # end)
+ # output_buffer.should have_tag('form li label', /Localized title/)
+ # end
+ # end
+ # end
+ # end
+ # end
+ #
+ # describe 'when localized label is NOT provided' do
+ # describe 'and object is not given' do
+ # it 'should default the humanized method name, passing it down to the label tag' do
+ # ::I18n.backend.store_translations :en, :formtastic => {}
+ # with_config :label_str_method, :humanize do
+ # concat(semantic_form_for(:project, :url => 'http://test.host') do |builder|
+ # concat(builder.input(:meta_description))
+ # end)
+ # output_buffer.should have_tag("form li label", /#{'meta_description'.humanize}/)
+ # end
+ # end
+ # end
+ #
+ # describe 'and object is given' do
+ # it 'should delegate the label logic to class human attribute name and pass it down to the label tag' do
+ # @new_post.stub!(:meta_description) # a two word method name
+ # @new_post.class.should_receive(:human_attribute_name).with('meta_description').and_return('meta_description'.humanize)
+ #
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:meta_description))
+ # end)
+ # output_buffer.should have_tag("form li label", /#{'meta_description'.humanize}/)
+ # end
+ # end
+ #
+ # describe 'and object is given with label_str_method set to :capitalize' do
+ # it 'should capitalize method name, passing it down to the label tag' do
+ # with_config :label_str_method, :capitalize do
+ # @new_post.stub!(:meta_description)
+ #
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:meta_description))
+ # end)
+ # output_buffer.should have_tag("form li label", /#{'meta_description'.capitalize}/)
+ # end
+ # end
+ # end
+ # end
+ #
+ # describe 'when localized label is provided' do
+ # before do
+ # @localized_label_text = 'Localized title'
+ # @default_localized_label_text = 'Default localized title'
+ # ::I18n.backend.store_translations :en,
+ # :formtastic => {
+ # :labels => {
+ # :title => @default_localized_label_text,
+ # :published => @default_localized_label_text,
+ # :post => {
+ # :title => @localized_label_text,
+ # :published => @default_localized_label_text
+ # }
+ # }
+ # }
+ # end
+ #
+ # it 'should render a label with localized label (I18n)' do
+ # with_config :i18n_lookups_by_default, false do
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:title, :label => true))
+ # concat(builder.input(:published, :as => :boolean, :label => true))
+ # end)
+ # output_buffer.should have_tag('form li label', Regexp.new('^' + @localized_label_text))
+ # end
+ # end
+ #
+ # it 'should render a hint paragraph containing an optional localized label (I18n) if first is not set' do
+ # with_config :i18n_lookups_by_default, false do
+ # ::I18n.backend.store_translations :en,
+ # :formtastic => {
+ # :labels => {
+ # :post => {
+ # :title => nil,
+ # :published => nil
+ # }
+ # }
+ # }
+ # concat(semantic_form_for(@new_post) do |builder|
+ # concat(builder.input(:title, :label => true))
+ # concat(builder.input(:published, :as => :boolean, :label => true))
+ # end)
+ # output_buffer.should have_tag('form li label', Regexp.new('^' + @default_localized_label_text))
+ # end
+ # end
+ # end
+ # end
+ #
+ #end
+ #
+ describe ':wrapper_html option' do
+
+ describe 'when provided' do
+ it 'should be passed down to the li tag' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :wrapper_html => {:id => :another_id}))
+ end)
+ output_buffer.should have_tag("form li#another_id")
+ end
+
+ it 'should append given classes to li default classes' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :wrapper_html => {:class => :another_class}))
+ end)
+ output_buffer.should have_tag("form li.action")
+ output_buffer.should have_tag("form li.input_action")
+ output_buffer.should have_tag("form li.another_class")
+ end
+
+ it 'should allow classes to be an array' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :wrapper_html => {:class => [ :my_class, :another_class ]}))
+ end)
+ output_buffer.should have_tag("form li.action")
+ output_buffer.should have_tag("form li.input_action")
+ output_buffer.should have_tag("form li.my_class")
+ output_buffer.should have_tag("form li.another_class")
+ end
+ end
+
+ describe 'when not provided' do
+ it 'should use default id and class' do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit))
+ end)
+ output_buffer.should have_tag("form li#post_submit_action")
+ output_buffer.should have_tag("form li.action")
+ output_buffer.should have_tag("form li.input_action")
+ end
+ end
+
+ end
+
+ end
+
+ describe 'instantiating an action class' do
+
+ context 'when a class does not exist' do
+ it "should raise an error" do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.action(:submit, :as => :non_existant)
+ end)
+ }.should raise_error(Formtastic::UnknownActionError)
+ end
+ end
+
+ context 'when a customized top-level class does not exist' do
+
+ it 'should instantiate the Formtastic action' do
+ action = mock('action', :to_html => 'some HTML')
+ Formtastic::Actions::ButtonAction.should_receive(:new).and_return(action)
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.action(:commit, :as => :button)
+ end)
+ end
+
+ end
+
+ describe 'when a top-level (custom) action class exists' do
+ it "should instantiate the top-level action instead of the Formtastic one" do
+ class ::ButtonAction < Formtastic::Actions::ButtonAction
+ end
+
+ action = mock('action', :to_html => 'some HTML')
+ Formtastic::Actions::ButtonAction.should_not_receive(:new).and_return(action)
+ ::ButtonAction.should_receive(:new).and_return(action)
+
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.action(:commit, :as => :button)
+ end)
+ end
+ end
+
+ describe 'when instantiated multiple times with the same action type' do
+
+ it "should be cached (not calling the internal methods)" do
+ # TODO this is really tied to the underlying implementation
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.should_receive(:custom_action_class_name).with(:button).once.and_return(::Formtastic::Actions::ButtonAction)
+ builder.action(:submit, :as => :button)
+ builder.action(:submit, :as => :button)
+ end)
+ end
+
+ end
+
+ describe 'support for :as on each action' do
+
+ it "should raise an error when the action does not support the :as" do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :link))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :input))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :button))
+ end)
+ }.should raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+ it "should not raise an error when the action does not support the :as" do
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:cancel, :as => :link))
+ end)
+ }.should_not raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :input))
+ end)
+ }.should_not raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:submit, :as => :button))
+ end)
+ }.should_not raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:reset, :as => :input))
+ end)
+ }.should_not raise_error(Formtastic::UnsupportedMethodForAction)
+
+ lambda {
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.action(:reset, :as => :button))
+ end)
+ }.should_not raise_error(Formtastic::UnsupportedMethodForAction)
+ end
+
+ end
+
+ end
+
+end
+
View
143 spec/helpers/actions_helper_spec.rb
@@ -0,0 +1,143 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe 'Formtastic::FormBuilder#actions' do
+
+ include FormtasticSpecHelper
+
+ before do
+ @output_buffer = ''
+ mock_everything
+ end
+
+ describe 'with a block' do
+ describe 'when no options are provided' do
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.actions do
+ concat('hello')
+ end)
+ end)
+ end
+
+ it 'should render a fieldset inside the form, with a class of "actions"' do
+ output_buffer.should have_tag("form fieldset.actions")
+ end
+
+ it 'should render an ol inside the fieldset' do
+ output_buffer.should have_tag("form fieldset.actions ol")
+ end
+
+ it 'should render the contents of the block inside the ol' do
+ output_buffer.should have_tag("form fieldset.actions ol", /hello/)
+ end
+
+ it 'should not render a legend inside the fieldset' do
+ output_buffer.should_not have_tag("form fieldset.actions legend")
+ end
+ end
+
+ describe 'when a :name option is provided' do
+ before do
+ @legend_text = "Advanced options"
+
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.actions :name => @legend_text do
+ end
+ end)
+ end
+ it 'should render a fieldset inside the form' do
+ output_buffer.should have_tag("form fieldset.actions legend", /#{@legend_text}/)
+ end
+ end
+
+ describe 'when other options are provided' do
+ before do
+ @id_option = 'advanced'
+ @class_option = 'wide'
+
+ concat(semantic_form_for(@new_post) do |builder|
+ builder.actions :id => @id_option, :class => @class_option do
+ end
+ end)
+ end
+ it 'should pass the options into the fieldset tag as attributes' do
+ output_buffer.should have_tag("form fieldset##{@id_option}")
+ output_buffer.should have_tag("form fieldset.#{@class_option}")
+ end
+ end
+
+ end
+
+ describe 'without a block' do
+
+ describe 'with no args (default buttons)' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.actions)
+ end)
+ end
+
+ it 'should render a form' do
+ output_buffer.should have_tag('form')
+ end
+
+ it 'should render an actions fieldset inside the form' do
+ output_buffer.should have_tag('form fieldset.actions')
+ end
+
+ it 'should not render a legend in the fieldset' do
+ output_buffer.should_not have_tag('form fieldset.actions legend')
+ end
+
+ it 'should render an ol in the fieldset' do
+ output_buffer.should have_tag('form fieldset.actions ol')
+ end
+
+ it 'should render a list item in the ol for each default action' do
+ output_buffer.should have_tag('form fieldset.actions ol li.action.input_action', :count => 1)
+ end
+
+ end
+
+ describe 'with button names as args' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.actions(:submit, :cancel, :reset))
+ end)
+ end
+
+ it 'should render a form with a fieldset containing a list item for each button arg' do
+ output_buffer.should have_tag('form > fieldset.actions > ol > li.action', :count => 3)
+ end
+
+ end
+
+ describe 'with button names as args and an options hash' do
+
+ before do
+ concat(semantic_form_for(@new_post) do |builder|
+ concat(builder.actions(:submit, :cancel, :reset, :name => "Now click a button", :id => "my-id"))
+ end)
+ end
+
+ it 'should render a form with a fieldset containing a list item for each button arg' do
+ output_buffer.should have_tag('form > fieldset.actions > ol > li.action', :count => 3)
+ end
+
+ it 'should pass the options down to the fieldset' do
+ output_buffer.should have_tag('form > fieldset#my-id.actions')
+ end
+
+ it 'should use the special :name option as a text for the legend tag' do
+ output_buffer.should have_tag('form > fieldset#my-id.actions > legend', /Now click a button/)
+ end
+
+ end
+
+ end
+
+end
+
View