Let T
be a turbo-frame
with data-turbo-action="advance"
attribute. When clicking a Turbo-enabled link targeting T
or redirecting after a form-submission within T
, T
gets updated from the response and Turbo triggers an application visit pushing a new entry onto the browser’s history stack. When the turbo-frame
in the response also contains turbo-stream
elements, other parts of the page the turbo-stream
s target can be updated as well. Something like this:
app/views/layouts/application.html.slim
html
...
body
...
/ Links targeting the turbo-frame with ID #frame-1.
= link_to 'Show resource', resource_path, data: { 'turbo-frame': 'frame-1', 'turbo-action': 'advance' }
= link_to 'New resource', new_resource_path, data: { 'turbo-frame': 'frame-1', 'turbo-action': 'advance' }
...
/ Renders a div element with ID #flash that's updated by a turbo-stream.
= render 'layouts/flash'
...
/ Renders the view of the controller. The view is wrapped in a turbo-frame with ID #frame-1.
= yield
...
app/views/resource/show.html.slim
/ Wrap the page in the turbo-frame with ID #frame-1.
= render 'layouts/turbo_frame'
...
app/views/resource/new.html.slim
/ Wrap the form in the turbo-frame with ID #frame-1.
= render 'layouts/turbo_frame'
= simple_form_for resource do |f|
...
app/views/layout/_turbo_frame.html.slim
= turbo_frame_tag 'frame-1', data: { 'turbo-action': 'advance' }
/ Render the view that is to be wrapped in this turbo-frame.
= yield
/ Update the div element with ID #flash with the flash message on Turbo requests targeting this turbo-frame.
- if turbo_frame_request_id == 'frame-1'
= turbo_stream.replace 'flash' do
= render 'layouts/flash', message: flash[:notice]
app/views/layouts/_flash.html.slim
#flash
- if defined? message
div(data-turbo-temporary) #{message}
app/controllers/resource_controller.rb
class ResourceController < ApplicationController
...
def create
...
redirect_to resource_path, status: :see_other
end
...
end
When such a link is clicked or the form is submitted, it appears that both the turbo-frame
and the turbo-stream
s in the response are processed before Turbo triggers the application visit and therefore before it emits the turbo:before-cache
event. This leads to an odd behavior when caching the current page before navigating to the new URL.
-
For the sake of the example, let’s assume that one of the
turbo-stream
s in the response targets an element where flash messages or alerts are displayed after a form got submitted as in the code snippet above. As pointed out in the Turbo documentation, such elements are inherently temporary, we don’t want to cache them. Fortunately, Turbo has a mechanism for such a use case and it automatically handles thedata-turbo-temporary
attribute. When this attribute is added to an element, Turbo is going to remove it from the document before it’s cached. Technically this should work but what happens, however, is that the stream updates the targeted element and renders the flash message, then Turbo initiates the application visit which caches the current page. At this point the flash message is already included in the DOM and flagged with thedata-turbo-temporary
attribute, so caching removes it from the page. At the end the flash message is not visible. -
Let’s try to
fix thiswork it around by not using thedata-turbo-temporary
attribute but, as per the documentation suggests, by listening for theturbo:before-cache
event such that the document can be prepared before Turbo caches it. After the form got submitted and the browser got redirected everything looks correct: the updated URL in the browser, the content of theturbo-frame
and all the other elements theturbo-stream
s targeted, including the flash message which is still visible. Due to the redirect an application visit occurs, Turbo caches the current page before nagivating to the new URL. Since we have an event handler registered on theturbo:before-cache
event, it gets called. But by the time it’s called, the document is already updated with the new content so the “current” page can’t be prepared for caching. Adocument.querySelector('some CSS selector')
method call in the event handler, for example, will find the newturbo-frame
content and the new elements updated by theturbo-stream
s, not the content that’s supposed be there before the HTTP response was received.Now click a Turbo-enabled link that targets the same
turbo-frame
. The same thing happens: this will again first update the frame and process the Turbo-streams then initiate an application visit and fire theturbo:before-cache
event. At this point, however, the DOM is already updated with the new content. If, for example, the previous form-submit rendered a flash message, it can’t be removed from within theturbo:before-cache
event handler because it’s not even there in case the response contained aturbo-stream
that changed the element which previously displayed the flash message. When the browser’s back button is clicked, however, we are taken to the previous page that is correctly restored from the cache (i.e. with the actual old content, including the flash message which was not present in the DOM inside theturbo:before-cache
event handler).
I am wondering whether I am doing something wrong or if the Turbo events happen in the wrong order? It appears to me that if the application visit and the caching happened before processing the response containing a turbo-frame
with advance action and with turbo-streams
within, everything would work correctly. On the other hand, in case Turbo is working as expected, I would like to know what it is that I need to do differently. Updating multiple parts of the page from a single response without a full page reload is not some odd edge case, neither is redirecting to a different URL (without a full page reload) after a form is submitted. I would expect Turbo to handle these but I can’t get it to work.
I have created a very simple app that sort of demonstrates the behavior described above. It has a sidebar and a main area with a turbo-frame
as well as a separate div
where flash messages are displayed by a turbo-stream
. The sidebar has two links targeting the turbo-frame
to load two different pages into it. The “Add key-value pairs” page displays a simple form inside a turbo-frame
where key-value pairs can be submitted to the server which saves them in the session for 5 seconds. After the form is submitted, the browser is redirected to the “Key-value pairs” page that displays the current key-value pairs in the session. Note that the page is not reloaded, only the turbo-frame
is updated with the content of the page the browser is redirected to. The response also contains a turbo-stream
that is supposed to render a flash message in the aforementioned div
element. In addition to the flash message, the active menu item in the siderbar is also updated by a turbo-stream
.
After submitting a form, a flash message is displayed. If one navigates to the other page from the sidebar then clicks the browser’s back button, the flash message “reappears” as it gets cached with the page (this is an undesired behavior). If one edits the views/layouts/_flash.html.slim file and replaces the
div #{message}
line with
div(data-turbo-temporary) #{message}
the flash message won’t be displayed at all (more specifically it gets displayed for a split second then removed as described above in case (1) - this is also an undesired behavior). In the views/layout/application.html.slim file there is an event handler registered on the turbo:before-cache
which logs to the console the content of the turbo-frame
and the div
that’s updated by a turbo-stream
. Based on these logs it looks like that when accessing the DOM from within the event handler, it is already updated from the response (as described above in case (2)), so the flash message can’t be removed programmatically either because it’s not present in the DOM when the event handler runs (yet another undesired behavior).
Any help / insight is appreciated. Turbo version is 7.3.0.