Skip to content

Export PageRenderer#305

Merged
dhh merged 1 commit into
hotwired:mainfrom
mrrooijen:export-PageRenderer
Jul 12, 2021
Merged

Export PageRenderer#305
dhh merged 1 commit into
hotwired:mainfrom
mrrooijen:export-PageRenderer

Conversation

@mrrooijen

Copy link
Copy Markdown
Contributor

There are situations where you don't want the entire body to be replaced. One of the reasons would be when you're using 3rd party libraries that inject code (i.e. an iframe) such as Intercom or HelpScout, as this would result in the removal- and state-loss of their UI component every time you navigate using turbo drive.

Looking through previous issues it seems that there's no desire to add the ability to select a different node to render turbo drive responses into. So instead, it would be nice to be able to monkey-patch this functionality in so we can opt-out of this constraint. However, in order to do this we'll need access to PageRenderer, which currently isn't exported. This PR exports it.

@dhh dhh merged commit a84d25d into hotwired:main Jul 12, 2021
@dhh

dhh commented Jul 12, 2021

Copy link
Copy Markdown
Member

Please do look at turning that monkey patch into more direct flexibility. @domchristie was just mentioning this in #290.

@michelson

michelson commented Jul 13, 2021

Copy link
Copy Markdown
Contributor

FYI, I've tackled this issue in chaskiq.io (intercom-like embed) by wrapping the embed div in a turbo-permanent. That will persist their state between navigation.

@mrrooijen mrrooijen deleted the export-PageRenderer branch July 13, 2021 09:49
@mrrooijen

mrrooijen commented Jul 13, 2021

Copy link
Copy Markdown
Contributor Author

@dhh Thanks for the merge!

I've already tried wrapping the node with data-turbo-permanent but it breaks the underlying iframe post-navigation/render. This solution may work (such as in @michelson's case) with various services, but not with others. It seems this is unfortunately beyond our control.

I came across the following issues that describe the problem (or similar/related problems):

Being able to intercept the rendering operation as described in #290 might be a viable solution to the problem if it'd allow you to switch the rendering target from <body> to a different node, assuming that there isn't a better solution that avoids the necessity to render into a different node entirely.

@dhh just an idea, but perhaps being able to specify a different rendering target by simply adding an optional attribute (i.e. data-turbo-drive-body) would be a minimal and straightforward way to switch targets.

Example Layout HTML diff

- <body>
-   <%= yield %> 
-   <!-- code from various services such as Stripe, HelpScout, is often injected here -->
- </body>
+ <body>
+   <div data-turbo-drive-body>
+     <%= yield %>  
+   </div>
+   <!-- code from various services such as Stripe, HelpScout, is often injected here -->
+ </body>

Example PageRenderer diff

- assignNewBody() {
-   if (document.body && this.newElement instanceof HTMLBodyElement) {
-     document.body.replaceWith(this.newElement)
-   } else {
-     document.documentElement.appendChild(this.newElement)
-   }
- }
+ assignNewBody() {
+   if (this.turboBody && this.newTurboElement) {
+     this.turboBody.replaceWith(this.newTurboElement)
+   } else if (document.body && this.newElement instanceof HTMLBodyElement) {
+     document.body.replaceWith(this.newElement)
+   } else {
+     document.documentElement.appendChild(this.newElement)
+   }
+ }
+ 
+ get turboBody() {
+   return document.querySelector("[data-turbo-drive-body]")
+ }
+ 
+ get newTurboElement() {
+   return this.newElement.querySelector("[data-turbo-drive-body]")
+ }

I haven't tried the above yet, but this is essentially what I had in mind. The default behavior stays the same (render into <body>), and if there's an element with data-turbo-drive-body, then render into that element instead.

@tbo tbo mentioned this pull request Jul 13, 2021
@dhh

dhh commented Jul 13, 2021

Copy link
Copy Markdown
Member

@mrrooijen Can you explain more the issue you're trying to solve? Why isn't data-turbo-permanent working for you? What's breaking and could we fix it?

