Skip to content

Commit b554fd3

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 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 84f773f commit b554fd3

File tree

15 files changed

+270
-46
lines changed

15 files changed

+270
-46
lines changed

actionpack/CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
* Accept render options and block in `render_to_string` calls made with `:renderable`
2+
3+
```ruby
4+
class Greeting
5+
def render_in(view_context, locals: {}, formats: nil, &block)
6+
if block
7+
view_context.render plain: block.call
8+
else
9+
case Array(formats).first
10+
when :json
11+
view_context.render plain: { greeting: "Hello, #{locals.fetch(:name, "World")}!" }.to_json
12+
else
13+
view_context.render inline: <<~ERB.strip, locals: locals
14+
Hello, <%= local_assigns.fetch(:name, "World") %>!
15+
ERB
16+
end
17+
end
18+
end
19+
end
20+
21+
ApplicationController.render(Greeting.new, name: "Local") # => "Hello, Local!"
22+
ApplicationController.render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
23+
ApplicationController.render(renderable: Greeting.new, formats: :json) # => "{\"greeting\":\"Hello, World!\"}"
24+
```
25+
26+
*Sean Doyle*
27+
128
* Add `allow_browser` to set minimum browser versions for the application.
229

330
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".

actionpack/lib/abstract_controller/rendering.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ def render(*args, &block)
4242
# needs to be overridden in order to still return a string.
4343
def render_to_string(*args, &block)
4444
options = _normalize_render(*args, &block)
45-
render_to_body(options)
45+
render_to_body(options, &block)
4646
end
4747

4848
# Performs the actual template rendering.
49-
def render_to_body(options = {})
49+
def render_to_body(options = {}, &block)
5050
end
5151

5252
# Returns +Content-Type+ of rendered content.

actionpack/lib/action_controller/metal/rendering.rb

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,31 @@ def inherited(klass)
3737
# # => renders app/views/posts/show.html.erb
3838
#
3939
# If the first argument responds to +render_in+, the template will be
40-
# rendered by calling +render_in+ with the current view context.
40+
# rendered by calling +render_in+ with the current view context, render
41+
# options, and block.
42+
#
43+
# class Greeting
44+
# def render_in(view_context, **options, &block)
45+
# if block
46+
# view_context.render html: block.call
47+
# else
48+
# view_context.render inline: <<~ERB.strip, **options
49+
# <%= Hello, <%= local_assigns.fetch(:name, "World") %>
50+
# ERB
51+
# end
52+
# end
53+
#
54+
# def format
55+
# :html
56+
# end
57+
# end
58+
#
59+
# render(Greeting.new) # => "Hello, World"
60+
# render(renderable: Greeting.new) # => "Hello, World"
61+
# render(Greeting.new, name: "Local") # => "Hello, Local"
62+
# render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local"
63+
# render(Greeting.new) { "Hello, Block" } # => "Hello, Block"
64+
# render(renderable: Greeting.new) { "Hello, Block" } # => "Hello, Block"
4165
#
4266
# class Greeting
4367
# def render_in(view_context)
@@ -110,12 +134,16 @@ def inherited(klass)
110134
#
111135
# [+:renderable+]
112136
# Renders the provided object by calling +render_in+ with the current view
113-
# context. The response format is determined by calling +format+ on the
114-
# renderable if it responds to +format+, falling back to +text/html+ by default.
137+
# context, render options, and block. The response format is determined by
138+
# calling +format+ on the renderable if it responds to +format+, falling
139+
# back to +text/html+ by default.
115140
#
116141
# render renderable: Greeting.new
117142
# # => renders "<h1>Hello, World</h1>"
118143
#
144+
# render renderable: Greeting.new, locals: { name: "Local" }
145+
# # => renders "Hello, Local"
146+
#
119147
# By default, when a rendering mode is specified, no layout template is
120148
# rendered.
121149
#

actionpack/lib/action_controller/renderer.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,14 @@ def defaults
120120
end
121121

122122
# Renders a template to a string, just like ActionController::Rendering#render_to_string.
123-
def render(*args)
123+
def render(...)
124124
request = ActionDispatch::Request.new(env_for_request)
125125
request.routes = controller._routes
126126

