Does Turbo 8 morphing make sense?
At Rails World I presented the Hotwire Cookbook - a collection of UI behaviours that you can achieve today with Turbo Drive, Frames, Streams, and Stimulus.
In the meantime Jorge Manrubia talked about new/future Hotwire/Turbo features. Mainly, about “Turbo Morphing”. It was introduced in this pull request.
What is “morphing”? #
Morphing = refresh current page with preserving the scroll position;
Full page refresh animation is skipped, because before the refresh happens, there is a “diff” of old VS new page, and only the diff gets updated.
Try morphing (Turbo Drive) #
Morphing will be released in Turbo 8, so currently the best way to try it together with all the other new features is to use the main
git branch:
# Gemfile
gem "turbo-rails", github: "hotwired/turbo-rails", branch: "main"
Enable turbo 8 morphing:
Old, default page refresh behaviour:
# app/views/layouts/application.html.erb
<head>
# <meta name="turbo-refresh-method" content="replace">
# <meta name="turbo-refresh-scroll" content="reset">
<%= turbo_refreshes_with method: :replace, scroll: :reset %>
<%= yield :head %>
</head>
New, add morphing to your app by adding this:
# app/views/layouts/application.html.erb
<head>
# <meta name="turbo-refresh-method" content="morph">
# <meta name="turbo-refresh-scroll" content="preserve">
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= yield :head %>
</head>
Now, whenever you redirect_to
current page, the scroll position will be preserved.
The easiest way to reproduce this behaviour is deleting a record from a list:
- without morphing, you would be redirected to the top of the page
- with morphing, you will keep scroll position, and the element will just disappear
This means you can use fewer TurboStreams in your app!
Do not morph-refresh some elements #
If an element should not be refreshed, add data-turbo-permanent
attribute.
Previously, data-turbo-permanent
required the presence of an [id] attribute. Now it does not!
For example: if you add data-turbo-permanent
to this <details>
dropdown, it will not close after a morph-refresh:
-<details>
+<details data-turbo-permanent>
<summary>Details</summary>
Something small enough to escape casual notice.
</details>
New turbo stream action - refresh
#
- Morph-refresh for current user, only current tab? =>
redirect_to
; - Morph-refresh for all users on a page? =>
TurboStreams
;
For this we have a new turbo stream redirect
action has been introduced:
<turbo-stream action="refresh"></turbo-stream>
Now you can trigger refreshes in a model, and send updates to all clients/browser tabs!
# app/models/project.rb
# For this to work out of the box, consider using very RESTful (scaffold-default) conventions.
broadcasts_refreshes
# same as:
after_create_commit -> { broadcast_refresh_later_to(model_name.plural) }
after_update_commit -> { broadcast_refresh_later }
after_destroy_commit -> { broadcast_refresh }
This would require having an open ActionCable/Websockets channel for this specific record:
# app/views/projects/index.html.erb
<% @projects.each do |project| %>
<%= turbo_stream_from project %>
<%= render project %>
<% end %>
or directly within project partial
# app/views/projects/_project.html.erb
<%= turbo_stream_from @project %>
I don’t like invoking view-related logic from a model.
Instead, we can trigger refreshes from from the console:
Turbo::StreamsChannel.broadcast_refresh_to @project
will refresh all pages that have the listener
<%= turbo_stream_from @project %>
Global refresh #
Refresh current_page(s) for all users in the app 🤪
# app/views/application.html.erb
Turbo::StreamsChannel.broadcast_refresh_to :global
<%= turbo_stream_from :global %>
Refresh all current page(s) for current_user
:
# app/views/application.html.erb
Turbo::StreamsChannel.broadcast_refresh_to current_user
<%= turbo_stream_from current_user %>
Debugging common problems #
After a page morph stimulus controllers can lose the default state. You can manually “re-connect” a stimulus controller:
<div data-action="turbo:morph@window->dropdown#connect">
You can also trigger a callback on a value change.
In the below scenario, a page morph would not affect elements inside a div, but we trigger some turbo
/form
behaviours manually:
<div data-turbo-permanent data-action="
turbo:submit-end->dropdown#close
turbo:submit-end->form-reset#connect">
<form ....>
Open questions: #
- Can morphing replace turbo frames search?
- Would it correctly refresh based on each users’ current query params?
- How would it work on a page with infinite pagination?
Resources:
Did you like this article? Did it save you some time?