It is an interesting idea to basically invert data-turbo-permanent, such that everything NOT marked is permanent. But I'm not sure why we're able to make that work but not specifically marking something as data-turbo-permanent? Is it about detaching and attaching the nodes that trigger something?

@mrrooijen

mrrooijen commented Jul 13, 2021

Copy link
Copy Markdown
Contributor Author

@dhh correct. When you detach an element containing an iframe, and then re-attach it, it'll cause the iframe to reload. This behavior is intended and there's no way around it as far as I know. See: https://bugs.webkit.org/show_bug.cgi?id=13574#c14

There apparently used to be a "magic iframe" that could work around this issue, but it's been removed due to security implications.

Third party nodes and iframes (Stripe, HelpScout, etc) are commonly injected just before the closing </body> tag, so replacing the entire body and then re-attaching the nodes containing the iframes will result in an undesirable reload- and thus breakage and/or the loss of state.

Therefore, the only way that I currently know of that resolves this issue is to, rather than render your content into body, instead render it into a child node such as body > div[data-turbo-drive-body]. This way Turbo Drive never touches anything inside of body other than div[data-turbo-drive-body], so the third party nodes and iframes that reside inside body but outside of div[data-turbo-drive-body] are left alone and correctly persist when navigating.

It is an interesting idea to basically invert data-turbo-permanent, such that everything NOT marked is permanent. But I'm not sure why we're able to make that work but not specifically marking something as data-turbo-permanent? Is it about detaching and attaching the nodes that trigger something?

Just to clarify, the solution that I proposed (the diffs) doesn't involve the use of data-turbo-permanent. Rather, it avoids it and turbo drive's rendering process entirely. It's not doing any inverted operations or anything complicated, really. It merely moves the rendering of the application's content from body into a child node (i.e. body > div[data-turbo-drive-body]).

@domchristie

domchristie commented Jul 13, 2021

Copy link
Copy Markdown
Contributor

I don't have a deep knowledge of the rendering process, but my feeling is that it won't be straightforward to just change the target render element, and that this might cause future maintenance issues. (It could become a reimplementation of Turbo frames :/)

Given the uncertainty around how this might be used, my preference would be for a "canary" API using the pausable render feature. For example something like:

addEventListener('turbo:before-render', function (event) {
  event.preventDefault()
  document.getElementById('root').replaceWith(event.detail.newBody.getElementById('root'))
  event.detail.resume({ skipSnapshotRendering: true })
})

@internets

Copy link
Copy Markdown

Our use case will require something similar. We are embedding Twilio Flex inside of an iframe in our CRM. The iframe reload during the visit causes state loss and breaks WebRTC connections which results in dropped calls.

I made a fork that did exactly what @mrrooijen suggested with data-turbo-drive-body, and had a problem with restoration visits. We haven't had time to look deeper, but we will soon. I'm happy to chip in and/or share anything that I learn along the way.

Something like what @domchristie suggests would definitely work for our use case.

@domchristie

Copy link
Copy Markdown
Contributor

API using the pausable render feature.
#305 (comment)

I have spiked an idea for this here: main...domchristie:custom_rendering Not tried it, but might be worth testing it out.

@internets

Copy link
Copy Markdown

Thanks @domchristie! Your spike does allow rendering to a child element instead of body without monkey patching PageRenderer. Unfortunately you lose the permanent element preservation and script activation that are handled by PageRenderer. For this particular use case, being able to configure the function that assigns the new body would be preferable:

addEventListener('turbo:before-render', function (event) {
  event.preventDefault()
  event.detail.resume({ 
    assignNewBody: () => {
      document.getElementById('root').replaceWith(event.detail.newBody.querySelector('#root'))
    } 
  })
})

I'm not sure if that's much better than monkey patching PageRenderer, though. Another option would be to extract those features from PageRenderer and allow people to use them in their custom renderers. That allows a high amount of flexibility without sacrificing some of turbo's great features in the process.

Either way the problem with restoration visits still exists, but it looks like @tbo already discovered the cause of that. See #218 (comment) re: snapshotting mechanism. I would love to see that one line merged, it would allow us to switch off our fork.

@domchristie

Copy link
Copy Markdown
Contributor

Unfortunately you lose the permanent element preservation and script activation that are handled by PageRenderer.

