diff --git a/src/core/drive/morphing_page_renderer.js b/src/core/drive/morphing_page_renderer.js index 2884a24fb..fbdf5bc7f 100644 --- a/src/core/drive/morphing_page_renderer.js +++ b/src/core/drive/morphing_page_renderer.js @@ -1,5 +1,4 @@ import { FrameElement } from "../../elements/frame_element" -import { MorphingFrameRenderer } from "../frames/morphing_frame_renderer" import { PageRenderer } from "./page_renderer" import { dispatch } from "../../util" import { morphElements } from "../morphing" @@ -13,7 +12,7 @@ export class MorphingPageRenderer extends PageRenderer { }) for (const frame of currentElement.querySelectorAll("turbo-frame")) { - if (canRefreshFrame(frame)) refreshFrame(frame) + if (canRefreshFrame(frame)) frame.reload() } dispatch("turbo:morph", { detail: { currentElement, newElement } }) @@ -38,11 +37,3 @@ function canRefreshFrame(frame) { frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]") } - -function refreshFrame(frame) { - frame.addEventListener("turbo:before-frame-render", ({ detail }) => { - detail.render = MorphingFrameRenderer.renderElement - }, { once: true }) - - frame.reload() -} diff --git a/src/core/frames/frame_controller.js b/src/core/frames/frame_controller.js index 07764fc86..f07d58859 100644 --- a/src/core/frames/frame_controller.js +++ b/src/core/frames/frame_controller.js @@ -20,6 +20,7 @@ import { FrameView } from "./frame_view" import { LinkInterceptor } from "./link_interceptor" import { FormLinkClickObserver } from "../../observers/form_link_click_observer" import { FrameRenderer } from "./frame_renderer" +import { MorphingFrameRenderer } from "./morphing_frame_renderer" import { session } from "../index" import { StreamMessage } from "../streams/stream_message" import { PageSnapshot } from "../drive/page_snapshot" @@ -89,6 +90,12 @@ export class FrameController { } sourceURLReloaded() { + if (this.element.shouldReloadWithMorph) { + this.element.addEventListener("turbo:before-frame-render", ({ detail }) => { + detail.render = MorphingFrameRenderer.renderElement + }, { once: true }) + } + const { src } = this.element this.element.removeAttribute("complete") this.element.src = null @@ -256,6 +263,7 @@ export class FrameController { detail: { newFrame, ...options }, cancelable: true }) + const { defaultPrevented, detail: { render } @@ -300,6 +308,7 @@ export class FrameController { if (newFrameElement) { const snapshot = new Snapshot(newFrameElement) const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false) + if (this.view.renderPromise) await this.view.renderPromise this.changeHistory() diff --git a/src/elements/frame_element.js b/src/elements/frame_element.js index 8dc2890f3..bbf5a24c8 100644 --- a/src/elements/frame_element.js +++ b/src/elements/frame_element.js @@ -91,6 +91,10 @@ export class FrameElement extends HTMLElement { } } + get shouldReloadWithMorph() { + return this.src && this.refresh === "morph" + } + /** * Determines if the element is loading */ diff --git a/src/tests/fixtures/frames.html b/src/tests/fixtures/frames.html index 0513e911a..e505852de 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -11,6 +11,12 @@ target.closest("turbo-frame")?.setAttribute("data-turbo-action", "advance") } else if (target.id == "remove-target-from-hello") { document.getElementById("hello").removeAttribute("target") + } else if (target.id == "add-refresh-reload-to-frame") { + target.closest("turbo-frame")?.setAttribute("refresh", "reload") + } else if (target.id == "add-refresh-morph-to-frame") { + target.closest("turbo-frame")?.setAttribute("refresh", "morph") + } else if (target.id == "add-src-to-frame") { + target.closest("turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") } }) @@ -28,6 +34,9 @@

Frames: #frame

+ + + Navigate #frame from within Navigate #frame with ?key=value Navigate #frame from within with a[data-turbo-action="advance"] @@ -60,7 +69,7 @@

