Skip to content

Commit dbed3b1

Browse files
committed
Pass render options and block to calls to #render_in
Closes [rails#45432][] Support for objects that respond to `#render_in` was introduced in [rails#36388][] and [rails#37919][]. Those implementations assume that the instance will all the context it needs to render itself. That assumption doesn't account for call-site arguments like `locals: { ... }` or a block. This commit expands support for rendering with a `:renderable` option to incorporate locals and blocks. For example: ```ruby class Greeting def render_in(view_context, **options, &block) if block view_context.render plain: block.call else case Array(options[:formats]).first when :json view_context.render json: { greeting: "Hello, World!" } else view_context.render **options, inline: "<%= Hello, <%= name %>!" end end end def format :html end end render(Greeting.new) # => "Hello, World!" render(Greeting.new, name: "Local") # => "Hello, Local!" render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!" render(renderable: Greeting.new, formats: :json) # => "{\"greeting\":\"Hello, World!\"}" ``` Since existing tools depend on the `#render_in(view_context)` signature without options, this commit deprecates that signature in favor of one that accepts options and a block. [rails#45432]: rails#45432 [rails#36388]: rails#36388 [rails#37919]: rails#37919
1 parent e1f8909 commit dbed3b1

File tree

18 files changed

+261
-93
lines changed

18 files changed

+261
-93
lines changed

actionpack/CHANGELOG.md

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,24 @@
1-
* Make `http_cache_forever` use `immutable: true`
2-
3-
*Nate Matykiewicz*
4-
5-
* Add `config.action_dispatch.strict_freshness`.
6-
7-
When set to `true`, the `ETag` header takes precedence over the `Last-Modified` header when both are present,
8-
as specified by RFC 7232, Section 6.
9-
10-
Defaults to `false` to maintain compatibility with previous versions of Rails, but is enabled as part of
11-
Rails 8.0 defaults.
12-
13-
*heka1024*
14-
15-
* Support `immutable` directive in Cache-Control
16-
17-
```ruby
18-
expires_in 1.minute, public: true, immutable: true
19-
# Cache-Control: public, max-age=60, immutable
20-
```
21-
22-
*heka1024*
23-
24-
* Add `:wasm_unsafe_eval` mapping for `content_security_policy`
1+
* Accept render options and block in `render` calls made with `:renderable`
252

263
```ruby
27-
# Before
28-
policy.script_src "'wasm-unsafe-eval'"
29-
30-
# After
31-
policy.script_src :wasm_unsafe_eval
4+
class Greeting
5+
def render_in(view_context, **options, &block)
6+
if block
7+
view_context.render html: block.call
8+
else
9+
view_context.render inline: <<~ERB.strip, **options
10+
Hello, <%= local_assigns.fetch(:name, "World") %>!
11+
ERB
12+
end
13+
end
14+
end
15+
16+
ApplicationController.render(Greeting.new) # => "Hello, World!"
17+
ApplicationController.render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
18+
ApplicationController.render(renderable: Greeting.new) # => "Hello, World!"
19+
ApplicationController.render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local!"
3220
```
3321

34-
*Joe Haig*
35-
36-
* Add `display_capture` and `keyboard_map` in `permissions_policy`
37-
38-
*Cyril Blaecke*
22+
*Sean Doyle*
3923

4024
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/actionpack/CHANGELOG.md) for previous changes.

actionpack/lib/abstract_controller/rendering.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ def render(*args, &block)
4444
# to be overridden in order to still return a string.
4545
def render_to_string(*args, &block)
4646
options = _normalize_render(*args, &block)
47-
render_to_body(options)
47+
render_to_body(options, &block)
4848
end
4949

5050
# Performs the actual template rendering.
51-
def render_to_body(options = {})
51+
def render_to_body(options = {}, &block)
5252
end
5353

5454
# Returns `Content-Type` of rendered content.