I suppose that if you were using a DOM diffing library like morphdom then you'd likely handle permanent elements yourself? Perhaps there's a way to implement both e.g. event.detail.resume({ skipBodyAssignment: true }) and event.detail.resume({ skipSnapshotRendering: true }).

@internets

Copy link
Copy Markdown

I'm not a morphdom expert, but I imagine there may be cases where you have some state that you would like to keep even though the server is returning a new body that contains some default state. I don't know if morphdom has its own way to express that.

skipBodyAssignment: true almost gets there, but I believe the mutation has to happen in the callback passed to PageRenderer.preservingPermanentElements for it to work, so we can't mutate and then continue the render skipping body assignment.

Letting people provide their own PageRenderer is a potential solution. Instead of letting people monkey-patch it, you could write your own or extend the one that Turbo exports. Example:

import { Turbo, PageRenderer } from '@hotwired/turbo';

class CustomPageRenderer extends PageRenderer {
  assignNewBody() {
    document.getElementById('root').replaceWith(this.newElement.querySelector('#root'))
  }
}

Turbo.setRenderer(CustomPageRenderer)

@domchristie

Copy link
Copy Markdown
Contributor

I'm not a morphdom expert, but I imagine there may be cases where you have some state that you would like to keep even though the server is returning a new body that contains some default state. I don't know if morphdom has its own way to express that.

I think this is possible with the onBeforeElUpdated hook:

addEventListener('turbo:before-render', async function (event) {
  event.preventDefault()
  morphdom(document.body, event.detail.newBody, {
    onBeforeElUpdated: function (fromEl, toEl) {
      const permanent = (
        fromEl.dataset.turboPermanent &&
        toEl.dataset.turboPermanent &&
        fromEl.id === toEl.id
      )
      if (permanent) return false
      return true
    }
  })
  event.detail.resume({ skipSnapshotRendering: true })
})

Letting people provide their own PageRenderer is a potential solution. … Turbo.setRenderer(CustomPageRenderer)

I think I'm generally against this approach, as the entire PageRenderer becomes public API, which then puts the burden of maintaining all of the unconventional things that could be done with it, onto the maintainers. Should PageRenderer ever need to change, it'd be breaking one :/

@internets

Copy link
Copy Markdown

I'm not a morphdom expert, but I imagine there may be cases where you have some state that you would like to keep even though the server is returning a new body that contains some default state. I don't know if morphdom has its own way to express that.

I think this is possible with the onBeforeElUpdated hook:

That looks straightforward to me. I like the idea. Far better than making PageRenderer public API :)

For our use case extra weight of morphdom isn't an issue, I'll give it a go with your branch and see if I can get script activation working as well.

Thanks!

@internets

Copy link
Copy Markdown

Not tested extensively, but what you had there more or less worked, and adding Turbo-compatible script activation was easy as well. In case anybody is curious:

function copyElementAttributes(destinationElement, sourceElement) {
  for (const { name, value } of [ ...sourceElement.attributes ]) {
    destinationElement.setAttribute(name, value)
  }
}

function createScriptElement(element) {
  if (element.getAttribute("data-turbo-eval") == "false") {
    return element
  } else {
    const createdScriptElement = document.createElement("script")
    const cspNonce = document.head.querySelector('meta[name="csp-nonce"]')?.getAttribute("content")
    if (cspNonce) {
      createdScriptElement.nonce = cspNonce
    }
    createdScriptElement.textContent = element.textContent
    createdScriptElement.async = false
    copyElementAttributes(createdScriptElement, element)
    return createdScriptElement
  }
}

addEventListener('turbo:before-render', async function (event) {
  event.preventDefault()
  morphdom(document.body, event.detail.newBody, {
    onNodeAdded: function (node) {
      if (node.nodeName === 'SCRIPT') {
        const activatedScriptElement = createScriptElement(node)
        node.replaceWith(activatedScriptElement)
      }
    },
    onBeforeElUpdated: function (fromEl, toEl) {
      const permanent = (
        fromEl.dataset.turboPermanent !== undefined &&
        toEl.dataset.turboPermanent !== undefined &&
        fromEl.id === toEl.id
      )
      if (permanent) return false

      if (fromEl.nodeName === "SCRIPT" && toEl.nodeName === "SCRIPT") {
        const activatedScriptElement = createScriptElement(toEl)
        fromEl.replaceWith(activatedScriptElement)

        return false;
      }

      return true
    }
  })
  event.detail.resume({ skipSnapshotRendering: true })
})

