Skip to content

Commit

Permalink
Introduce turbo:before-permanent-element-render
Browse files Browse the repository at this point in the history
While transposing a [permanent element][] from page to page, dispatch a
`turbo:before-permanent-element-render` event on the document with a
`(currentPermanentElement: Element, newPermanentElement: Element) =>
void` available as the event's `detail.render` property.

The `turbo:before-permanent-element-render` is named to follow the
pattern established by its predecessors (`turbo:before-render`,
`turbo:before-frame-render`, `turbo:before-stream-render`), and its
`CustomEvent.detail.render` property name mirrors the
`turbo:before-render` event's `CustomEvent.detail.render` property.

This opportunity to modify a permanent element might be useful if it's
possible for the preserved element to become partially stale. For
example, consider an invalid `<form>` submission with an `<input
type="file">`.

Since the server's response cannot fully encode the submitted file into
the response's element, it might be a useful to mark it with
`[data-turbo-permanent]` to preserve whatever client-side state precedes
the submission:

```html
<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent>
```

Suppose the uploaded file is too large, or doesn't match the server's
expectations for file type. The server's response might contain
a fragment like:

```html
<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent
       class="invalid" aria-describedby="profile_image_error">
<p id="profile_image_error">Profile image is too large (5GB). Try a smaller file (<5KB)</p>
```

Prior to this change, the fact that the `<input>` is marked with
`[data-turbo-permanent]` would ignore the `[class]` and
`[aria-describedby]` attributes from the response, and the `<input>`
would remain unchanged (with the attached file still in-memory).

With a new `turbo:before-permanent-element-render` available, there's an
opportunity to do application-specific merging:

```js
addEventListener("turbo:before-permanent-element-render", ({ target, { detail } }) => {
  detail.render = (currentElement, newElement) => {
    currentElement.setAttribute("class", newElement.getAttribute("class"))
    currentElement.setAttribute("aria-describedby", newElement.getAttribute("aria-describedby"))
  }
})
```

[permanent element]: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads
  • Loading branch information
seanpdoyle committed Sep 14, 2023
1 parent 191c085 commit a7b66e5
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 5 deletions.
12 changes: 9 additions & 3 deletions src/core/bardo.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { dispatch } from "../util"

export class Bardo {
static async preservingPermanentElements(delegate, permanentElementMap, callback) {
const bardo = new this(delegate, permanentElementMap)
Expand All @@ -21,8 +23,12 @@ export class Bardo {

leave() {
for (const id in this.permanentElementMap) {
const [currentPermanentElement] = this.permanentElementMap[id]
this.replaceCurrentPermanentElementWithClone(currentPermanentElement)
const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id]
const event = dispatch("turbo:before-permanent-element-render", {
detail: { render: this.replaceCurrentPermanentElementWithClone }
})

event.detail.render(currentPermanentElement, newPermanentElement)
this.replacePlaceholderWithPermanentElement(currentPermanentElement)
this.delegate.leavingBardo(currentPermanentElement)
}
Expand All @@ -33,7 +39,7 @@ export class Bardo {
permanentElement.replaceWith(placeholder)
}

replaceCurrentPermanentElementWithClone(permanentElement) {
replaceCurrentPermanentElementWithClone(permanentElement, _newElement) {
const clone = permanentElement.cloneNode(true)
permanentElement.replaceWith(clone)
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/permanent_element.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<section>
<h1>Permanent element</h1>
</section>
<div id="permanent" data-turbo-permanent>Permanent element</div>
<div id="permanent" class="loaded" data-turbo-permanent>Permanent element</div>

<turbo-frame id="frame">
<div id="permanent-in-frame" data-turbo-permanent>Permanent element</div>
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/rendering.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html data-skip-event-details="turbo:before-permanent-element-render">
<head>
<meta charset="utf-8">
<title>Turbo</title>
Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"turbo:before-stream-render",
"turbo:before-cache",
"turbo:before-render",
"turbo:before-permanent-element-render",
"turbo:before-visit",
"turbo:load",
"turbo:render",
Expand Down
24 changes: 24 additions & 0 deletions src/tests/functional/rendering_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,39 @@ test("test does not evaluate data-turbo-eval=false scripts", async ({ page }) =>
test("test preserves permanent elements", async ({ page }) => {
const permanentElement = await page.locator("#permanent")
assert.equal(await permanentElement.textContent(), "Rendering")
assert.equal(await permanentElement.getAttribute("class"), null)

await page.click("#permanent-element-link")
await nextEventNamed(page, "turbo:render")
assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent")))
assert.equal(await permanentElement.textContent(), "Rendering")
assert.equal(await permanentElement.getAttribute("class"), null)

await page.goBack()
await nextEventNamed(page, "turbo:render")
assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent")))
assert.equal(await permanentElement.getAttribute("class"), null)
})

test("test dispatches a turbo:before-permanent-element-render event while preserving permanent elements", async ({ page }) => {
await page.evaluate(() => {
addEventListener("turbo:before-permanent-element-render", (event) => {
event.detail.render = (currentElement, newElement) => {
currentElement.setAttribute("class", newElement.classList.toString())
}
})
})

const permanentElement = await page.locator("#permanent")

assert.equal(await page.textContent("#permanent"), "Rendering")
assert.equal(await permanentElement.getAttribute("class"), null)

await page.click("#permanent-element-link")
await nextEventNamed(page, "turbo:before-permanent-element-render")

assert.equal(await page.textContent("#permanent"), "Rendering")
assert.equal(await permanentElement.getAttribute("class"), "loaded")
})

test("test restores focus during page rendering when transposing the activeElement", async ({ page }) => {
Expand Down

0 comments on commit a7b66e5

Please sign in to comment.