From 5dfaeccd436c87074d4ffd11b61d37cf93569889 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 15 Sep 2024 11:41:19 +0200 Subject: [PATCH 1/4] reloading a morphing frame should trigger reloads on its child morphing frames recursively. --- src/core/drive/morphing_page_renderer.js | 3 ++- src/core/frames/morphing_frame_renderer.js | 18 +++++++++++++++++- src/core/morphing.js | 3 ++- src/tests/fixtures/frames.html | 5 +++++ src/tests/functional/frame_tests.js | 15 +++++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/core/drive/morphing_page_renderer.js b/src/core/drive/morphing_page_renderer.js index fbdf5bc7f..c070c173e 100644 --- a/src/core/drive/morphing_page_renderer.js +++ b/src/core/drive/morphing_page_renderer.js @@ -35,5 +35,6 @@ function canRefreshFrame(frame) { return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && - !frame.closest("[data-turbo-permanent]") + !frame.closest("[data-turbo-permanent]") && + !frame.parentElement.closest("turbo-frame[src][refresh=morph]") } diff --git a/src/core/frames/morphing_frame_renderer.js b/src/core/frames/morphing_frame_renderer.js index 4d5ea9c34..3ce2ae5af 100644 --- a/src/core/frames/morphing_frame_renderer.js +++ b/src/core/frames/morphing_frame_renderer.js @@ -1,3 +1,4 @@ +import { FrameElement } from "../../elements/frame_element" import { FrameRenderer } from "./frame_renderer" import { morphChildren } from "../morphing" import { dispatch } from "../../util" @@ -9,10 +10,25 @@ export class MorphingFrameRenderer extends FrameRenderer { detail: { currentElement, newElement } }) - morphChildren(currentElement, newElement) + morphChildren(currentElement, newElement, { + callbacks: { + beforeNodeMorphed: element => !canRefreshFrame(element, currentElement) + } + }) + for (const frame of currentElement.querySelectorAll("turbo-frame")) { + if (canRefreshFrame(frame, currentElement)) frame.reload() + } } async preservingPermanentElements(callback) { return await callback() } } + +function canRefreshFrame(frame, currentFrame) { + return frame instanceof FrameElement && + frame.src && + frame.refresh === "morph" && + !frame.closest("[data-turbo-permanent]") && + frame.parentElement.closest("turbo-frame[src][refresh=morph]").id === currentFrame.id +} diff --git a/src/core/morphing.js b/src/core/morphing.js index dfd21e839..7719b9125 100644 --- a/src/core/morphing.js +++ b/src/core/morphing.js @@ -8,8 +8,9 @@ export function morphElements(currentElement, newElement, { callbacks, ...option }) } -export function morphChildren(currentElement, newElement) { +export function morphChildren(currentElement, newElement, options = {}) { morphElements(currentElement, newElement.children, { + ...options, morphStyle: "innerHTML" }) } diff --git a/src/tests/fixtures/frames.html b/src/tests/fixtures/frames.html index 1749de14b..e6cc19fc7 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -79,6 +79,8 @@

Frames: #hello

Frames: #nested-root

+ + Inner/Outer frame link
@@ -86,6 +88,8 @@

Frames: #nested-root

Frames: #nested-child

+ + Load #nested-child Visit one.html @@ -95,6 +99,7 @@

Frames: #nested-child

+ diff --git a/src/tests/functional/frame_tests.js b/src/tests/functional/frame_tests.js index 264024720..372381f20 100644 --- a/src/tests/functional/frame_tests.js +++ b/src/tests/functional/frame_tests.js @@ -306,6 +306,21 @@ test("calling reload on a frame[refresh=morph] morphs the contents", async ({ pa expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() }) +test("calling reload on a frame[refresh=morph] reloads descendant frame[refresh=morph] with morphing", async ({ page }) => { + await page.click("#nested-root #add-refresh-morph-to-frame") + await page.click("#nested-root #add-src-to-frame") + await page.click("#nested-child #add-refresh-morph-to-frame") + await page.click("#nested-child #add-src-to-frame") + await page.click("#nested-child-navigate-top #add-src-to-frame") + + await page.evaluate(() => document.getElementById("nested-root").reload()) + + // Only the frames marked with refresh="morph" uses morphing + expect(await nextEventOnTarget(page, "nested-root", "turbo:before-frame-morph")).toBeTruthy() + expect(await nextEventOnTarget(page, "nested-child", "turbo:before-frame-morph")).toBeTruthy() + expect(await noNextEventOnTarget(page, "nested-child-navigate-top", "turbo:before-frame-morph")).toBeTruthy() +}) + test("calling reload on a frame[refresh=morph] preserves [data-turbo-permanent] elements", async ({ page }) => { await page.click("#add-src-to-frame") await page.click("#add-refresh-morph-to-frame") From 1240b05483a727d3cfabf7c8d9bc861b651badf1 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 15 Sep 2024 13:23:13 +0200 Subject: [PATCH 2/4] refactor out duplication, made a bit awkward by the use of static methods. --- src/core/drive/morphing_page_renderer.js | 16 ++++++---------- src/core/frames/morphing_frame_renderer.js | 16 ++++++---------- src/core/renderer.js | 8 ++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/drive/morphing_page_renderer.js b/src/core/drive/morphing_page_renderer.js index c070c173e..489aceafe 100644 --- a/src/core/drive/morphing_page_renderer.js +++ b/src/core/drive/morphing_page_renderer.js @@ -1,4 +1,3 @@ -import { FrameElement } from "../../elements/frame_element" import { PageRenderer } from "./page_renderer" import { dispatch } from "../../util" import { morphElements } from "../morphing" @@ -7,12 +6,16 @@ export class MorphingPageRenderer extends PageRenderer { static renderElement(currentElement, newElement) { morphElements(currentElement, newElement, { callbacks: { - beforeNodeMorphed: element => !canRefreshFrame(element) + beforeNodeMorphed: element => { + return !super.shouldRefreshChildFrameWithMorphing(null, element) + } } }) for (const frame of currentElement.querySelectorAll("turbo-frame")) { - if (canRefreshFrame(frame)) frame.reload() + if (super.shouldRefreshChildFrameWithMorphing(null, frame)) { + frame.reload() + } } dispatch("turbo:morph", { detail: { currentElement, newElement } }) @@ -31,10 +34,3 @@ export class MorphingPageRenderer extends PageRenderer { } } -function canRefreshFrame(frame) { - return frame instanceof FrameElement && - frame.src && - frame.refresh === "morph" && - !frame.closest("[data-turbo-permanent]") && - !frame.parentElement.closest("turbo-frame[src][refresh=morph]") -} diff --git a/src/core/frames/morphing_frame_renderer.js b/src/core/frames/morphing_frame_renderer.js index 3ce2ae5af..b62f68ae6 100644 --- a/src/core/frames/morphing_frame_renderer.js +++ b/src/core/frames/morphing_frame_renderer.js @@ -1,4 +1,3 @@ -import { FrameElement } from "../../elements/frame_element" import { FrameRenderer } from "./frame_renderer" import { morphChildren } from "../morphing" import { dispatch } from "../../util" @@ -12,11 +11,15 @@ export class MorphingFrameRenderer extends FrameRenderer { morphChildren(currentElement, newElement, { callbacks: { - beforeNodeMorphed: element => !canRefreshFrame(element, currentElement) + beforeNodeMorphed: element => { + return !super.shouldRefreshChildFrameWithMorphing(currentElement, element) + } } }) for (const frame of currentElement.querySelectorAll("turbo-frame")) { - if (canRefreshFrame(frame, currentElement)) frame.reload() + if (super.shouldRefreshChildFrameWithMorphing(currentElement, frame)) { + frame.reload() + } } } @@ -25,10 +28,3 @@ export class MorphingFrameRenderer extends FrameRenderer { } } -function canRefreshFrame(frame, currentFrame) { - return frame instanceof FrameElement && - frame.src && - frame.refresh === "morph" && - !frame.closest("[data-turbo-permanent]") && - frame.parentElement.closest("turbo-frame[src][refresh=morph]").id === currentFrame.id -} diff --git a/src/core/renderer.js b/src/core/renderer.js index 8ede76e65..97b97682a 100644 --- a/src/core/renderer.js +++ b/src/core/renderer.js @@ -1,3 +1,4 @@ +import { FrameElement } from "../elements/frame_element" import { Bardo } from "./bardo" export class Renderer { @@ -7,6 +8,13 @@ export class Renderer { // Abstract method } + static shouldRefreshChildFrameWithMorphing(parentFrame, frame) { + return frame instanceof FrameElement && + frame.shouldReloadWithMorph && + !frame.closest("[data-turbo-permanent]") && + frame.parentElement.closest("turbo-frame[src][refresh=morph]") === parentFrame + } + constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) { this.currentSnapshot = currentSnapshot this.newSnapshot = newSnapshot From c714a4101fa50191affff493cfef9a5799986046 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 24 Sep 2024 14:53:19 +0200 Subject: [PATCH 3/4] failing test to expose issue with turbo-frames changing. --- src/tests/fixtures/frames.html | 3 +++ src/tests/functional/frame_tests.js | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/tests/fixtures/frames.html b/src/tests/fixtures/frames.html index e6cc19fc7..60f96adef 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -17,6 +17,8 @@ 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") + } else if (target.id == "change-frame-id-to-different-nested-child") { + target.closest("turbo-frame")?.setAttribute("id", "different-nested-child") } }) @@ -90,6 +92,7 @@

Frames: #nested-root

Frames: #nested-child

+ Load #nested-child Visit one.html diff --git a/src/tests/functional/frame_tests.js b/src/tests/functional/frame_tests.js index 372381f20..fc1321e52 100644 --- a/src/tests/functional/frame_tests.js +++ b/src/tests/functional/frame_tests.js @@ -306,7 +306,7 @@ test("calling reload on a frame[refresh=morph] morphs the contents", async ({ pa expect(await nextEventOnTarget(page, "frame", "turbo:before-frame-morph")).toBeTruthy() }) -test("calling reload on a frame[refresh=morph] reloads descendant frame[refresh=morph] with morphing", async ({ page }) => { +test("calling reload on a frame[refresh=morph] reloads descendant frame[refresh=morph]s via reload() as well", async ({ page }) => { await page.click("#nested-root #add-refresh-morph-to-frame") await page.click("#nested-root #add-src-to-frame") await page.click("#nested-child #add-refresh-morph-to-frame") @@ -321,6 +321,21 @@ test("calling reload on a frame[refresh=morph] reloads descendant frame[refresh= expect(await noNextEventOnTarget(page, "nested-child-navigate-top", "turbo:before-frame-morph")).toBeTruthy() }) +test("calling reload on a frame[refresh=morph] morphs descendant frame[refresh=morph] normally if the id no longer matches", async ({ page }) => { + await page.click("#nested-root #add-refresh-morph-to-frame") + await page.click("#nested-root #add-src-to-frame") + await page.click("#nested-child #add-refresh-morph-to-frame") + await page.click("#nested-child #add-src-to-frame") + await page.click("#nested-child #change-frame-id-to-different-nested-child") + + await page.evaluate(() => document.getElementById("nested-root").reload()) + + expect(await nextEventOnTarget(page, "nested-root", "turbo:before-frame-morph")).toBeTruthy() + expect(await noNextEventOnTarget(page, "different-nested-child", "turbo:before-frame-morph")).toBeTruthy() + assert.ok(await hasSelector(page, "#nested-child")) + assert.notOk(await hasSelector(page, "#different-nested-child")) +}) + test("calling reload on a frame[refresh=morph] preserves [data-turbo-permanent] elements", async ({ page }) => { await page.click("#add-src-to-frame") await page.click("#add-refresh-morph-to-frame") From c10576d2df55898dfd39e87cf65111e1512fd5d6 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 24 Sep 2024 12:20:30 +0200 Subject: [PATCH 4/4] only reload child frames during morph if the old and new frame have matching ids. --- src/core/drive/morphing_page_renderer.js | 14 ++++++-------- src/core/frames/morphing_frame_renderer.js | 13 ++++++------- src/core/renderer.js | 15 ++++++++++----- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/core/drive/morphing_page_renderer.js b/src/core/drive/morphing_page_renderer.js index 489aceafe..0fff99992 100644 --- a/src/core/drive/morphing_page_renderer.js +++ b/src/core/drive/morphing_page_renderer.js @@ -6,18 +6,16 @@ export class MorphingPageRenderer extends PageRenderer { static renderElement(currentElement, newElement) { morphElements(currentElement, newElement, { callbacks: { - beforeNodeMorphed: element => { - return !super.shouldRefreshChildFrameWithMorphing(null, element) + beforeNodeMorphed: (node, newNode) => { + if (super.shouldRefreshChildFrameWithMorphing(null, node, newNode)) { + node.reload() + return false + } + return true } } }) - for (const frame of currentElement.querySelectorAll("turbo-frame")) { - if (super.shouldRefreshChildFrameWithMorphing(null, frame)) { - frame.reload() - } - } - dispatch("turbo:morph", { detail: { currentElement, newElement } }) } diff --git a/src/core/frames/morphing_frame_renderer.js b/src/core/frames/morphing_frame_renderer.js index b62f68ae6..c6c6d13b2 100644 --- a/src/core/frames/morphing_frame_renderer.js +++ b/src/core/frames/morphing_frame_renderer.js @@ -11,16 +11,15 @@ export class MorphingFrameRenderer extends FrameRenderer { morphChildren(currentElement, newElement, { callbacks: { - beforeNodeMorphed: element => { - return !super.shouldRefreshChildFrameWithMorphing(currentElement, element) + beforeNodeMorphed: (node, newNode) => { + if (super.shouldRefreshChildFrameWithMorphing(currentElement, node, newNode)) { + node.reload() + return false + } + return true } } }) - for (const frame of currentElement.querySelectorAll("turbo-frame")) { - if (super.shouldRefreshChildFrameWithMorphing(currentElement, frame)) { - frame.reload() - } - } } async preservingPermanentElements(callback) { diff --git a/src/core/renderer.js b/src/core/renderer.js index 97b97682a..aea85ac1e 100644 --- a/src/core/renderer.js +++ b/src/core/renderer.js @@ -8,11 +8,16 @@ export class Renderer { // Abstract method } - static shouldRefreshChildFrameWithMorphing(parentFrame, frame) { - return frame instanceof FrameElement && - frame.shouldReloadWithMorph && - !frame.closest("[data-turbo-permanent]") && - frame.parentElement.closest("turbo-frame[src][refresh=morph]") === parentFrame + static shouldRefreshChildFrameWithMorphing(parentFrame, currentFrame, newFrame) { + return currentFrame instanceof FrameElement && + // newFrame cannot yet be an instance of FrameElement because custom + // elements don't get initialized until they're attached to the DOM, so + // test its Element#nodeName instead + newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && + currentFrame.shouldReloadWithMorph && + currentFrame.id === newFrame.id && + !currentFrame.closest("[data-turbo-permanent]") && + currentFrame.parentElement.closest("turbo-frame[src][refresh=morph]") === parentFrame } constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {