Problem
When creating custom Turbo Stream actions that don't require a target element but
need custom attributes, there's no clean way to generate them from a controller
using turbo_stream.action().
Use Case
We have a custom Turbo Stream action for redirecting from within a Turbo Frame:
// Custom Turbo Stream action
Turbo.StreamActions.redirect_to = function () {
const redirectTo = this.getAttribute("redirect_to")
Turbo.visit(redirectTo)
}
Expected HTML output:
What We Tried
Attempt 1: Pass nil as target with custom attribute
render turbo_stream: turbo_stream.action(:redirect_to, nil, redirect_to: url)
Result: ArgumentError: You invoked render but did not give any of :body, :file,
:html...
Attempt 2: Omit target, pass only custom attribute
render turbo_stream: turbo_stream.action(:redirect_to, redirect_to: url)
Result: ArgumentError: wrong number of arguments (given 1, expected 2..3)
Root Cause
Looking at TagBuilder#action:
def action(name, target, content = nil, method: nil, allow_inferred_rendering:
true, **rendering, &block)
template = render_template(target, content, allow_inferred_rendering:
allow_inferred_rendering, **rendering, &block)
turbo_stream_action_tag name, target: target, template: template, method:
method
end
Two issues:
- target is a required positional argument
- Custom attributes in **rendering are not passed through to
turbo_stream_action_tag
Meanwhile, turbo_stream_action_tag in ActionHelper already supports both optional
targets and custom attributes:
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil,
**attributes)
# ...
tag.turbo_stream(template, **attributes, action: action)
end
Current Workaround
render turbo_stream: helpers.turbo_stream_action_tag(:redirect_to, redirect_to:
url)
This works but feels like unnecessary indirection when turbo_stream.action()
should handle it.
Proposed Fix
Update TagBuilder#action to:
- Make target a keyword argument with nil default
- Accept and pass through custom attributes
def action(name, target: nil, content: nil, method: nil,
allow_inferred_rendering: true, **attributes, &block)
template = render_template(target, content, allow_inferred_rendering:
allow_inferred_rendering, &block)
turbo_stream_action_tag name, target: target, template: template, method:
method, **attributes
end
This would allow:
turbo_stream.action(:redirect_to, redirect_to: url)
turbo_stream.action(:custom_action, target: "my-element", custom_attr: "value")
Breaking Change Consideration
Changing target from positional to keyword would be breaking. An alternative is
to add a new method like action_without_target or detect when first argument is a
symbol/hash, though neither is as clean.
Environment
- turbo-rails 2.0.20
- Rails 8.0
Problem
When creating custom Turbo Stream actions that don't require a target element but
need custom attributes, there's no clean way to generate them from a controller
using
turbo_stream.action().Use Case
We have a custom Turbo Stream action for redirecting from within a Turbo Frame:
Expected HTML output:
What We Tried
Attempt 1: Pass nil as target with custom attribute
render turbo_stream: turbo_stream.action(:redirect_to, nil, redirect_to: url)
Result: ArgumentError: You invoked render but did not give any of :body, :file,
:html...
Attempt 2: Omit target, pass only custom attribute
render turbo_stream: turbo_stream.action(:redirect_to, redirect_to: url)
Result: ArgumentError: wrong number of arguments (given 1, expected 2..3)
Root Cause
Looking at TagBuilder#action:
def action(name, target, content = nil, method: nil, allow_inferred_rendering:
true, **rendering, &block)
template = render_template(target, content, allow_inferred_rendering:
allow_inferred_rendering, **rendering, &block)
turbo_stream_action_tag name, target: target, template: template, method:
method
end
Two issues:
turbo_stream_action_tag
Meanwhile, turbo_stream_action_tag in ActionHelper already supports both optional
targets and custom attributes:
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil,
**attributes)
# ...
tag.turbo_stream(template, **attributes, action: action)
end
Current Workaround
render turbo_stream: helpers.turbo_stream_action_tag(:redirect_to, redirect_to:
url)
This works but feels like unnecessary indirection when turbo_stream.action()
should handle it.
Proposed Fix
Update TagBuilder#action to:
def action(name, target: nil, content: nil, method: nil,
allow_inferred_rendering: true, **attributes, &block)
template = render_template(target, content, allow_inferred_rendering:
allow_inferred_rendering, &block)
turbo_stream_action_tag name, target: target, template: template, method:
method, **attributes
end
This would allow:
turbo_stream.action(:redirect_to, redirect_to: url)
turbo_stream.action(:custom_action, target: "my-element", custom_attr: "value")
Breaking Change Consideration
Changing target from positional to keyword would be breaking. An alternative is
to add a new method like action_without_target or detect when first argument is a
symbol/hash, though neither is as clean.
Environment