1
0
Fork 0
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:
Sean Doyle 2020-08-27 20:28:43 -04:00
parent 89414f561a
commit 59ca21c011
6 changed files with 183 additions and 14 deletions

View file

@ -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 <%= tag.attributes(type: :text, aria: { label: "Search" }) %> >
# => <input type="text" aria-label="Search"> # => <input type="text" aria-label="Search">

View file

@ -1691,6 +1691,47 @@ module ActionView
@index = options[:index] || options[:child_index] @index = options[:index] || options[:child_index]
end 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 # :method: text_field
# #

View file

@ -79,6 +79,42 @@ module ActionView
end end
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 # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
# choice selection box. # choice selection box.
# #

View file

@ -117,19 +117,7 @@ module ActionView
end end
def tag_id(index = nil) def tag_id(index = nil)
# a little duplication to construct fewer strings @template_object.field_id(@object_name, @method_name, index: index)
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("_")
end end
def sanitized_method_name def sanitized_method_name

View file

@ -1597,6 +1597,60 @@ class FormHelperTest < ActionView::TestCase
ActionView::Helpers::FormHelper.form_with_generates_ids = old_value ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
end 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 def test_form_for_with_collection_radio_buttons
post = Post.new post = Post.new
def post.active; false; end def post.active; false; end

View file

@ -187,6 +187,30 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer assert_dom_equal expected, output_buffer
end 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 def test_hidden_field_tag
actual = hidden_field_tag "id", 3 actual = hidden_field_tag "id", 3
expected = %(<input id="id" name="id" type="hidden" value="3" />) expected = %(<input id="id" name="id" type="hidden" value="3" />)