Skip to content

Commit

Permalink
Add [method] and [scroll] attributes for Refresh Stream
Browse files Browse the repository at this point in the history
> I have a page where I am using Turbo morph. when I submit a form and
> redirect I would like to reset the scroll, but if a refresh is triggered
> by a broadcast I would like to preserve teh scroll. Is that possible ?
>
> [#turbo Discord][discord]

This commit expands the set of attributes for `<turbo-stream
action="refresh">` to include `[method]` and `[scroll]` (in addition to
`[request-id]`). These attributes correspond directly to the
[`turbo-refresh`-prefixed `<meta>` element][meta] elements that control
morphing and scroll preservation.

When present on the `<turbo-stream action="refresh">`, their values are
forward along to the `Session.refresh` method call, which in turn
encodes them into Visit options under the `refresh` key. Those options
are then used during `Visit` instantiation, and transformed into
`.refresh` properties.

At render time, the `PageRenderer` attempts to read the refresh method
and scroll preservation settings with the following precedence:

1. read from the corresponding `Visit.refresh` property (possibly null)
2. read from the corresponding `<meta name="turbo-...">` element (possibly null)

If no value is provided, fallback to the default (`{ method: "replace",
scroll: "reset" }`).

[discord]: https://discord.com/channels/988103760050012160/1044659721229054033/1212443786270212167
[meta]: https://turbo.hotwired.dev/handbook/page_refreshes#morphing
  • Loading branch information
seanpdoyle committed Mar 29, 2024
1 parent 9fb05e3 commit 99d0755
Show file tree
Hide file tree
Showing 7 changed files with 42 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/core/drive/navigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class Navigator {
} else {
await this.view.renderPage(snapshot, false, true, this.currentVisit)
}
if(!snapshot.shouldPreserveScrollPosition) {
if (snapshot.refreshScroll !== "preserve") {
this.view.scrollToTop()
}
this.view.clearSnapshotCache()
Expand Down
8 changes: 4 additions & 4 deletions src/core/drive/page_snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ export class PageSnapshot extends Snapshot {
return this.headSnapshot.getMetaValue("view-transition") === "same-origin"
}

get shouldMorphPage() {
return this.getSetting("refresh-method") === "morph"
get refreshMethod() {
return this.getSetting("refresh-method")
}

get shouldPreserveScrollPosition() {
return this.getSetting("refresh-scroll") === "preserve"
get refreshScroll() {
return this.getSetting("refresh-scroll")
}

// Private
Expand Down
6 changes: 4 additions & 2 deletions src/core/drive/page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export class PageView extends View {
}

renderPage(snapshot, isPreview = false, willRender = true, visit) {
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage
const refreshMethod = visit?.refresh?.method || this.snapshot.refreshMethod
const shouldMorphPage = this.isPageRefresh(visit) && refreshMethod === "morph"
const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer

const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender)
Expand Down Expand Up @@ -60,7 +61,8 @@ export class PageView extends View {
}

shouldPreserveScrollPosition(visit) {
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
const refreshScroll = visit?.refresh?.scroll || this.snapshot.refreshScroll
return this.isPageRefresh(visit) && refreshScroll === "preserve"
}

get snapshot() {
Expand Down
7 changes: 5 additions & 2 deletions src/core/drive/visit.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const defaultOptions = {
willRender: true,
updateHistory: true,
shouldCacheSnapshot: true,
acceptsStreamResponse: false
acceptsStreamResponse: false,
refresh: {}
}

export const TimingMetric = {
Expand Down Expand Up @@ -72,7 +73,8 @@ export class Visit {
updateHistory,
shouldCacheSnapshot,
acceptsStreamResponse,
direction
direction,
refresh
} = {
...defaultOptions,
...options
Expand All @@ -92,6 +94,7 @@ export class Visit {
this.shouldCacheSnapshot = shouldCacheSnapshot
this.acceptsStreamResponse = acceptsStreamResponse
this.direction = direction || Direction[action]
this.refresh = refresh
}

get adapter() {
Expand Down
6 changes: 4 additions & 2 deletions src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@ export class Session {
}
}

refresh(url, requestId) {
refresh(url, options = {}) {
const { method, requestId, scroll } = options
const isRecentRequest = requestId && this.recentRequests.has(requestId)

if (!isRecentRequest) {
this.visit(url, { action: "replace", shouldCacheSnapshot: false })
this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } })
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/core/streams/stream_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export const StreamActions = {
},

refresh() {
session.refresh(this.baseURI, this.requestId)
const method = this.getAttribute("method")
const requestId = this.requestId
const scroll = this.getAttribute("scroll")

session.refresh(this.baseURI, { method, requestId, scroll })
},

morph() {
Expand Down
19 changes: 19 additions & 0 deletions src/tests/functional/page_refresh_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ test("uses morphing to update remote frames marked with refresh='morph'", async
await expect(page.locator("#refresh-reload")).toHaveText("Loaded reloadable frame")
})

test("overrides the meta value to render with replace when the Turbo Stream has [method=replace] attribute", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

await page.evaluate(() => document.body.insertAdjacentHTML("beforeend", `<turbo-stream action="refresh" method="replace"></turbo-stream>`))
await nextEventNamed(page, "turbo:render", { renderMethod: "replace" })
})

test("don't refresh frames contained in [data-turbo-permanent] elements", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

Expand Down Expand Up @@ -225,6 +232,18 @@ test("it preserves the scroll position when the turbo-refresh-scroll meta tag is
await assertPageScroll(page, 10, 10)
})

test("overrides the meta value to reset the scroll position when the Turbo Stream has [scroll=reset] attribute", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

await page.evaluate(() => window.scrollTo(10, 10))
await assertPageScroll(page, 10, 10)

await page.evaluate(() => document.body.insertAdjacentHTML("beforeend", `<turbo-stream action="refresh" scroll="reset"></turbo-stream>`))
await nextEventNamed(page, "turbo:render", { renderMethod: "morph" })

await assertPageScroll(page, 0, 0)
})

test("it does not preserve the scroll position on regular 'advance' navigations, despite of using a 'preserve' option", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

Expand Down

0 comments on commit 99d0755

Please sign in to comment.