While adopting morphdom and copying some turbo functionality can work for us, I can see value in also having an official Turbo solution using @mrrooijen's suggestion of specifying a target for body assignment with something like data-turbo-drive-body. It allows people with iframes that can't be reloaded to use Turbo without adding additional libraries and duplicating Turbo functionality, and despite increasing the API surface of Turbo it is a small change code-wise (once mutating renders are supported).

In other words, I like both skipSnapshotRendering as a way to allow custom rendering and data-turbo-drive-body as a way to work around iframe issues.

@mrrooijen

Copy link
Copy Markdown
Contributor Author

With PageRenderer now being exportable, the following works for me.

import { PageRenderer } from "@hotwired/turbo"

PageRenderer.prototype.assignNewBody = function() {
  const body = document.querySelector("[data-turbo-drive-body]")
  const el   = this.newElement.querySelector("[data-turbo-drive-body]")

  if (body && el) {
    body.replaceWith(el)
  } else if (document.body && this.newElement instanceof HTMLBodyElement) {
    document.body.replaceWith(this.newElement)
  } else {
    document.documentElement.appendChild(this.newElement)
  }
}
<head>
  <meta content="no-cache" name="turbo-cache-control" />
</head>
<body>
  <div data-turbo-drive-body>
    <%= yield %>
  </div>
</body>

Page restorations appear to work for me because I'm not caching pages. Otherwise, as @internets pointed out, it will break.

@internets

Copy link
Copy Markdown

Page restorations appear to work for me because I'm not caching pages. Otherwise, as @internets pointed out, it will break.

If you'd like to continue caching, you can eliminate the pause before cloning the snapshot. Here's the monkey patch:

Turbo.navigator.view.cacheSnapshot = function() {
  if (this.shouldCacheSnapshot) {
    this.delegate.viewWillCacheSnapshot()
    const { snapshot, lastRenderedLocation: location } = this
    // await nextEventLoopTick()
    this.snapshotCache.put(location, snapshot.clone())
  }
}

I'm not 100% sure yet, but I believe the await nextEventLoopTick()is a performance optimization. The test suite passes with that modification, so it should be mostly safe :)

@rawberg

rawberg commented Aug 28, 2021

Copy link
Copy Markdown

Thanks for exporting this, this works great!

import morphdom from "morphdom";
import { PageRenderer } from "@hotwired/turbo";

PageRenderer.prototype.assignNewBody = function() {
    if (document.body && this.newElement instanceof HTMLBodyElement) {
        morphdom(document.body, this.newElement, {
            onBeforeElUpdated: function(fromEl, toEl) {
                return !fromEl.isEqualNode(toEl);
            }
        });
    } else {
        document.documentElement.appendChild(this.newElement)
    }
}

@agrobbin

agrobbin commented Jul 13, 2022

Copy link
Copy Markdown
Contributor

For those coming across this issue, who might be looking for a more limited patch that still allows for caching to function as expected, I went about it by actually adding await nextEventLoopTick() to PageRenderer#assignNewBody():

// copied since `util` is not exported
const nextEventLoopTick = () => new Promise((resolve) => {
  setTimeout(() => resolve(), 0);
});

const findBodyElement = (body) => body.querySelector('[data-turbo-drive-body]') || body;

PageRenderer.prototype.assignNewBody = async function assignNewBody() {
  await nextEventLoopTick();

  if (document.body && this.newElement instanceof HTMLBodyElement) {
    const currentBody = findBodyElement(document.body);
    const newBody = findBodyElement(this.newElement);

    currentBody.replaceWith(newBody);
  } else {
    document.documentElement.appendChild(this.newElement);
  }
};