127127
instance = controller.new
128128
instance.set_request! request
129129
instance.set_response! controller.make_response!(request)
130-
instance.render_to_string(*args)
130+
instance.render_to_string(...)
131131
end
132132
alias_method :render_to_string, :render # :nodoc:
133133

actionpack/test/controller/renderer_test.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ 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 :formats" do
91+
renderer = ApplicationController.renderer
92+
93+
assert_equal(
94+
{ greeting: "Hello, World!" }.to_json,
95+
renderer.render(renderable: TestRenderable.new, formats: :json)
96+
)
97+
assert_equal(
98+
{ greeting: "Hello, Local!" }.to_json,
99+
renderer.render(renderable: TestRenderable.new, locals: { name: "Local" }, formats: :json)
100+
)
101+
end
102+
103+
test "render a renderable object with block" do
104+
renderer = ApplicationController.renderer
105+
106+
assert_equal(
107+
%(Goodbye, World!),
108+
renderer.render(TestRenderable.new) { "Goodbye, World!" }
109+
)
110+
assert_equal(
111+
%(Goodbye, World!),
112+
renderer.render(renderable: TestRenderable.new) { "Goodbye, World!" }
113+
)
80114
end
81115

82116
test "rendering with custom env" do
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# frozen_string_literal: true
22

33
class TestRenderable
4-
def render_in(_view_context)
5-
"Hello, World!"
6-
end
4+
def render_in(view_context, locals: {}, formats: nil, **options, &block)
5+
if block
6+
view_context.render plain: block.call
7+
elsif Array(formats).first == :json
8+
json = { greeting: "Hello, #{locals.fetch(:name, "World")}!" }
79

8-
def format
9-
:html
10+
view_context.render plain: json.to_json
11+
else
12+
view_context.render inline: <<~ERB.strip, locals: locals
13+
Hello, <%= local_assigns.fetch(:name, "World") %>!
14+
ERB
15+
end
1016
end
1117
end

actionview/CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
* Pass render options and block to calls to `#render_in`
2+
3+
```ruby
4+
class Greeting
5+
def render_in(view_context, locals: {}, formats: nil, **options, &block)
6+
if block
7+
view_context.render plain: block.call
8+
else
9+
case Array(formats).first
10+
when :json
11+
json = { greeting: "Hello, #{locals.fetch(:name, "World")}!" }
12+
13+
view_context.render plain: json.to_json
14+
else
15+
view_context.render inline: <<~ERB.strip, locals: locals
16+
Hello, <%= local_assigns.fetch(:name, "World") %>!
17+
ERB
18+
end
19+
end
20+
end
21+
end
22+
23+
render(Greeting.new, name: "Local") # => "Hello, Local!"
24+
render(Greeting.new) { "Hello, Block!" } # => "Hello, Block!"
25+
render(renderable: Greeting.new, formats: :json) # => "{\"greeting\":\"Hello, World!\"}"
26+
```
27+
28+
*Sean Doyle*
29+
130
* Add the `nonce: true` option for `stylesheet_link_tag` helper to support automatic nonce generation for Content Security Policy.
231
Works the same way as `javascript_include_tag nonce: true` does.
332

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)

actionview/lib/action_view/renderer/template_renderer.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
module ActionView
44
class TemplateRenderer < AbstractRenderer # :nodoc:
5-
def render(context, options)
5+
def render(context, options, &block)
66
@details = extract_details(options)
7-
template = determine_template(options)
7+
template = determine_template(options, &block)
88

99
prepend_formats(template.format)
1010

@@ -13,7 +13,7 @@ def render(context, options)
1313

1414
private
1515
# Determine the template to be rendered using the given options.
16-
def determine_template(options)
16+
def determine_template(options, &block)
1717
keys = options.has_key?(:locals) ? options[:locals].keys : []
1818

1919
if options.key?(:body)
@@ -41,7 +41,7 @@ def determine_template(options)
4141
end
4242
Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
4343
elsif options.key?(:renderable)
44-
Template::Renderable.new(options[:renderable])
44+
Template::Renderable.new(options[:renderable], options[:formats], &block)
4545
elsif options.key?(:template)
4646
if options[:template].respond_to?(:render)
4747
options[:template]

0 commit comments

Comments
 (0)