diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 98abc0287ca73..0ce31713eaf45 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,25 @@
+* Accept render options and block in `render` calls made with `:renderable`
+
+ ```ruby
+ class Greeting
+ def render_in(view_context, **options, &block)
+ if block
+ view_context.render html: block.call
+ else
+ view_context.render inline: <<~ERB.strip, **options
+ Hello, <%= local_assigns.fetch(:name, "World") %>!
+ ERB
+ end
+ end
+
+ ApplicationController.render(Greeting.new) # => "Hello, World!"
+ ApplicationController.render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
+ ApplicationController.render(renderable: Greeting.new) # => "Hello, World!"
+ ApplicationController.render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local!"
+ ```
+
+ *Sean Doyle*
+
* Add `allow_browser` to set minimum browser versions for the application.
A browser that's blocked will by default be served the file in `public/426.html` with a HTTP status code of "426 Upgrade Required".
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 18dbb53ce0bbb..9cc8d37def17d 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -42,11 +42,11 @@ def render(*args, &block)
# needs to be overridden in order to still return a string.
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
- render_to_body(options)
+ render_to_body(options, &block)
end
# Performs the actual template rendering.
- def render_to_body(options = {})
+ def render_to_body(options = {}, &block)
end
# Returns +Content-Type+ of rendered content.
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 1d5639d92b59e..4b267b086a8f5 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -37,11 +37,18 @@ def inherited(klass)
# # => renders app/views/posts/show.html.erb
#
# If the first argument responds to +render_in+, the template will be
- # rendered by calling +render_in+ with the current view context.
+ # rendered by calling +render_in+ with the current view context, render
+ # options, and block.
#
# class Greeting
- # def render_in(view_context)
- # view_context.render html: "
Hello, World
"
+ # def render_in(view_context, **options, &block)
+ # if block
+ # view_context.render html: block.call
+ # else
+ # view_context.render inline: <<~ERB.strip, **options
+ # <%= Hello, <%= local_assigns.fetch(:name, "World") %>
+ # ERB
+ # end
# end
#
# def format
@@ -49,11 +56,12 @@ def inherited(klass)
# end
# end
#
- # render(Greeting.new)
- # # => "Hello, World
"
- #
- # render(renderable: Greeting.new)
- # # => "Hello, World
"
+ # render(Greeting.new) # => "Hello, World
"
+ # render(renderable: Greeting.new) # => "Hello, World
"
+ # render(Greeting.new, name: "Local") # => "Hello, Local
"
+ # render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local
"
+ # render(Greeting.new) { "Hello, Block
" } # => "Hello, Block
"
+ # render(renderable: Greeting.new) { "Hello, Block" } # => "Hello, Block
"
#
# ==== \Rendering Mode
#
@@ -110,12 +118,16 @@ def inherited(klass)
#
# [+:renderable+]
# Renders the provided object by calling +render_in+ with the current view
- # context. The response format is determined by calling +format+ on the
- # renderable if it responds to +format+, falling back to +text/html+ by default.
+ # context, render options, and block. The response format is determined by
+ # calling +format+ on the renderable if it responds to +format+, falling
+ # back to +text/html+ by default.
#
# render renderable: Greeting.new
# # => renders "Hello, World
"
#
+ # render renderable: Greeting.new, locals: { name: "Local" }
+ # # => renders "Hello, Local
"
+ #
# By default, when a rendering mode is specified, no layout template is
# rendered.
#
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index c15ac5ad7f2c5..1c61a71461a8f 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -120,14 +120,14 @@ def defaults
end
# Renders a template to a string, just like ActionController::Rendering#render_to_string.
- def render(*args)
+ def render(...)
request = ActionDispatch::Request.new(env_for_request)
request.routes = controller._routes
instance = controller.new
instance.set_request! request
instance.set_response! controller.make_response!(request)
- instance.render_to_string(*args)
+ instance.render_to_string(...)
end
alias_method :render_to_string, :render # :nodoc:
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index a210d4a2b6bb4..4d340ca8854ea 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -2,6 +2,7 @@
require "abstract_unit"
require "controller/fake_models"
+require "test_renderable"
class TestControllerWithExtraEtags < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
@@ -363,6 +364,10 @@ class MetalTestController < ActionController::Metal
def accessing_logger_in_template
render inline: "<%= logger.class %>"
end
+
+ def render_renderable
+ render renderable: TestRenderable.new, locals: params.fetch(:locals, {})
+ end
end
class ExpiresInRenderTest < ActionController::TestCase
@@ -790,6 +795,16 @@ def test_access_to_logger_in_view
get :accessing_logger_in_template
assert_equal "NilClass", @response.body
end
+
+ def test_render_renderable
+ get :render_renderable
+
+ assert_equal "Hello, World!", @response.parsed_body.text
+
+ get :render_renderable, params: { locals: { name: "Local" } }
+
+ assert_equal "Hello, Local!", @response.parsed_body.text
+ end
end
class ActionControllerRenderTest < ActionController::TestCase
diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb
index 490e111745e0d..01fe5a89fb0a3 100644
--- a/actionpack/test/controller/renderer_test.rb
+++ b/actionpack/test/controller/renderer_test.rb
@@ -77,6 +77,27 @@ class RendererTest < ActiveSupport::TestCase
%(Hello, World!),
renderer.render(renderable: TestRenderable.new)
)
+ assert_equal(
+ %(Hello, Local!),
+ renderer.render(TestRenderable.new, name: "Local")
+ )
+ assert_equal(
+ %(Hello, Local!),
+ renderer.render(renderable: TestRenderable.new, locals: { name: "Local" })
+ )
+ end
+
+ test "render a renderable object with block" do
+ renderer = ApplicationController.renderer
+
+ assert_equal(
+ %(Goodbye, World!
),
+ renderer.render(TestRenderable.new) { "Goodbye, World!
".html_safe }
+ )
+ assert_equal(
+ %(Goodbye, World!
),
+ renderer.render(renderable: TestRenderable.new) { "Goodbye, World!
".html_safe }
+ )
end
test "rendering with custom env" do
diff --git a/actionpack/test/lib/test_renderable.rb b/actionpack/test/lib/test_renderable.rb
index 46e2f8983c5c2..8cf0732e73d44 100644
--- a/actionpack/test/lib/test_renderable.rb
+++ b/actionpack/test/lib/test_renderable.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
class TestRenderable
- def render_in(_view_context)
- "Hello, World!"
- end
-
- def format
- :html
+ def render_in(view_context, locals: {}, **options, &block)
+ if block
+ view_context.render html: block.call
+ else
+ view_context.render inline: <<~ERB.strip, locals: locals
+ Hello, <%= local_assigns.fetch(:name, "World") %>!
+ ERB
+ end
end
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 668bbc48021ad..9fd90c4e66ae2 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,26 @@
+* Pass render options and block to calls to `#render_in`
+
+ ```ruby
+ class Greeting
+ def render_in(view_context, **options, &block)
+ if block
+ view_context.render html: block.call
+ else
+ view_context.render inline: <<~ERB.strip, **options
+ Hello, <%= local_assigns.fetch(:name, "World") %>!
+ ERB
+ end
+ end
+ end
+
+ render(Greeting.new) # => "Hello, World!"
+ render(Greeting.new, name: "Local") # => "Hello, Local!"
+ render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local!"
+ render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
+ ```
+
+ *Sean Doyle*
+
* Add the `nonce: true` option for `stylesheet_link_tag` helper to support automatic nonce generation for Content Security Policy.
Works the same way as `javascript_include_tag nonce: true` does.
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 5be8eca1de30a..c89d3f3df9e92 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -24,22 +24,23 @@ module RenderingHelper
# If no options hash is passed or if :update is specified, then:
#
# If an object responding to +render_in+ is passed, +render_in+ is called on the object,
- # passing in the current view context.
+ # passing in the current view context, render options, and block. The
+ # object can optionally control its rendered format by defining the +format+ method.
#
# Otherwise, a partial is rendered using the second parameter as the locals hash.
def render(options = {}, locals = {}, &block)
case options
when Hash
in_rendering_context(options) do |renderer|
- if block_given?
+ if block_given? && !options.key?(:renderable)
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
- view_renderer.render(self, options)
+ view_renderer.render(self, options, &block)
end
end
else
if options.respond_to?(:render_in)
- options.render_in(self, &block)
+ view_renderer.render(self, renderable: options, locals: locals, &block)
else
view_renderer.render_partial(self, partial: options, locals: locals, &block)
end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 844cf49b01135..e5702654e1b7a 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -79,7 +79,7 @@ def partial_path(object, view)
path = if object.respond_to?(:to_partial_path)
object.to_partial_path
else
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
end
if view.prefix_partial_path_with_controller_namespace
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index df5138bb0cf53..808a2077d4ed1 100644
--- a/actionview/lib/action_view/renderer/renderer.rb
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -20,15 +20,15 @@ def initialize(lookup_context)
end
# Main render entry point shared by Action View and Action Controller.
- def render(context, options)
- render_to_object(context, options).body
+ def render(context, options, &block)
+ render_to_object(context, options, &block).body
end
- def render_to_object(context, options) # :nodoc:
+ def render_to_object(context, options, &block) # :nodoc:
if options.key?(:partial)
render_partial_to_object(context, options)
else
- render_template_to_object(context, options)
+ render_template_to_object(context, options, &block)
end
end
@@ -54,8 +54,8 @@ def cache_hits # :nodoc:
end
private
- def render_template_to_object(context, options)
- TemplateRenderer.new(@lookup_context).render(context, options)
+ def render_template_to_object(context, options, &block)
+ TemplateRenderer.new(@lookup_context).render(context, options, &block)
end
def render_partial_to_object(context, options, &block)
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index 6f2ae66f5fae1..b6118a227ac37 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -2,9 +2,9 @@
module ActionView
class TemplateRenderer < AbstractRenderer # :nodoc:
- def render(context, options)
+ def render(context, options, &block)
@details = extract_details(options)
- template = determine_template(options)
+ template = determine_template(options, &block)
prepend_formats(template.format)
@@ -13,7 +13,7 @@ def render(context, options)
private
# Determine the template to be rendered using the given options.
- def determine_template(options)
+ def determine_template(options, &block)
keys = options.has_key?(:locals) ? options[:locals].keys : []
if options.key?(:body)
@@ -41,7 +41,13 @@ def determine_template(options)
end
Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
elsif options.key?(:renderable)
- Template::Renderable.new(options[:renderable])
+ renderable = options[:renderable]
+
+ unless renderable.respond_to?(:render_in)
+ raise ArgumentError, "'#{renderable.inspect}' is not a renderable object. It must implement #render_in."
+ end
+
+ Template::Renderable.new(renderable, &block)
elsif options.key?(:template)
if options[:template].respond_to?(:render)
options[:template]
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index ea5c0cbb43325..d329d6f793e15 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -116,14 +116,14 @@ def view_renderer # :nodoc:
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
- def render_to_body(options = {})
+ def render_to_body(options = {}, &block)
_process_options(options)
- _render_template(options)
+ _render_template(options, &block)
end
private
# Find and render a template based on the options given.
- def _render_template(options)
+ def _render_template(options, &block)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
@@ -132,7 +132,7 @@ def _render_template(options)
lookup_context.variants = variant if variant
rendered_template = context.in_rendering_context(options) do |renderer|
- renderer.render_to_object(context, options)
+ renderer.render_to_object(context, options, &block)
end
rendered_format = rendered_template.format || lookup_context.formats.first
@@ -164,6 +164,7 @@ def _normalize_args(action = nil, options = {})
options = action
elsif action.respond_to?(:render_in)
options[:renderable] = action
+ options[:locals] = options
else
options[:partial] = action
end
diff --git a/actionview/lib/action_view/template/renderable.rb b/actionview/lib/action_view/template/renderable.rb
index c37bd1cb5e2d5..6f5f86bd73e1e 100644
--- a/actionview/lib/action_view/template/renderable.rb
+++ b/actionview/lib/action_view/template/renderable.rb
@@ -4,16 +4,30 @@ module ActionView
class Template
# = Action View Renderable Template for objects that respond to #render_in
class Renderable # :nodoc:
- def initialize(renderable)
+ def initialize(renderable, &block)
@renderable = renderable
+ @block = block
end
def identifier
@renderable.class.name
end
- def render(context, *args)
- @renderable.render_in(context)
+ def render(context, locals)
+ options =
+ if @renderable.method(:render_in).arity == 1
+ ActionView.deprecator.warn <<~WARN
+ Action View's support for #render_in without options is deprecated.
+
+ Change #render_in to accept keyword arguments.
+ WARN
+
+ {}
+ else
+ { locals: locals }
+ end
+
+ @renderable.render_in(context, **options, &@block)
end
def format
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 8c2a9df0b4adc..bf3469095a729 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -592,6 +592,10 @@ def partial_hash_collection_with_locals
render partial: "hash_greeting", collection: [ { first_name: "Pratik" }, { first_name: "Amy" } ], locals: { greeting: "Hola" }
end
+ def renderable_hash
+ render renderable: Quiz::Question.new(params[:name])
+ end
+
def partial_with_implicit_local_assignment
@customer = Customer.new("Marcel")
render partial: "customer"
@@ -706,6 +710,7 @@ class RenderTest < ActionController::TestCase
get :partial_with_nested_object_shorthand, to: "test#partial_with_nested_object_shorthand"
get :partial_with_hashlike_locals, to: "test#partial_with_hashlike_locals"
get :partials_list, to: "test#partials_list"
+ get :renderable_hash, to: "test#renderable_hash"
get :render_action_hello_world, to: "test#render_action_hello_world"
get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string"
get :render_action_hello_world_with_symbol, to: "test#render_action_hello_world_with_symbol"
@@ -1483,6 +1488,11 @@ def test_partial_hash_collection_with_locals
assert_equal "Hola: PratikHola: Amy", @response.body
end
+ def test_renderable_hash
+ get :renderable_hash, params: { name: "Why?" }
+ assert_equal "Why?", @response.body
+ end
+
def test_render_missing_partial_template
assert_raise(ActionView::MissingTemplate) do
get :missing_partial
diff --git a/actionview/test/lib/test_renderable.rb b/actionview/test/lib/test_renderable.rb
index c2b7411e14c30..228aa523f5856 100644
--- a/actionview/test/lib/test_renderable.rb
+++ b/actionview/test/lib/test_renderable.rb
@@ -1,7 +1,13 @@
# frozen_string_literal: true
class TestRenderable
- def render_in(_view_context)
- "Hello, World!"
+ def render_in(view_context, **options, &block)
+ if block
+ view_context.render html: block.call
+ else
+ view_context.render inline: <<~ERB.strip, **options
+ Hello, <%= local_assigns.fetch(:name, "World") %>!
+ ERB
+ end
end
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 4462e8d086811..6751402344120 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -294,7 +294,21 @@ def test_render_partial_with_missing_filename
def test_render_partial_with_incompatible_object
e = assert_raises(ArgumentError) { @view.render(partial: nil) }
- assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message
+ assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.", e.message
+ end
+
+ def test_render_renderable_with_nil
+ assert_raises ArgumentError, match: "'#{nil.inspect}' is not a renderable object. It must implement #render_in." do
+ @view.render renderable: nil
+ end
+ end
+
+ def test_render_renderable_with_incompatible_object
+ object = Object.new
+
+ assert_raises ArgumentError, match: "'#{object.inspect}' is not a renderable object. It must implement #render_in." do
+ @view.render renderable: object
+ end
end
def test_render_partial_starting_with_a_capital
@@ -375,9 +389,45 @@ def test_render_renderable_object
assert_equal "NilClass", @view.render(partial: "test/klass", object: nil)
end
+ def test_render_renderable_object_without_block_without_options_deprecated
+ renderable = Object.new
+ def renderable.render_in(view_context)
+ end
+
+ assert_deprecated "without options", ActionView.deprecator do
+ @view.render renderable
+ end
+ end
+
+ def test_render_renderable_object_with_block_without_options_deprecated
+ renderable = Object.new
+ def renderable.render_in(view_context, &block)
+ end
+
+ assert_deprecated "without options", ActionView.deprecator do
+ @view.render renderable
+ end
+ end
+
def test_render_renderable_render_in
- assert_equal "Hello, World!", @view.render(TestRenderable.new)
- assert_equal "Hello, World!", @view.render(renderable: TestRenderable.new)
+ assert_equal "Hello, World!
", @view.render(TestRenderable.new)
+ assert_equal "Hello, World!
", @view.render(renderable: TestRenderable.new)
+
+ assert_equal "Hello, Renderable!
", @view.render(TestRenderable.new, name: "Renderable")
+ assert_equal "Hello, Renderable!
", @view.render(renderable: TestRenderable.new, locals: { name: "Renderable" })
+
+ assert_equal "Goodbye, Block!
", @view.render(TestRenderable.new) { @view.tag.h1 "Goodbye, Block!" }
+ assert_equal "Goodbye, Block!
", @view.render(renderable: TestRenderable.new) { @view.tag.h1 "Goodbye, Block!" }
+ end
+
+ def test_render_renderable_render_in_excludes_renderable_key
+ renderable = Object.new
+ def renderable.render_in(view_context, **options)
+ view_context.render plain: options, **options
+ end
+ options = { locals: { a: true, b: false } }
+
+ assert_equal options.to_s, @view.render(renderable: renderable, **options)
end
def test_render_object_different_name
@@ -763,7 +813,7 @@ def test_render_throws_exception_when_no_extensions_passed_to_register_template_
def test_render_object
assert_equal(
- %(Hello, World!),
+ %(Hello, World!
),
@view.render(TestRenderable.new)
)
end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 253ec94bf9c91..2f58b1dbfdde9 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,28 @@
+* Add default `#render_in` implementation to `ActiveModel::Conversion`
+
+ With the following view partial:
+
+ ```erb
+ <%# app/views/people/_person.html.erb %>
+ <% local_assigns.with_defaults(shout: false) => { shout: } %>
+
+ <%= shout ? person.name.upcase : person.name %>
+ ```
+
+ Callers can render an instance of `Person` as a positional argument or a
+ `:renderable` option:
+
+ ```ruby
+ person = Person.new(name: "Ralph")
+
+ render person # => "Ralph"
+ render person, shout: true # => "RALPH"
+ render renderable: person # => "Ralph"
+ render renderable: person, locals: { shout: true } # => "RALPH"
+ ```
+
+ *Sean Doyle*
+
* Port the `type_for_attribute` method to Active Model. Classes that include
`ActiveModel::Attributes` will now provide this method. This method behaves
the same for Active Model as it does for Active Record.
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index c6f4b2fc898ad..6be329671e935 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -104,6 +104,30 @@ def to_partial_path
self.class._to_partial_path
end
+ # Renders the object into an Action View context.
+ #
+ # # app/models/person.rb
+ # class Person
+ # include ActiveModel::Conversion
+ #
+ # attr_reader :name
+ #
+ # def initialize(name)
+ # @name = name
+ # end
+ # end
+ #
+ # # app/views/people/_person.html.erb
+ #
<%= person.name %>
+ #
+ # person = Person.new name: "Ralph"
+ #
+ # render(person) # => "Ralph
+ # render(renderable: person) # => "Ralph
+ def render_in(view_context, **options, &block)
+ view_context.render(partial: to_partial_path, object: self, **options, &block)
+ end
+
module ClassMethods # :nodoc:
# Provide a class level cache for #to_partial_path. This is an
# internal method and should not be accessed directly.
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 09527cdd859fb..dd45a65380a25 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -60,6 +60,20 @@ def test_to_partial_path
assert_kind_of String, model.to_partial_path
end
+ # Passes if the object's model responds to render_in
+ # calling this method returns a string. Fails otherwise.
+ #
+ # render_in is used for rendering an instance. For example,
+ # a BlogPost model might render itself into a "blog_posts/blog_post" partial.
+ def test_render_in
+ view_context = Object.new
+ def view_context.render(...)
+ ""
+ end
+ assert_respond_to model, :render_in
+ assert_kind_of String, model.render_in(view_context)
+ end
+
# Passes if the object's model responds to persisted? and if
# calling this method returns either +true+ or +false+. Fails otherwise.
#
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
index eb134595a039e..d7c9eee01adbf 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -62,6 +62,21 @@ def persisted?
assert_equal "attack_helicopters/ah-64", Helicopter::Apache.new.to_partial_path
end
+ test "#render_in defaults to rendering a partial with the object" do
+ contact = Contact.new
+ view_context = Object.new
+ def view_context.render(**options, &block)
+ [options, block.call]
+ end
+
+ options, rendered = contact.render_in(view_context, locals: { size: "small" }) { "block" }
+
+ assert_equal contact.to_partial_path, options[:partial]
+ assert_equal contact, options[:object]
+ assert_equal "small", options.dig(:locals, :size)
+ assert_equal "block", rendered
+ end
+
test "#to_param_delimiter allows redefining the delimiter used in #to_param" do
old_delimiter = Contact.param_delimiter
Contact.param_delimiter = "_"
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index e25454bade1a9..a215d81059459 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -163,6 +163,27 @@ irb> person.to_key
=> nil
irb> person.to_param
=> nil
+irb> person.to_partial_path
+=> "people/person"
+```
+
+The `ActiveModel::Conversion` module also defines a `render_in` method for
+integration with Action View. For example, consider a `people/person` partial:
+
+```erb
+<%# app/views/people/_person.html.erb %>
+Persisted: <%= person.persisted? %>
+```
+
+By default, passing the object to `render` will invoke `to_partial_path` to
+determine the view partial to render:
+
+```ruby
+render person
+# => Persisted: false
+
+render renderable: person
+# => Persisted: false
```
### Dirty
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index f14a7d6d1de41..16059a956e649 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -283,28 +283,44 @@ TIP: `send_file` is often a faster and better option if a layout isn't required.
#### Rendering Objects
-Rails can render objects responding to `#render_in`. The format can be controlled by defining `#format` on the object.
+Rails can render objects that respond to `#render_in`. You can provide the object as the first positional argument or provide it as the `:renderable` option to `render`:
```ruby
class Greeting
- def render_in(view_context)
- view_context.render html: "Hello, World"
- end
-
- def format
- :html
+ def render_in(view_context, **options, &block)
+ if block
+ view_context.render html: block.call
+ else
+ view_context.render inline: <<~ERB.strip, **options
+ Hello <%= local_assigns.fetch(:name, "World") %>!
+ ERB
+ end
end
end
render Greeting.new
-# => "Hello World"
+# => "Hello World!
"
+
+render renderable: Greeting.new
+# => "Hello World!
"
+
+render Greeting.new, name: "Rails"
+# => "Hello Rails!
"
+
+render renderable: Greeting.new, locals: { name: "Rails" }
+# => "Hello Rails!
"
```
-This calls `render_in` on the provided object with the current view context. You can also provide the object by using the `:renderable` option to `render`:
+If the format is known ahead of rendering, control it by defining `#format` on the object:
```ruby
-render renderable: Greeting.new
-# => "Hello World"
+class Greeting
+ # …
+
+ def format
+ :html
+ end
+end
```
#### Options for `render`