mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Declare ActionView::Helpers::FormBuilder#id
`ActionView::Helpers::FormBuilder#id` --- Generate an HTML `id` attribute value. Return the [`<form>` element's][mdn-form] `id` attribute. ```html+erb <%= form_for @post do |f| %> <%# ... %> <% content_for :sticky_footer do %> <%= form.button(form: f.id) %> <% end %> <% end %> ``` In the example above, the `:sticky_footer` content area will exist outside of the `<form>` element. [By declaring the `form` HTML attribute][mdn-button-attr-form], we hint to the browser that the generated `<button>` element should be treated as the `<form>` element's submit button, regardless of where it exists in the DOM. [A similar pattern could be used for `<input>` elements][mdn-input-attr-form] (or other form controls) that do not descend from the `<form>` element. `ActionView::Helpers::FormBuilder#field_id` --- Generate an HTML <tt>id</tt> attribute value for the given field Return the value generated by the <tt>FormBuilder</tt> for the given attribute name. ```html+erb <%= form_for @post do |f| %> <%= f.label :title %> <%= f.text_field :title, aria: { describedby: form.field_id(:title, :error) } %> <span id="<%= f.field_id(:title, :error) %>">is blank</span> <% end %> ``` In the example above, the <tt><input type="text"></tt> element built by the call to <tt>FormBuilder#text_field</tt> declares an <tt>aria-describedby</tt> attribute referencing the <tt><span></tt> element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this case). This method is powered by the `field_id` helper declared in `action_view/helpers/form_tag_helper`, which is made available for general template calls, separate from a `FormBuilder` instance. [mdn-form]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form [mdn-button-attr-form]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-form [mdn-input-attr-form]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-form [mdn-aria-describedby]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute [w3c-wai]: https://www.w3.org/WAI/tutorials/forms/notifications/#listing-errors
This commit is contained in:
parent
89414f561a
commit
59ca21c011
6 changed files with 183 additions and 14 deletions
|
@ -1,4 +1,30 @@
|
|||
* Transforms a Hash into HTML Attributes, ready to be interpolated into ERB.
|
||||
* `ActionView::Helpers::FormBuilder#id` returns the value
|
||||
of the `<form>` element's `id` attribute. With a `method` argument, returns
|
||||
the `id` attribute for a form field with that name.
|
||||
|
||||
<%= form_for @post do |f| %>
|
||||
<%# ... %>
|
||||
|
||||
<% content_for :sticky_footer do %>
|
||||
<%= form.button(form: f.id) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
*Sean Doyle*
|
||||
|
||||
* `ActionView::Helpers::FormBuilder#field_id` returns the value generated by
|
||||
the FormBuilder for the given attribute name.
|
||||
|
||||
<%= form_for @post do |f| %>
|
||||
<%= f.label :title %>
|
||||
<%= f.text_field :title, aria: { describedby: f.field_id(:title, :error) } %>
|
||||
<%= tag.span("is blank", id: f.field_id(:title, :error) %>
|
||||
<% end %>
|
||||
|
||||
*Sean Doyle*
|
||||
|
||||
* Add `tag.attributes` to transform a Hash into HTML Attributes, ready to be
|
||||
interpolated into ERB.
|
||||
|
||||
<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %> >
|
||||
# => <input type="text" aria-label="Search">
|
||||
|
|
|
@ -1691,6 +1691,47 @@ module ActionView
|
|||
@index = options[:index] || options[:child_index]
|
||||
end
|
||||
|
||||
# Generate an HTML <tt>id</tt> attribute value.
|
||||
#
|
||||
# return the <tt><form></tt> element's <tt>id</tt> attribute.
|
||||
#
|
||||
# <%= form_for @post do |f| %>
|
||||
# <%# ... %>
|
||||
#
|
||||
# <% content_for :sticky_footer do %>
|
||||
# <%= form.button(form: f.id) %>
|
||||
# <% end %>
|
||||
# <% end %>
|
||||
#
|
||||
# In the example above, the <tt>:sticky_footer</tt> content area will
|
||||
# exist outside of the <tt><form></tt> element. By declaring the
|
||||
# <tt>form</tt> HTML attribute, we hint to the browser that the generated
|
||||
# <tt><button></tt> element should be treated as the <tt><form></tt>
|
||||
# element's submit button, regardless of where it exists in the DOM.
|
||||
def id
|
||||
options.dig(:html, :id)
|
||||
end
|
||||
|
||||
# Generate an HTML <tt>id</tt> attribute value for the given field
|
||||
#
|
||||
# Return the value generated by the <tt>FormBuilder</tt> for the given
|
||||
# attribute name.
|
||||
#
|
||||
# <%= form_for @post do |f| %>
|
||||
# <%= f.label :title %>
|
||||
# <%= f.text_field :title, aria: { describedby: f.field_id(:title, :error) } %>
|
||||
# <%= tag.span("is blank", id: f.field_id(:title, :error) %>
|
||||
# <% end %>
|
||||
#
|
||||
# In the example above, the <tt><input type="text"></tt> element built by
|
||||
# the call to <tt>FormBuilder#text_field</tt> declares an
|
||||
# <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
|
||||
# element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
|
||||
# case).
|
||||
def field_id(method, *suffixes, index: @index)
|
||||
@template.field_id(@object || @object_name, method, *suffixes, index: @index)
|
||||
end
|
||||
|
||||
##
|
||||
# :method: text_field
|
||||
#
|
||||
|
|
|
@ -79,6 +79,42 @@ module ActionView
|
|||
end
|
||||
end
|
||||
|
||||
# Generate an HTML <tt>id</tt> attribute value for the given name and
|
||||
# field combination
|
||||
#
|
||||
# Return the value generated by the <tt>FormBuilder</tt> for the given
|
||||
# attribute name.
|
||||
#
|
||||
# <%= label_tag :post, :title %>
|
||||
# <%= text_field_tag :post, :title, aria: { describedby: field_id(:post, :title, :error) } %>
|
||||
# <%= tag.span("is blank", id: field_id(:post, :title, :error) %>
|
||||
#
|
||||
# In the example above, the <tt><input type="text"></tt> element built by
|
||||
# the call to <tt>text_field_tag</tt> declares an
|
||||
# <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
|
||||
# element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
|
||||
# case).
|
||||
def field_id(object_name, method_name, *suffixes, index: nil)
|
||||
if object_name.respond_to?(:model_name)
|
||||
object_name = object_name.model_name.singular
|
||||
end
|
||||
|
||||
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
|
||||
|
||||
sanitized_method_name = method_name.to_s.delete_suffix("?")
|
||||
|
||||
# a little duplication to construct fewer strings
|
||||
if sanitized_object_name.empty?
|
||||
sanitized_method_name.dup
|
||||
elsif suffixes.any?
|
||||
[sanitized_object_name, index, sanitized_method_name, *suffixes].compact.join("_")
|
||||
elsif index
|
||||
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
|
||||
else
|
||||
"#{sanitized_object_name}_#{sanitized_method_name}"
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
|
||||
# choice selection box.
|
||||
#
|
||||
|
|
|
@ -117,19 +117,7 @@ module ActionView
|
|||
end
|
||||
|
||||
def tag_id(index = nil)
|
||||
# a little duplication to construct fewer strings
|
||||
case
|
||||
when @object_name.empty?
|
||||
sanitized_method_name.dup
|
||||
when index
|
||||
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
|
||||
else
|
||||
"#{sanitized_object_name}_#{sanitized_method_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def sanitized_object_name
|
||||
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
|
||||
@template_object.field_id(@object_name, @method_name, index: index)
|
||||
end
|
||||
|
||||
def sanitized_method_name
|
||||
|
|
|
@ -1597,6 +1597,60 @@ class FormHelperTest < ActionView::TestCase
|
|||
ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
|
||||
end
|
||||
|
||||
def test_form_for_id
|
||||
form_for(Post.new) do |form|
|
||||
concat form.button(form: form.id)
|
||||
end
|
||||
|
||||
expected = whole_form("/posts", "new_post", "new_post") do
|
||||
'<button name="button" type="submit" form="new_post">Create Post</button>'
|
||||
end
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_field_id_with_model
|
||||
value = field_id(Post.new, :title)
|
||||
|
||||
assert_equal "post_title", value
|
||||
end
|
||||
|
||||
def test_field_id_with_predicate_method
|
||||
value = field_id(Post.new, :secret?)
|
||||
|
||||
assert_equal "post_secret", value
|
||||
end
|
||||
|
||||
def test_form_for_field_id
|
||||
form_for(Post.new) do |form|
|
||||
concat form.label(:title)
|
||||
concat form.text_field(:title, aria: { describedby: form.field_id(:title, :error) })
|
||||
concat tag.span("is blank", id: form.field_id(:title, :error))
|
||||
end
|
||||
|
||||
expected = whole_form("/posts", "new_post", "new_post") do
|
||||
'<label for="post_title">Title</label>' \
|
||||
'<input id="post_title" name="post[title]" type="text" aria-describedby="post_title_error">' \
|
||||
'<span id="post_title_error">is blank</span>'
|
||||
end
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_for_field_id_with_index
|
||||
form_for(Post.new, index: 1) do |form|
|
||||
concat form.text_field(:title, aria: { describedby: form.field_id(:title, :error) })
|
||||
concat tag.span("is blank", id: form.field_id(:title, :error))
|
||||
end
|
||||
|
||||
expected = whole_form("/posts", "new_post", "new_post") do
|
||||
'<input id="post_1_title" name="post[1][title]" type="text" aria-describedby="post_1_title_error">' \
|
||||
'<span id="post_1_title_error">is blank</span>'
|
||||
end
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_for_with_collection_radio_buttons
|
||||
post = Post.new
|
||||
def post.active; false; end
|
||||
|
|
|
@ -187,6 +187,30 @@ class FormTagHelperTest < ActionView::TestCase
|
|||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_field_id_without_suffixes_or_index
|
||||
value = field_id(:post, :title)
|
||||
|
||||
assert_equal "post_title", value
|
||||
end
|
||||
|
||||
def test_field_id_with_suffixes
|
||||
value = field_id(:post, :title, :error)
|
||||
|
||||
assert_equal "post_title_error", value
|
||||
end
|
||||
|
||||
def test_field_id_with_suffixes_and_index
|
||||
value = field_id(:post, :title, :error, index: 1)
|
||||
|
||||
assert_equal "post_1_title_error", value
|
||||
end
|
||||
|
||||
def test_field_id_with_nested_object_name
|
||||
value = field_id("post[author]", :name)
|
||||
|
||||
assert_equal "post_author_name", value
|
||||
end
|
||||
|
||||
def test_hidden_field_tag
|
||||
actual = hidden_field_tag "id", 3
|
||||
expected = %(<input id="id" name="id" type="hidden" value="3" />)
|
||||
|
|
Loading…
Reference in a new issue