-
Notifications
You must be signed in to change notification settings - Fork 329
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
"Break out" of a frame from the server
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. 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. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][], and marking it with [data-turbo-cache="false"][] [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript [data-turbo-cache="false"]: https://turbo.hotwired.dev/reference/attributes#data-attributes
- Loading branch information
1 parent
0e42702
commit aee95a8
Showing
8 changed files
with
241 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
module Turbo::Streams::Redirect | ||
extend ActiveSupport::Concern | ||
|
||
def redirect_to(options = {}, response_options = {}) | ||
turbo_frame = response_options.delete(:turbo_frame) | ||
turbo_action = response_options.delete(:turbo_action) | ||
location = url_for(options) | ||
|
||
if request.format.turbo_stream? && turbo_frame.present? | ||
alert, notice, flash_override = response_options.values_at(:alert, :notice, :flash) | ||
flash.merge!(flash_override || {alert: alert, notice: notice}) | ||
|
||
case Rack::Utils.status_code(response_options.fetch(:status, :created)) | ||
when 300..399 then response_options[:status] = :created | ||
end | ||
|
||
render "turbo/streams/redirect", **response_options.with_defaults( | ||
locals: {location: location, turbo_frame: turbo_frame, turbo_action: turbo_action}, | ||
location: location, | ||
) | ||
else | ||
super | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<%= turbo_stream.append_all "head" do %> | ||
<%= javascript_tag nonce: true, data: {turbo_cache: false} do %> | ||
window.Turbo.visit("<%= escape_javascript response.location %>", { | ||
frame: "<%= escape_javascript turbo_frame %>", | ||
<% if turbo_action.present? %> | ||
action: "<%= escape_javascript turbo_action %>", | ||
<% end %> | ||
}) | ||
document.currentScript.remove() | ||
<% end %> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<details> | ||
<summary>New Article</summary> | ||
|
||
<%= turbo_frame_tag @article do %> | ||
<%= form_with model: @article do |form| %> | ||
<%= form.label :body %> | ||
<%= form.text_area :body, aria: { describedby: (dom_id(@article, :errors) if @article.errors[:body].any?) } %> | ||
<% if @article.errors[:body].any? %> | ||
<p id="<%= dom_id(@article, :errors) %>"> | ||
<%= @article.errors[:body].to_sentence %> | ||
</p> | ||
<% end %> | ||
<%= form.button name: :turbo_frame, value: "_top"%> | ||
<% end %> | ||
<% end %> | ||
</details> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
require "test_helper" | ||
|
||
class Turbo::Streams::RedirectTest < ActionDispatch::IntegrationTest | ||
test "html requests respond with a redirect HTTP status" do | ||
post articles_path, params: { | ||
turbo_frame: "_top", status: 303, | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_response :see_other | ||
assert_redirected_to articles_url | ||
assert_equal "Created!", flash[:notice] | ||
end | ||
|
||
test "html redirects write to the flash" do | ||
post articles_path, params: { | ||
turbo_frame: "_top", flash: {alert: "Wrote to alert:"}, | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to alert:", flash[:alert] | ||
end | ||
|
||
test "html redirects write to alert" do | ||
post articles_path, params: { | ||
turbo_frame: "_top", alert: "Wrote to alert:", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to alert:", flash[:alert] | ||
end | ||
|
||
test "html redirects write to notice" do | ||
post articles_path, params: { | ||
turbo_frame: "_top", notice: "Wrote to notice:", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to notice:", flash[:notice] | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: option responds with a redirect Turbo Stream" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_turbo_stream action: :append, targets: "head", status: :created do | ||
assert_select "script[data-turbo-cache=false]", count: 1 do |script| | ||
assert_includes script.text, %(frame: "_top",) | ||
assert_not_includes script.text, %(action:) | ||
end | ||
end | ||
assert_equal articles_url, response.location | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: and turbo_action: options responds with a redirect Turbo Stream" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", | ||
turbo_action: "replace", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_turbo_stream action: :append, targets: "head", status: :created do | ||
assert_select "script[data-turbo-cache=false]", count: 1 do |script| | ||
assert_includes script.text, %(frame: "_top",) | ||
assert_includes script.text, %(action: "replace",) | ||
end | ||
end | ||
assert_equal articles_url, response.location | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: option preserves status: values in the 2xx range" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", status: 200, | ||
article: { body: "A valid value" } | ||
} | ||
|
||
assert_response 200 | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: option replaces status: values in the 3xx range with 201 Created" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", status: 303, | ||
article: { body: "A valid value" } | ||
} | ||
|
||
assert_response 201 | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: option preserves status: values in the 4xx range" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", status: 403, | ||
article: { body: "A valid value" } | ||
} | ||
|
||
assert_response 403 | ||
end | ||
|
||
test "turbo_stream requests with the turbo_frame: option preserves status: values in the 5xx range" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", status: 500, | ||
article: { body: "A valid value" } | ||
} | ||
|
||
assert_response 500 | ||
end | ||
|
||
test "turbo_stream requests without the turbo_frame: option respond with a redirect HTTP status" do | ||
post articles_path, as: :turbo_stream, params: { | ||
article: { body: "A valid value" } | ||
} | ||
|
||
assert_redirected_to articles_url | ||
end | ||
|
||
test "turbo_stream redirects write to the flash" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", flash: {alert: "Wrote to alert:"}, | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to alert:", flash[:alert] | ||
end | ||
|
||
test "turbo_stream redirects write to alert" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", alert: "Wrote to alert:", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to alert:", flash[:alert] | ||
end | ||
|
||
test "turbo_stream redirects write to notice" do | ||
post articles_path, as: :turbo_stream, params: { | ||
turbo_frame: "_top", notice: "Wrote to notice:", | ||
article: {body: "A valid value"} | ||
} | ||
|
||
assert_equal "Wrote to notice:", flash[:notice] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
require "application_system_test_case" | ||
|
||
class FramesTest < ApplicationSystemTestCase | ||
test "can render an invalid submission within a frame" do | ||
visit new_article_path | ||
toggle_disclosure "New Article" do | ||
click_on "Create Article" | ||
end | ||
|
||
within_disclosure "New Article" do | ||
assert_field "Body", described_by: "can't be blank" | ||
end | ||
end | ||
|
||
test "can redirect the entire page after a valid submission within a frame" do | ||
visit new_article_path | ||
toggle_disclosure "New Article" do | ||
fill_in "Body", with: "An article's body" | ||
click_on "Create Article" | ||
end | ||
|
||
assert_no_selector :disclosure, "New Article" | ||
assert_no_field "Body" | ||
assert_text "An article's body" | ||
end | ||
end |