Execute `field_error_proc` within view

Instead of treating it as an anonymous block, execute the
`ActionView::Base.field_error_proc` within the context of the
`ActionView::Base` instance.

This enables consumer applications to continue to override the proc as
they see fit, but frees them from declaring templating logic within a
`config/initializers/*.rb`, `config/environments/*.rb` or
`config/application.rb` file.

This makes it possible to replace something like:

```ruby
config.action_view.field_error_proc = proc do |html_tag, instance|
  <<~HTML.html_safe
    #{html_tag}
    <span class="errors">#{instance.error_message.to_sentence}</span>
  HTML
end
```

With inline calls to Action View helpers like:

```ruby
config.action_view.field_error_proc = proc do |html_tag, instance|
  safe_join [ html_tag, tag.span(instance.error_message.to_sentence, class: "errors") ]
end
```

Or with a view partial rendering, like:

```ruby
config.action_view.field_error_proc = proc do |html_tag, instance|
  render partial: "application/field_with_errors", locals: { html_tag: html_tag, instance: instance }
end
```

Then, elsewhere in `app/views/application/field_with_errors.html.erb`:

```erb
<%= html_tag %>
<span class="errors"><%= instance.error_message.to_sentence %></span>
```
This commit is contained in:
Sean Doyle 2021-07-11 12:30:45 -04:00
parent 9f980664fc
commit 9c86593caa
5 changed files with 72 additions and 2 deletions

View File

@ -1,3 +1,12 @@
* Execute the `ActionView::Base.field_error_proc` within the context of the
`ActionView::Base` instance:
```ruby
config.action_view.field_error_proc = proc { |html| content_tag(:div, html, class: "field_with_errors") }
```
*Sean Doyle*
* Add `:day_format` option to `date_select`
date_select("article", "written_on", day_format: ->(day) { day.ordinalize })

View File

@ -142,7 +142,7 @@ module ActionView # :nodoc:
include Helpers, ::ERB::Util, Context
# Specify the proc used to decorate input tags that refer to attributes with errors.
cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| content_tag :div, html_tag, class: "field_with_errors" }
# How to complete the streaming when an exception occurs.
# This is our best guess: first try to close the attribute, then the tag.

View File

@ -27,7 +27,7 @@ module ActionView
def error_wrapping(html_tag)
if object_has_errors?
Base.field_error_proc.call(html_tag, self)
@template_object.instance_exec(html_tag, self, &Base.field_error_proc)
else
html_tag
end

View File

@ -33,6 +33,12 @@ module Fun
end
end
class ValidatingPost < Post
include ActiveModel::Validations
validates :title, presence: true
end
class TestController < ActionController::Base
protect_from_forgery
@ -487,6 +493,14 @@ class TestController < ActionController::Base
render partial: ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
end
def partial_with_form_builder_and_invalid_model
post = ValidatingPost.new
post.validate
render partial: ActionView::Helpers::FormBuilder.new(:post, post, view_context, {})
end
def partial_with_form_builder_subclass
render partial: LabellingFormBuilder.new(:post, nil, view_context, {})
end
@ -680,6 +694,7 @@ class RenderTest < ActionController::TestCase
get :partial_only, to: "test#partial_only"
get :partial_with_counter, to: "test#partial_with_counter"
get :partial_with_form_builder, to: "test#partial_with_form_builder"
get :partial_with_form_builder_and_invalid_model, to: "test#partial_with_form_builder_and_invalid_model"
get :partial_with_form_builder_subclass, to: "test#partial_with_form_builder_subclass"
get :partial_with_hash_object, to: "test#partial_with_hash_object"
get :partial_with_locals, to: "test#partial_with_locals"
@ -1300,6 +1315,44 @@ class RenderTest < ActionController::TestCase
assert_equal "<label for=\"post_title\">Title</label>\n", @response.body
end
def test_partial_with_form_builder_and_invalid_model
get :partial_with_form_builder_and_invalid_model
assert_equal <<~HTML.strip, @response.body.strip
<div class="field_with_errors"><label for="post_title">Title</label></div>
HTML
end
def test_partial_with_form_builder_and_invalid_model_custom_field_error_proc
old_proc = ActionView::Base.field_error_proc
ActionView::Base.field_error_proc = proc { |html| tag.div html, class: "errors" }
get :partial_with_form_builder_and_invalid_model
assert_equal <<~HTML.strip, @response.body.strip
<div class="errors"><label for="post_title">Title</label></div>
HTML
ensure
ActionView::Base.field_error_proc = old_proc if old_proc
end
def test_partial_with_form_builder_and_invalid_model_custom_rendering_field_error_proc
old_proc = ActionView::Base.field_error_proc
ActionView::Base.field_error_proc = proc do |html_tag, instance|
render inline: <<~ERB, locals: { html_tag: html_tag, instance: instance }
<div class="field_with_errors"><%= html_tag %> <span class="error"><%= [instance.error_message].join(', ') %></span></div>
ERB
end
get :partial_with_form_builder_and_invalid_model
assert_equal <<~HTML.strip, @response.body.strip
<div class="field_with_errors"><label for="post_title">Title</label> <span class="error">can&#39;t be blank</span></div>
HTML
ensure
ActionView::Base.field_error_proc = old_proc if old_proc
end
def test_partial_with_form_builder_subclass
get :partial_with_form_builder_subclass
assert_equal "<label for=\"post_title\">Title</label>\n", @response.body

View File

@ -1083,6 +1083,14 @@ Controls whether or not templates should be reloaded on each request. Defaults t
#### `config.action_view.field_error_proc`
* `config.action_view.field_error_proc` provides an HTML generator for
displaying errors that come from Active Model. The block is evaluated within
the context of an Action View template. The default is
```ruby
Proc.new { |html_tag, instance| content_tag :div, html_tag, class: "field_with_errors" }
```
Provides an HTML generator for displaying errors that come from Active Model. The default is
```ruby