Skip to content

Commit

Permalink
allow customizing the element that Drive replaces
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
agrobbin committed Jul 19, 2022
1 parent 975054b commit 0e5191d
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 3 deletions.
26 changes: 23 additions & 3 deletions src/core/drive/page_renderer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Renderer } from "../renderer"
import { PageSnapshot } from "./page_snapshot"
import { ReloadReason } from "../native/browser_adapter"
import { nextEventLoopTick, getBodyElementId } from "../../util"

export class PageRenderer extends Renderer<HTMLBodyElement, PageSnapshot> {
static renderElement(currentElement: HTMLBodyElement, newElement: HTMLBodyElement) {
static async renderElement(currentElement: HTMLBodyElement, newElement: HTMLBodyElement) {
await nextEventLoopTick()

if (document.body && newElement instanceof HTMLBodyElement) {
document.body.replaceWith(newElement)
const bodyElementId = getBodyElementId()

const currentBody = (bodyElementId && document.querySelector(`#${bodyElementId}`)) || document.body
const newBody = (bodyElementId && newElement.querySelector(`#${bodyElementId}`)) || newElement

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

get shouldRender() {
return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical
return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical && this.bodyElementMatches
}

get reloadReason(): ReloadReason {
Expand All @@ -27,6 +35,12 @@ export class PageRenderer extends Renderer<HTMLBodyElement, PageSnapshot> {
reason: "tracked_element_mismatch",
}
}

if (!this.bodyElementMatches) {
return {
reason: "body_element_mismatch",
}
}
}

prepareToRender() {
Expand Down Expand Up @@ -76,6 +90,12 @@ export class PageRenderer extends Renderer<HTMLBodyElement, PageSnapshot> {
return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature
}

get bodyElementMatches() {
const elementId = getBodyElementId()

return !elementId || this.newElement.querySelector(`#${elementId}`) !== null
}

copyNewHeadStylesheetElements() {
for (const element of this.newHeadStylesheetElements) {
document.head.appendChild(element)
Expand Down
21 changes: 21 additions & 0 deletions src/tests/fixtures/drive_custom_body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="turbo-body" content="app">
<title>Drive (with custom body)</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
</head>
<body>
<h1>Drive (with custom body)</h1>

<div id="app">
<div>
<a id="drive" href="/src/tests/fixtures/drive_custom_body_2.html">Drive enabled link</a>
</div>

<p id="different-content">Drive 1</p>
</div>
</body>
</html>
21 changes: 21 additions & 0 deletions src/tests/fixtures/drive_custom_body_2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="turbo-body" content="app">
<title>Drive (with custom body)</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
</head>
<body>
<h1>Drive (with custom body 2)</h1>

<div id="app">
<div>
<a id="drive" href="/src/tests/fixtures/drive_custom_body.html">Drive enabled link</a>
</div>

<p id="different-content">Drive 2</p>
</div>
</body>
</html>
21 changes: 21 additions & 0 deletions src/tests/functional/drive_custom_body_tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBody, pathname } from "../helpers/page"

const path = "/src/tests/fixtures/drive_custom_body.html"

test.beforeEach(async ({ page }) => {
await page.goto(path)
})

test("test drive with a custom body element", async ({ page }) => {
page.click("#drive")
await nextBody(page)

const h1 = await page.locator("h1")
const differentContent = await page.locator("#different-content")

assert.equal(pathname(page.url()), "/src/tests/fixtures/drive_custom_body_2.html")
assert.equal(await h1.textContent(), "Drive (with custom body)")
assert.equal(await differentContent.textContent(), "Drive 2")
})
4 changes: 4 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export function clearBusyState(...elements: Element[]) {
}
}

export function getBodyElementId(): string | null {
return getMetaContent("turbo-body")
}

export function getMetaElement(name: string): HTMLMetaElement | null {
return document.querySelector(`meta[name="${name}"]`)
}
Expand Down

0 comments on commit 0e5191d

Please sign in to comment.