Skip to content

Commit aa39599

Browse files
committed
Introduce turbo-stream[action=visit] and #break_out_of_turbo_frame_and_redirect_to
Introduces the `Turbo::Stream::Redirect` concern to introduce the `#break_out_of_turbo_frame_and_redirect_to` and `#turbo_stream_redirect_to` methods. The `#break_out_of_turbo_frame_and_redirect_to` draws inspiration from the methods provided by the [Turbo::Native::Navigation][] concern. When handling requests made from outside a `<turbo-frame>` elements (without the `Turbo-Frame` HTTP header), respond with a typical HTML redirect response. When handling request made from inside a `<turbo-frame>` element (with the `Turbo-Frame` HTTP header), render a `<turbo-stream action="visit">` element with the redirect's pathname or URL encoded into the `[location]` attribute. When Turbo Drive receives the response, it will call `Turbo.visit()` with the value read from the `[location]` attribute. ```ruby class ArticlesController < ApplicationController def show @Article = Article.find(params[:id]) end def create @Article = Article.new(article_params) if @article.save break_out_of_turbo_frame_and_redirect_to @Article else render :new, status: :unprocessable_entity end end end ``` Response options (like `:notice`, `:alert`, `:status`, etc.) are forwarded to the underlying redirection mechanism (`#redirect_to` for `Mime[:html]` requests and `#turbo_stream_redirect_to` for `Mime[:turbo_stream]` requests). This enables server-side actions to navigate the entire page with a, regardless of the provenance of the request. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. To support this behavior, this commit introduces the first `@hotwire/turbo-rails`-specific Turbo `StreamAction`: the `turbo-stream[action="visit"]`. [Turbo::Native::Navigation]: https://github.com/hotwired/turbo-rails/blob/v2.0.11/app/controllers/turbo/native/navigation.rb
1 parent aee95a8 commit aa39599

File tree

15 files changed

+211
-159
lines changed

15 files changed

+211
-159
lines changed

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ end
3434

3535
group :test do
3636
gem 'capybara'
37-
gem 'capybara_accessible_selectors', github: 'citizensadvice/capybara_accessible_selectors', branch: 'main'
3837
gem 'rexml'
3938
gem 'cuprite', '~> 0.9', require: 'capybara/cuprite'
4039
gem 'sqlite3', '1.5'

app/assets/javascripts/turbo.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5437,6 +5437,10 @@ function isBodyInit(body) {
54375437
return body instanceof FormData || body instanceof URLSearchParams;
54385438
}
54395439

5440+
StreamActions.visit = function() {
5441+
visit(this.getAttribute("location"));
5442+
};
5443+
54405444
window.Turbo = Turbo$1;
54415445

54425446
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);

app/assets/javascripts/turbo.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/turbo.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,78 @@
11
module Turbo::Streams::Redirect
22
extend ActiveSupport::Concern
33

4-
def redirect_to(options = {}, response_options = {})
5-
turbo_frame = response_options.delete(:turbo_frame)
6-
turbo_action = response_options.delete(:turbo_action)
7-
location = url_for(options)
4+
private
85

9-
if request.format.turbo_stream? && turbo_frame.present?
10-
alert, notice, flash_override = response_options.values_at(:alert, :notice, :flash)
11-
flash.merge!(flash_override || {alert: alert, notice: notice})
6+
# Instruct Turbo Drive to redirect the page, regardless of whether or not the
7+
# request was made from within a `<turbo-frame>` element.
8+
#
9+
# When handling requests made from outside a `<turbo-frame>` elements (without
10+
# the `Turbo-Frame` HTTP header), respond with a typical HTML redirect
11+
# response.
12+
#
13+
# When handling request made from inside a `<turbo-frame>` element (with the
14+
# `Turbo-Frame` HTTP header), render a `<turbo-stream action="visit">` element
15+
# with the redirect's pathname or URL encoded into the `[location]` attribute.
16+
#
17+
# When Turbo Drive receives the response, it will call `Turbo.visit()` with
18+
# the value read from the `[location]` attribute.
19+
#
20+
# class ArticlesController < ApplicationController
21+
# def show
22+
# @article = Article.find(params[:id])
23+
# end
24+
#
25+
# def create
26+
# @article = Article.new(article_params)
27+
#
28+
# if @article.save
29+
# break_out_of_turbo_frame_and_redirect_to @article
30+
# else
31+
# render :new, status: :unprocessable_entity
32+
# end
33+
# end
34+
# end
35+
#
36+
# Response options (like `:notice`, `:alert`, `:status`, etc.) are forwarded
37+
# to the underlying redirection mechanism (`#redirect_to` for `Mime[:html]`
38+
# requests and `#turbo_stream_redirect_to` for `Mime[:turbo_stream]`
39+
# requests).
40+
def break_out_of_turbo_frame_and_redirect_to(options = {}, response_options_and_flash = {}) # :doc:
41+
respond_to do |format|
42+
format.html { redirect_to(options, response_options_and_flash) }
1243