The order of execution without adding await nextEventLoopTick() to PageRenderer#assignNewBody() seems to be:

  1. PageView#cacheSnapshot() before await nextEventLoopTick()
  2. PageRenderer#assignNewBody()
  3. PageView#cacheSnapshot() after await nextEventLoopTick()

By adding await nextEventLoopTick() to PageRenderer#assignNewBody(), (2) and (3) get flipped. This way, PageRenderer#assignNewBody() waits until PageView#cacheSnapshot() has finished, before replacing part of document.body.

I believe the reason this isn't an issue if you replace all of document.body is that the old HTMLBodyElement has not been manipulated, so the out-of-order execution doesn't have any practical implications on what is cloned. Since the HTMLBodyElement is not different, the out-of-order execution has a clear impact.

agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 14, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 14, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 14, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 14, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 16, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 16, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 16, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 19, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Jul 19, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Aug 12, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Aug 12, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Sep 9, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
agrobbin added a commit to agrobbin/turbo that referenced this pull request Sep 10, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
dhh pushed a commit that referenced this pull request Sep 13, 2022
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in #305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
@agrobbin agrobbin mentioned this pull request Sep 13, 2022
@scottnicolson

Copy link
Copy Markdown

For anyone that takes inspiration from #305 (comment) please be aware that the code provided fixes the cache visits issue, but for non-cache visits, if the new body and old body contain data-turbo-permanent elements, they do not get reattached. The solution seems to work fine when then body doesn't contain turbo-permanent elements, so just use with care until a proper solution is available.

@manuelpuyol

Copy link
Copy Markdown
Contributor

@agrobbin @scottnicolson I'd like to point that implementing #305 (comment) causes turbo:load to be fired before we actually replace the element, so this may cause some unwanted behaviors

@manuelpuyol

Copy link
Copy Markdown
Contributor
-    static preservingPermanentElements(delegate, permanentElementMap, callback) {
+    static async preservingPermanentElements(delegate, permanentElementMap, callback) {
         const bardo = new this(delegate, permanentElementMap);
         bardo.enter();
-        callback();
+        await callback();
         bardo.leave();
     }
     enter() {
@@ -1225,8 +1225,8 @@ class Renderer {
             delete this.resolvingFunctions;
         }
     }
-    preservingPermanentElements(callback) {
-        Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
+    async preservingPermanentElements(callback) {
+        await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
     }
     focusFirstAutofocusableElement() {
         const element = this.connectedSnapshot.firstAutofocusableElement;
@@ -2582,7 +2582,7 @@ class PageRenderer extends Renderer {
     }
     async render() {
         if (this.willRender) {
-            this.replaceBody();
+            await this.replaceBody();
         }
     }
     finishRendering() {
@@ -2607,10 +2607,10 @@ class PageRenderer extends Renderer {
         this.copyNewHeadProvisionalElements();
         await newStylesheetElements;
     }
-    replaceBody() {
-        this.preservingPermanentElements(() => {
+    async replaceBody() {
+        await this.preservingPermanentElements(async () => {
             this.activateNewBody();
-            this.assignNewBody();
+            await this.assignNewBody();
         });
     }
     get trackedElementsAreIdentical() {
@@ -2649,8 +2649,8 @@ class PageRenderer extends Renderer {
             inertScriptElement.replaceWith(activatedScriptElement);
         }
     }
-    assignNewBody() {
-        this.renderElement(this.currentElement, this.newElement);
+    async assignNewBody() {
+        await this.renderElement(this.currentElement, this.newElement);
     }

I think these are the minimal changes to make turbo:load correctly fire after the custom render actually replaces the elements

Challenge-Guy pushed a commit to Challenge-Guy/turbo-cfm1 that referenced this pull request Mar 8, 2025
While most of the time, replacing `<body>` makes perfect sense, when working with 3rd party integrations (i.e. Stripe), there are elements injected just before the closing `</body>` tag that should not be
removed between page visits. There is more detail on this issue, particularly with injected `<iframe>` elements in hotwired/turbo#305 (comment).

Now, if someone wants to customize the element that is replaced by Drive, they can add `data-turbo-drive-body` to an element, and only that element will be replaced between visits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

9 participants