Skip to content

TagBuilder#action doesn't support custom attributes or optional targets #771

Description

@aviflombaum

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:

  1. target is a required positional argument
  2. 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:

  1. Make target a keyword argument with nil default
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions