Skip to content

Commit

Permalink
turbo_stream tag builder: support :partial with block
Browse files Browse the repository at this point in the history
The Background
---

Consider an application with a shared `application/flash` partial that
accepts its message as a partial-local assignment:

```erb
<%# app/views/application/_flash.html.erb %>

<p role="alert"><%= message %></p>
```

Also consider an `application/layout.turbo_stream.erb` template for
appending the `Flash` messages to a element with `[id="flashes"]`:

```erb
<%# app/views/layouts/application.turbo_stream.erb %>

<% flash.each do |name, message| %>
  <%= turbo_stream.append "flashes", partial: "application/flash", locals: { message: message } %>
<% end %>
```

This works as you'd expect, since the `:partial` and `:locals` keyword
arguments are forwarded along to the underlying `render` call.

The Scenario
---

Now consider that the `application/flash` message changed its interface
to expect the `message` as block content yielded to the partial instead
of an assignment to the `:locals` options:

```diff
 <%# app/views/application/_flash.html.erb %>

-<p role="alert"><%= message %></p>
+<p role="alert"><%= yield %></p>
```

The `layouts/application.turbo_stream.erb` template would need to change
as well:

```diff
 <%# app/views/layouts/application.turbo_stream.erb %>

 <% flash.each do |name, message| %>
-  <%= turbo_stream.append "flashes", partial: "application/flash", locals: { message: message } %>
+  <%= turbo_stream.append "flashes", partial: "application/flash" do %>
+    <span style="color: red"><%= message %></span>
+  <%= end %>
 <% end %>
```

The Problem
---

This style of invocation of `turbo_stream.append` does not work the same
as if it were passed a block of content generated by calling `render`
with the same keywords.

The presence of a `&block` argument triggers an entirely separate code
path than the presence of the `**rendering` keywords.

To work around this issue, you'd have to capture the rendering
separately:

```diff
 <%# app/views/layouts/application.turbo_stream.erb %>

 <% flash.each do |name, message| %>
-  <%= turbo_stream.append "flashes", partial: "application/flash", locals: { message: message } %>
+  <% content = capture do %>
+    <%= render partial: "application/flash" do %>
+      <span style="color: red"><%= message %></span>
+    <% end %>
+  <% end %>
+
+  <%= turbo_stream.append "flashes", content %>
 <% end %>
```

The Proposal
---

This commit alters the tag builder's decision making process to
incorporate a check for a combination of both a `&block` and `:partial`
or `:layout` keyword arguments.
  • Loading branch information
seanpdoyle committed Dec 5, 2024
1 parent 0e42702 commit 9373c41
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/models/turbo/streams/tag_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ def render_template(target, content = nil, allow_inferred_rendering: true, **ren
content.render_in(@view_context, &block)
when content
allow_inferred_rendering ? (render_record(content) || content) : content
when block_given? && (rendering.key?(:partial) || rendering.key?(:layout))
@view_context.render(formats: [ :html ], layout: rendering[:partial], **rendering, &block)
when block_given?
@view_context.capture(&block)
when rendering.any?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p><%= yield %></p>
42 changes: 42 additions & 0 deletions test/streams/streams_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,48 @@ def to_key

attr_accessor :formats

test "turbo_stream builder captures block when called without :partial keyword" do
rendered = turbo_stream.update "target_id" do
tag.span "Hello, world"
end

assert_dom_equal <<~HTML.strip, rendered
<turbo-stream action="update" target="target_id">
<template>
<span>Hello, world</span>
</template>
</turbo-stream>
HTML
end

test "turbo_stream builder forwards block to partial when called with :partial keyword" do
rendered = turbo_stream.update "target_id", partial: "application/partial_with_block" do
"Hello, from application/partial_with_block partial"
end

assert_dom_equal <<~HTML.strip, rendered
<turbo-stream action="update" target="target_id">
<template>
<p>Hello, from application/partial_with_block partial</p>
</template>
</turbo-stream>
HTML
end

test "turbo_stream builder forwards block to partial when called with :layout keyword" do
rendered = turbo_stream.update "target_id", layout: "application/partial_with_block" do
"Hello, from application/partial_with_block partial"
end

assert_dom_equal <<~HTML.strip, rendered
<turbo-stream action="update" target="target_id">
<template>
<p>Hello, from application/partial_with_block partial</p>
</template>
</turbo-stream>
HTML
end

test "supports valid :renderable option object with nil content" do
component = Component.new(id: 1, content: "Hello, world")

Expand Down

0 comments on commit 9373c41

Please sign in to comment.