Turbo events in incorrect order?

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-streams 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-streams 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.

  1. For the sake of the example, let’s assume that one of the turbo-streams 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 the data-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 the data-turbo-temporary attribute, so caching removes it from the page. At the end the flash message is not visible.

  2. Let’s try to fix this work it around by not using the data-turbo-temporary attribute but, as per the documentation suggests, by listening for the turbo: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 the turbo-frame and all the other elements the turbo-streams 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 the turbo: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. A document.querySelector('some CSS selector') method call in the event handler, for example, will find the new turbo-frame content and the new elements updated by the turbo-streams, 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 the turbo: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 the turbo:before-cache event handler because it’s not even there in case the response contained a turbo-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 the turbo: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.