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`