-
Notifications
You must be signed in to change notification settings - Fork 116
Describe criteria for link and form morphing #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,18 @@ A typical scenario for page refreshes is submitting a form and getting redirecte | |||||||||
|
|
||||||||||
| ${toc} | ||||||||||
|
|
||||||||||
| ## Page Refreshes | ||||||||||
|
|
||||||||||
| A "page refresh" is a [application visit](/handbook/drive#application-visits) with a `"replace"` action to a URL with a whose [pathname](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) matches the current URL [path](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL#path_to_resource). Page refreshes can be initiated by driving the page with a link, or by [redirecting after a form submission](/handbook/drive#redirecting-after-a-form-submission). In either case, the elements must have a `[data-turbo-action="replace"]` attribute: | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seanpdoyle here I'd clarify that that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we clarify that URL parameters are not considered in this pathname check? It should be a given, but if I don't read the linked MDN article I might forget. |
||||||||||
|
|
||||||||||
| ```html | ||||||||||
| <a href="/" data-turbo-action="replace">Page refresh link</a> | ||||||||||
|
|
||||||||||
| <form action="/redirect_back" method="post" data-turbo-action="replace"> | ||||||||||
| <button>Page refresh form</button> | ||||||||||
| </form> | ||||||||||
|
Comment on lines
+19
to
+23
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jorgemanrubia is this guidance correct? Are the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seanpdoyle same-location redirects are already So the attribute is not necessary.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jorgemanrubia this documentation was mostly in response to confusion stemming from I've verified that redirects in the way you've described *do* work as expected:require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails"
gem "propshaft"
gem "sqlite3"
gem "turbo-rails"
gem "capybara"
gem "cuprite", "~> 0.9", require: "capybara/cuprite"
end
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
# require "action_mailer/railtie"
# require "active_job/railtie"
# require "action_cable/engine"
# require "action_mailbox/engine"
# require "action_text/engine"
require "rails/test_unit/railtie"
class App < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.root = __dir__
config.hosts << "example.org"
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.consider_all_requests_local = true
config.turbo.draw_routes = false
Rails.logger = config.logger = Logger.new($stdout)
routes.append do
post "/" => "application#create"
root to: "application#index"
end
end
class ApplicationController < ActionController::Base
include Rails.application.routes.url_helpers
class_attribute :template, default: DATA.read
def index
render inline: template, formats: :html
end
def create
redirect_to root_url
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chrome, screen_size: [1400, 1400], options: { js_errors: true}
end
Capybara.configure do |config|
config.server = :webrick
config.default_normalize_ws = true
end
ENV["DATABASE_URL"] = "sqlite3::memory:"
ENV["RAILS_ENV"] ||= "test"
Rails.application.initialize!
require "rails/test_help"
class TurboSystemTest < ApplicationSystemTestCase
test "reproduces bug" do
visit root_path
assert_css "h1", text: "Loaded without morphing"
click_button "Morph"
assert_css "h1", text: "Loaded with morph"
end
end
__END__
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<script type="importmap">
{
"imports": {
"@hotwired/turbo-rails": "<%= asset_path("turbo.js") %>"
}
}
</script>
<script type="module">
import "@hotwired/turbo-rails"
addEventListener("turbo:morph", () => document.querySelector("h1").textContent = "Loaded with morph")
</script>
<meta name="turbo-refresh-method" content="morph">
</head>
<body>
<h1>Loaded without morphing</h1>
<%= button_to "Morph", root_path %>
</body>
</html>
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On further inspection, I realized the difference between #178 (comment) and #178 (comment). The Navigator.getDefaultAction you've shared: #getDefaultAction(fetchResponse) {
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href
return sameLocationRedirect ? "replace" : "advance"
}differs from the Visit.isPageRefresh comparison: isPageRefresh(visit) {
return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
}The Put another way, redirects back require exact matches of the full URLs (including query parameters), while refreshing when the action is forced only compares the URL paths (excluding query parameters). This script passes with the `data: {turbo_action: "replace"}` option, then fails without the option:require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails"
gem "propshaft"
gem "sqlite3"
gem "turbo-rails"
gem "capybara"
gem "cuprite", "~> 0.9", require: "capybara/cuprite"
end
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
# require "action_mailer/railtie"
# require "active_job/railtie"
# require "action_cable/engine"
# require "action_mailbox/engine"
# require "action_text/engine"
require "rails/test_unit/railtie"
class App < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.root = __dir__
config.hosts << "example.org"
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.consider_all_requests_local = true
config.turbo.draw_routes = false
Rails.logger = config.logger = Logger.new($stdout)
routes.append do
post "/" => "application#create"
root to: "application#index"
end
end
class ApplicationController < ActionController::Base
include Rails.application.routes.url_helpers
class_attribute :template, default: DATA.read
def index
render inline: template, formats: :html
end
def create
redirect_to root_url(count: params[:count].to_i + 1)
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chrome, screen_size: [1400, 1400], options: { js_errors: true }
end
Capybara.configure do |config|
config.server = :webrick
config.default_normalize_ws = true
end
ENV["DATABASE_URL"] = "sqlite3::memory:"
ENV["RAILS_ENV"] ||= "test"
Rails.application.initialize!
require "rails/test_help"
class TurboSystemTest < ApplicationSystemTestCase
test "reproduces bug" do
visit root_path
scroll_to find_button("Morph")
assert_scroll_preserved do
click_button "Morph"
assert_text "Count 1"
end
end
def assert_scroll_preserved(&block)
assert_no_changes -> { evaluate_script("window.scrollY") }, &block
end
end
__END__
<html>
<head>
<script type="importmap">
{
"imports": {
"@hotwired/turbo-rails": "<%= asset_path("turbo.js") %>"
}
}
</script>
<script type="module">
import "@hotwired/turbo-rails"
</script>
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
</head>
<body>
<p style="margin-bottom: 100vh;">Count <%= params.fetch(:count, 0) %><p>
<%= button_to "Morph", root_path,
data: {turbo_action: "replace"},
params: {count: params[:count].to_i} %>
</body>
</html>Does that inconsistency need to be resolved? Should replace actions become more flexible by comparing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seanpdoyle The motivation behind using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think behavior should be consistent. I'm not sure I have enough practical experience to judge which side should change to match the other. If it's determined that inconsistency is the best option, then I think a documentation change (like the one proposed in this PR) will be necessary to clarify what is and is not going to result in a Page Refresh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jorgemanrubia should determining Visit Action based on the URL (as described in #178 (comment)) be consistent in both If it should, do you have an opinion on which implementation should change to be consistent with the other? |
||||||||||
| ``` | ||||||||||
|
|
||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm adding this suggestion because I got stuck on it for a while. I had a form with a GET action and I didn't understand why Turbo was using morphing to merge the response into the existing page data.
Suggested change
|
||||||||||
| ## Morphing | ||||||||||
|
|
||||||||||
| You can configure how Turbo handles page refresh with a `<meta name="turbo-refresh-method">` in the page's head. | ||||||||||
|
|
@@ -22,13 +34,13 @@ You can configure how Turbo handles page refresh with a `<meta name="turbo-refre | |||||||||
| </head> | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The possible values are `morph` or `replace` (the default). When it is `morph,` when a page refresh happens, instead of replacing the page's `<body>,` Turbo will only update the DOM elements that have changed, keeping the rest untouched. This approach delivers better sensations because it keeps the screen state. | ||||||||||
| The possible values are `morph` or `replace` (the default). When the `<meta>` element is omitted or its `content` attribute is `replace`, Turbo will [replace the page's `<body>` element](/handbook/drive#page-navigation-basics). When the `content` attribute is `morph`, Turbo will handle [page refreshes](#page-refreshes) by updating *only* the DOM elements that have changed. This approach delivers better sensations because it keeps the screen state like element focus. | ||||||||||
|
|
||||||||||
| Under the hood, Turbo uses the fantastic [idiomorph library](https://github.com/bigskysoftware/idiomorph). | ||||||||||
|
|
||||||||||
| ## Scroll preservation | ||||||||||
|
|
||||||||||
| You can configure how Turbo handles scrolling with a `<meta name="turbo-refresh-scroll">` in the page's head. | ||||||||||
| You can configure how Turbo handles scrolling when handling with a `<meta name="turbo-refresh-scroll">` in the page's head. | ||||||||||
|
|
||||||||||
| ```html | ||||||||||
| <head> | ||||||||||
|
|
@@ -37,7 +49,7 @@ You can configure how Turbo handles scrolling with a `<meta name="turbo-refresh- | |||||||||
| </head> | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The possible values are `preserve` or `reset` (the default). When it is `preserve`, when a page refresh happens, Turbo will keep the page's vertical and horizontal scroll. | ||||||||||
| The possible values are `preserve` or `reset` (the default). When the `<meta>` element is omitted or its `content` attribute is `reset`, Turbo will [reset the page's scroll position](/handbook/drive#application-visits). When the `content` attribute is `preserve`, Turbo will handle [page refreshes](#page-refreshes) by maintaining the page's vertical and horizontal scroll. | ||||||||||
|
|
||||||||||
| ## Exclude sections from morphing | ||||||||||
|
|
||||||||||
|
|
@@ -67,7 +79,7 @@ There is a new [turbo stream action](/handbook/streams.html) called `refresh` th | |||||||||
| <turbo-stream action="refresh"></turbo-stream> | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Server-side frameworks can leverage these streams to offer a simple but powerful broadcasting model: the server broadcasts a single general signal, and pages smoothly refresh with morphing. | ||||||||||
| Server-side frameworks can leverage these streams to offer a simple but powerful broadcasting model: the server broadcasts a single general signal, and pages smoothly refresh with morphing. | ||||||||||
|
|
||||||||||
| You can see how the [`turbo-rails`](https://github.com/hotwired/turbo-rails) gem does it for Rails: | ||||||||||
|
|
||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An important callout until hotwired/turbo#1316 and hotwired/turbo#1319 land.
I was stuck on this for hours!