13-
case Rack::Utils.status_code(response_options.fetch(:status, :created))
14-
when 300..399 then response_options[:status] = :created
44+
if turbo_frame_request?
45+
format.turbo_stream { turbo_stream_redirect_to(options, response_options_and_flash) }
1546
end
47+
end
48+
end
1649

17-
render "turbo/streams/redirect", **response_options.with_defaults(
18-
locals: {location: location, turbo_frame: turbo_frame, turbo_action: turbo_action},
19-
location: location,
20-
)
21-
else
22-
super
50+
# Respond with a `<turbo-stream action="visit">` with the `[location]`
51+
# attribute set to the pathname or URL. Preserves `:alert`, `:notice`, and
52+
# `:flash` options.
53+
#
54+
# When passed a `:status` HTTP status code option between `300` and `399`,
55+
# replace it with a `201 Created` status and set the `Location` HTTP header to
56+
# the pathname or URL.
57+
def turbo_stream_redirect_to(options = {}, response_options_and_flash = {}) # :doc:
58+
location = url_for(options)
59+
60+
self.class._flash_types.each do |flash_type|
61+
if (type = response_options_and_flash.delete(flash_type))
62+
flash[flash_type] = type
63+
end
2364
end
65+
66+
if (other_flashes = response_options_and_flash.delete(:flash))
67+
flash.update(other_flashes)
68+
end
69+
70+
case Rack::Utils.status_code(response_options_and_flash.fetch(:status, :created))
71+
when 201, 300..399
72+
response_options_and_flash[:status] = :created
73+
response_options_and_flash[:location] = location
74+
end
75+
76+
render turbo_stream: turbo_stream.visit(location), **response_options_and_flash
2477
end
2578
end

app/helpers/turbo/streams/action_helper.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def turbo_stream_action_tag(action, attributes = {})
2828
target = attributes.delete(:target)
2929
targets = attributes.delete(:targets)
3030
template = attributes.delete(:template)
31-
template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe)
31+
template = action.to_sym.in?(%i[ remove refresh visit ]) ? "" : tag.template(template.to_s.html_safe)
3232

3333
if target = convert_to_turbo_stream_dom_id(target)
3434
tag.turbo_stream(template, **attributes, action: action, target: target)
@@ -47,6 +47,14 @@ def turbo_stream_refresh_tag(request_id: Turbo.current_request_id, **attributes)
4747
turbo_stream_action_tag(:refresh, attributes.with_defaults({ "request-id": request_id }.compact))
4848
end
4949

50+
# Creates a `turbo-stream` tag with an `action="visit"` attribute. Example:
51+
#
52+
# turbo_stream_visit_tag "/"
53+
# # => <turbo-stream action="visit" location="/"></turbo-stream>
54+
def turbo_stream_visit_tag(location)
55+
turbo_stream_action_tag(:visit, location: location)
56+
end
57+
5058
private
5159
def convert_to_turbo_stream_dom_id(target, include_selector: false)
5260
target_array = Array.wrap(target)

app/javascript/turbo/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export { cable }
88

99
import { encodeMethodIntoRequestBody } from "./fetch_requests"
1010

11+
/**
12+
* Call `Turbo.visit(location)`, where `location` is read from the
13+
* `<turbo-stream>` element's `[location]` attribute.
14+
*/
15+
Turbo.StreamActions.visit = function() {
16+
Turbo.visit(this.getAttribute("location"))
17+
}
18+
1119
window.Turbo = Turbo
1220

1321
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)

app/models/turbo/streams/tag_builder.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@ def refresh(**options)
244244
turbo_stream_refresh_tag(**options)
245245
end
246246

247+
# Creates a `turbo-stream` tag with an `[action="visit"]` attribute and an
248+
# `[location]` attribute
249+
#
250+
# turbo_stream.visit("/")
251+
# # => <turbo-stream action="visit" location="/"></turbo-stream>
252+
def visit(location)
253+
turbo_stream_visit_tag(location)
254+
end
255+
247256
# Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
248257
def action(name, target, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
249258
template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)

app/views/turbo/streams/redirect.turbo_stream.erb

Lines changed: 0 additions & 11 deletions
This file was deleted.

lib/turbo/test_assertions.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ module TestAssertions
4545
# assert_select "template p", text: "Hello!"
4646
# end
4747
#
48-
def assert_turbo_stream(action:, target: nil, targets: nil, count: 1, &block)
48+
def assert_turbo_stream(action:, target: nil, targets: nil, count: 1, **attributes, &block)
4949
selector = %(turbo-stream[action="#{action}"])
5050
selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
5151
selector << %([targets="#{targets}"]) if targets
52+
attributes.each { |name, value| selector << %([#{name}="#{value}"]) }
5253
assert_select selector, count: count, &block
5354
end
5455

0 commit comments

Comments
 (0)