Frames: #frame

Frames: #hello

- Load #frame + Load #frame
diff --git a/src/tests/functional/frame_tests.js b/src/tests/functional/frame_tests.js index 30381a52c..11d327cb9 100644 --- a/src/tests/functional/frame_tests.js +++ b/src/tests/functional/frame_tests.js @@ -267,12 +267,43 @@ test("following a link to a page with a matching frame does not dispatch a turbo ) }) -test("following a link within a frame with a target set navigates the target frame", async ({ page }) => { +test("following a link within a frame which has a target set navigates the target frame without morphing even when frame[refresh=morph]", async ({ page }) => { + await page.click("#add-refresh-morph-to-frame") await page.click("#hello a") await nextBeat() - const frameText = await page.textContent("#frame h2") - assert.equal(frameText, "Frame: Loaded") + expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-render")).toBeTruthy() + expect(await noNextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() + await expect(page.locator("#frame h2")).toHaveText("Frame: Loaded") +}) + +test("navigating from within replaces the contents even with turbo-frame[refresh=morph]", async ({ page }) => { + await page.click("#add-refresh-morph-to-frame") + await page.click("#link-frame") + await nextBeat() + + expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-render")).toBeTruthy() + expect(await noNextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() + await expect(page.locator("#frame h2")).toHaveText("Frame: Loaded") +}) + +test("calling reload on a frame replaces the contents", async ({ page }) => { + await page.click("#add-src-to-frame") + + await page.evaluate(() => document.getElementById("frame").reload()) + + expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-render")).toBeTruthy() + expect(await noNextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() +}) + +test("calling reload on a frame[refresh=morph] morphs the contents", async ({ page }) => { + await page.click("#add-src-to-frame") + await page.click("#add-refresh-morph-to-frame") + + await page.evaluate(() => document.getElementById("frame").reload()) + + expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-render")).toBeTruthy() + expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() }) test("following a link in rapid succession cancels the previous request", async ({ page }) => { @@ -667,7 +698,7 @@ test("navigating turbo-frame[data-turbo-action=advance] from within pushes URL s test("navigating turbo-frame[data-turbo-action=advance] from outside with target pushes URL state", async ({ page }) => { await page.click("#add-turbo-action-to-frame") - await page.click("#hello-link-frame") + await page.click("#hello a") await nextEventNamed(page, "turbo:load") await expect(page.locator("h1")).toHaveText("Frames") diff --git a/src/tests/functional/page_refresh_tests.js b/src/tests/functional/page_refresh_tests.js index 0b6945063..252a454c4 100644 --- a/src/tests/functional/page_refresh_tests.js +++ b/src/tests/functional/page_refresh_tests.js @@ -160,7 +160,7 @@ test("doesn't morph when the navigation doesn't go to the same URL", async ({ pa expect(await noNextEventNamed(page, "turbo:render", { renderMethod: "morph" })).toBeTruthy() }) -test("uses morphing to update remote frames marked with refresh='morph'", async ({ page }) => { +test("uses morphing to only update remote frames marked with refresh='morph'", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html") await page.click("#form-submit") @@ -180,12 +180,15 @@ test("uses morphing to update remote frames marked with refresh='morph'", async test("don't refresh frames contained in [data-turbo-permanent] elements", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html") + // Set the frame's text since the final assertion cannot be noNextEventOnTarget as that is passing even when the frame reloads. + const frame = page.locator("#remote-permanent-frame") + await frame.evaluate((frame) => frame.textContent = "Frame to be preserved") + await page.click("#form-submit") await nextEventNamed(page, "turbo:render", { renderMethod: "morph" }) await nextBeat() - // Only the frame marked with refresh="morph" uses morphing - expect(await noNextEventOnTarget(page, "refresh-reload", "turbo:before-frame-morph")).toBeTruthy() + await expect(page.locator("#remote-permanent-frame")).toHaveText("Frame to be preserved") }) test("frames marked with refresh='morph' are excluded from full page morphing", async ({ page }) => {