actionpack/lib/action_controller/metal/rendering.rb

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,27 @@ def inherited(klass)
3838
# render :show
3939
# # => renders app/views/posts/show.html.erb
4040
#
41-
# If the first argument responds to `render_in`, the template will be rendered
42-
# by calling `render_in` with the current view context.
41+
# If the first argument responds to +render_in+, the template will be
42+
# rendered by calling +render_in+ with the current view context, render
43+
# options, and block.
4344
#
4445
# class Greeting
45-
# def render_in(view_context)
46-
# view_context.render html: "<h1>Hello, World</h1>"
46+
# def render_in(view_context, **options, &block)
47+
# if block
48+
# view_context.render html: block.call
49+
# else
50+
# view_context.render inline: <<~ERB.strip, **options
51+
# <h1><%= Hello, <%= local_assigns.fetch(:name, "World") %></h1>
52+
# ERB
53+
# end
4754
# end
4855
#
49-
# def format
50-
# :html
51-
# end
52-
# end
53-
#
54-
# render(Greeting.new)
55-
# # => "<h1>Hello, World</h1>"
56-
#
57-
# render(renderable: Greeting.new)
58-
# # => "<h1>Hello, World</h1>"
56+
# render(Greeting.new) # => "<h1>Hello, World</h1>"
57+
# render(renderable: Greeting.new) # => "<h1>Hello, World</h1>"
58+
# render(Greeting.new, name: "Local") # => "<h1>Hello, Local</h1>"
59+
# render(renderable: Greeting.new, locals: { name: "Local" }) # => "<h1>Hello, Local</h1>"
60+
# render(Greeting.new) { "<h1>Hello, Block</h1>" } # => "<h1>Hello, Block</h1>"
61+
# render(renderable: Greeting.new) { "<h1>Hello, Block<h1>" } # => "<h1>Hello, Block</h1>"
5962
#
6063
# #### Rendering Mode
6164
#
@@ -112,13 +115,15 @@ def inherited(klass)
112115
#
113116
# `:renderable`
114117
# : Renders the provided object by calling `render_in` with the current view
115-
# context. The response format is determined by calling `format` on the
116-
# renderable if it responds to `format`, falling back to `text/html` by
117-
# default.
118+
# context, render options, and block. The response format is determined by
119+
# calling `format` on the renderable if it responds to `format`, falling
120+
# back to `text/html` by default.
118121
#
119122
# render renderable: Greeting.new
120123
# # => renders "<h1>Hello, World</h1>"
121124
#
125+
# render renderable: Greeting.new, locals: { name: "Local" }
126+
# # => renders "<h1>Hello, Local</h1>"
122127
#
123128
# By default, when a rendering mode is specified, no layout template is
124129
# rendered.

actionpack/lib/action_controller/renderer.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ def defaults
126126

127127
# Renders a template to a string, just like
128128
# ActionController::Rendering#render_to_string.
129-
def render(*args)
129+
def render(...)
130130
request = ActionDispatch::Request.new(env_for_request)
131131
request.routes = controller._routes
132132

133133
instance = controller.new
134134
instance.set_request! request
135135
instance.set_response! controller.make_response!(request)
136-
instance.render_to_string(*args)
136+
instance.render_to_string(...)
137137
end
138138
alias_method :render_to_string, :render # :nodoc:
139139

actionpack/test/controller/render_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "abstract_unit"
44
require "controller/fake_models"
55
require "support/etag_helper"
6+
require "test_renderable"
67

