hotwired / turbo-site

turbo.hotwired.dev web site
83 stars 101 forks source link

Describe criteria for link and form morphing #178

Open seanpdoyle opened 7 months ago

seanpdoyle commented 7 months ago

Expand upon the Page Refresh sections explaining how to morph and preserve scrolling.

Page Refreshes

A "page refresh" is a application visit with a "replace" action to a URL with a whose pathname matches the current URL path. Page refreshes can be initiated by driving the page with a link, or by redirecting after a form submission. In either case, the elements must have a [data-turbo-action="replace"] attribute:

<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>
seanpdoyle commented 7 months ago

This level of detail feels necessary, since there has been some surprise around links and forms not morphing.

Below is a self-contained Turbo Rails application that morphs with form submissions and link clicks. The system test covering scroll preservation passes. If the data: {turbo_action: "replace"} options are omitted, it fails:

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

$template = DATA.read

class ApplicationController < ActionController::Base
  include Rails.application.routes.url_helpers

  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

    assert_scroll_preserved do
      click_button "Morph"

      assert_text "Count 2"
    end

    assert_scroll_preserved do
      click_button "Morph"

      assert_text "Count 3"
    end

    assert_scroll_preserved do
      click_link "Morph"

      assert_text "Count 4"
    end

    assert_scroll_preserved do
      click_link "Morph"

      assert_text "Count 5"
    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>

    <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
    <%= yield :head %>
  </head>

  <body>
    <p style="margin-bottom: 100vh;">Count <%= params.fetch(:count, 0) %><p>

    <%= button_to "Morph", root_path, method: "post",
          data: {turbo_action: "replace"},
          params: {count: params[:count].to_i} %>

    <%= link_to "Morph", root_path(count: params[:count].to_i + 1),
          data: {turbo_action: "replace"} %>
  </body>
</html>