78
class TestControllerWithExtraEtags < ActionController::Base
89
self.view_paths = [ActionView::FixtureResolver.new(
@@ -369,6 +370,10 @@ class MetalTestController < ActionController::Metal
369370
def accessing_logger_in_template
370371
render inline: "<%= logger.class %>"
371372
end
373+
374+
def render_renderable
375+
render renderable: TestRenderable.new, locals: params.fetch(:locals, {})
376+
end
372377
end
373378

374379
class ExpiresInRenderTest < ActionController::TestCase
@@ -793,6 +798,16 @@ def test_access_to_logger_in_view
793798
get :accessing_logger_in_template
794799
assert_equal "NilClass", @response.body
795800
end
801+
802+
def test_render_renderable
803+
get :render_renderable
804+
805+
assert_equal "Hello, World!", @response.parsed_body.text
806+
807+
get :render_renderable, params: { locals: { name: "Local" } }
808+
809+
assert_equal "Hello, Local!", @response.parsed_body.text
810+
end
796811
end
797812

798813
class ActionControllerRenderTest < ActionController::TestCase

actionpack/test/controller/renderer_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ class RendererTest < ActiveSupport::TestCase
7777
%(Hello, World!),
7878
renderer.render(renderable: TestRenderable.new)
7979
)
80+
assert_equal(
81+
%(Hello, Local!),
82+
renderer.render(TestRenderable.new, name: "Local")
83+
)
84+
assert_equal(
85+
%(Hello, Local!),
86+
renderer.render(renderable: TestRenderable.new, locals: { name: "Local" })
87+
)
88+
end
89+
90+
test "render a renderable object with block" do
91+
renderer = ApplicationController.renderer
92+
93+
assert_equal(
94+
%(<h1>Goodbye, World!</h1>),
95+
renderer.render(TestRenderable.new) { "<h1>Goodbye, World!</h1>".html_safe }
96+
)
97+
assert_equal(
98+
%(<h1>Goodbye, World!</h1>),
99+
renderer.render(renderable: TestRenderable.new) { "<h1>Goodbye, World!</h1>".html_safe }
100+
)
80101
end
81102

82103
test "rendering with custom env" do

actionpack/test/lib/test_renderable.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# frozen_string_literal: true
22

33
class TestRenderable
4-
def render_in(_view_context)
5-
"Hello, World!"
4+
def render_in(view_context, locals: {}, **options, &block)
5+
if block
6+
view_context.render html: block.call
7+
else
8+
view_context.render inline: <<~ERB.strip, locals: locals
9+
Hello, <%= local_assigns.fetch(:name, "World") %>!
10+
ERB
11+
end
612
end
713

814
def format

actionview/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,24 @@
1+
* Pass render options and block to calls to `#render_in`
2+
3+
```ruby
4+
class Greeting
5+
def render_in(view_context, **options, &block)
6+
if block
7+
view_context.render html: block.call
8+
else
9+
view_context.render inline: <<~ERB.strip, **options
10+
Hello, <%= local_assigns.fetch(:name, "World") %>!
11+
ERB
12+
end
13+
end
14+
end
15+
16+
render(Greeting.new) # => "Hello, World!"
17+
render(Greeting.new, name: "Local") # => "Hello, Local!"
18+
render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local!"
19+
render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
20+
```
21+
22+
*Sean Doyle*
123

224
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/actionview/CHANGELOG.md) for previous changes.

actionview/lib/action_view/helpers/rendering_helper.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,23 @@ module RenderingHelper
2424
# If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
2525
#
2626
# If an object responding to +render_in+ is passed, +render_in+ is called on the object,
27-
# passing in the current view context.
27+
# passing in the current view context, render options, and block. The
28+
# object can optionally control its rendered format by defining the +format+ method.
2829
#
2930
# Otherwise, a partial is rendered using the second parameter as the locals hash.
3031
def render(options = {}, locals = {}, &block)
3132
case options
3233
when Hash
3334
in_rendering_context(options) do |renderer|
34-
if block_given?
35+
if block_given? && !options.key?(:renderable)
3536
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
3637
else
37-
view_renderer.render(self, options)
38+
view_renderer.render(self, options, &block)
3839
end
3940
end
4041
else
4142
if options.respond_to?(:render_in)
42-
options.render_in(self, &block)
43+
view_renderer.render(self, renderable: options, locals: locals, &block)
4344
else
4445
view_renderer.render_partial(self, partial: options, locals: locals, &block)
4546
end

actionview/lib/action_view/renderer/renderer.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ def initialize(lookup_context)
2020
end
2121

2222
# Main render entry point shared by Action View and Action Controller.
23-
def render(context, options)
24-
render_to_object(context, options).body
23+
def render(context, options, &block)
24+
render_to_object(context, options, &block).body
2525
end
2626

27-
def render_to_object(context, options) # :nodoc:
27+
def render_to_object(context, options, &block) # :nodoc:
2828
if options.key?(:partial)
2929
render_partial_to_object(context, options)
3030
else
31-
render_template_to_object(context, options)
31+
render_template_to_object(context, options, &block)
3232
end
3333
end
3434

@@ -54,8 +54,8 @@ def cache_hits # :nodoc:
5454
end
5555

5656
private
57-
def render_template_to_object(context, options)
58-
TemplateRenderer.new(@lookup_context).render(context, options)
57+
def render_template_to_object(context, options, &block)
58+
TemplateRenderer.new(@lookup_context).render(context, options, &block)
5959
end
6060

6161
def render_partial_to_object(context, options, &block)

0 commit comments

Comments
 (0)