diff --git a/src/core/session.ts b/src/core/session.ts index 6df0c9b62..5e6a735e0 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -11,6 +11,7 @@ import { Navigator, NavigatorDelegate } from "./drive/navigator" import { PageObserver, PageObserverDelegate } from "../observers/page_observer" import { ScrollObserver } from "../observers/scroll_observer" import { StreamMessage } from "./streams/stream_message" +import { StreamMessageRenderer } from "./streams/stream_message_renderer" import { StreamObserver } from "../observers/stream_observer" import { Action, Position, StreamSource, isAction } from "./types" import { clearBusyState, dispatch, markAsBusy } from "../util" @@ -62,6 +63,7 @@ export class Session readonly streamObserver = new StreamObserver(this) readonly formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement) readonly frameRedirector = new FrameRedirector(this, document.documentElement) + readonly streamMessageRenderer = new StreamMessageRenderer() drive = true enabled = true @@ -129,7 +131,7 @@ export class Session } renderStreamMessage(message: StreamMessage | string) { - document.documentElement.appendChild(StreamMessage.wrap(message).fragment) + this.streamMessageRenderer.render(StreamMessage.wrap(message)) } clearCache() { diff --git a/src/core/snapshot.ts b/src/core/snapshot.ts index 414d4afc8..7d5431117 100644 --- a/src/core/snapshot.ts +++ b/src/core/snapshot.ts @@ -37,11 +37,11 @@ export class Snapshot { } get permanentElements() { - return [...this.element.querySelectorAll("[id][data-turbo-permanent]")] + return queryPermanentElementsAll(this.element) } getPermanentElementById(id: string) { - return this.element.querySelector(`#${id}[data-turbo-permanent]`) + return getPermanentElementById(this.element, id) } getPermanentElementMapForSnapshot(snapshot: Snapshot) { @@ -59,4 +59,12 @@ export class Snapshot { } } +export function getPermanentElementById(node: ParentNode, id: string) { + return node.querySelector(`#${id}[data-turbo-permanent]`) +} + +export function queryPermanentElementsAll(node: ParentNode) { + return node.querySelectorAll("[id][data-turbo-permanent]") +} + export type PermanentElementMap = Record diff --git a/src/core/streams/stream_message_renderer.ts b/src/core/streams/stream_message_renderer.ts new file mode 100644 index 000000000..549e4de8d --- /dev/null +++ b/src/core/streams/stream_message_renderer.ts @@ -0,0 +1,36 @@ +import { StreamMessage } from "./stream_message" +import { StreamElement } from "../../elements/stream_element" +import { Bardo, BardoDelegate } from "../bardo" +import { PermanentElementMap, getPermanentElementById, queryPermanentElementsAll } from "../snapshot" + +export class StreamMessageRenderer implements BardoDelegate { + render({ fragment }: StreamMessage) { + Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => + document.documentElement.appendChild(fragment) + ) + } + + enteringBardo(currentPermanentElement: Element, newPermanentElement: Element) { + newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true)) + } + + leavingBardo() {} +} + +function getPermanentElementMapForFragment(fragment: DocumentFragment): PermanentElementMap { + const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement) + const permanentElementMap: PermanentElementMap = {} + for (const permanentElementInDocument of permanentElementsInDocument) { + const { id } = permanentElementInDocument + + for (const streamElement of fragment.querySelectorAll("turbo-stream")) { + const elementInStream = getPermanentElementById(streamElement.templateElement.content, id) + + if (elementInStream) { + permanentElementMap[id] = [permanentElementInDocument, elementInStream] + } + } + } + + return permanentElementMap +} diff --git a/src/tests/functional/rendering_tests.ts b/src/tests/functional/rendering_tests.ts index 24a6fc2b6..6ed9f8a87 100644 --- a/src/tests/functional/rendering_tests.ts +++ b/src/tests/functional/rendering_tests.ts @@ -395,6 +395,36 @@ test("test preserves permanent element video playback", async ({ page }) => { assert.equal(timeAfterRender, timeBeforeRender, "element state is preserved") }) +test("test preserves permanent element through Turbo Stream update", async ({ page }) => { + await page.evaluate(() => { + window.Turbo.renderStreamMessage(` + + + + `) + }) + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + +test("test preserves permanent element through Turbo Stream append", async ({ page }) => { + await page.evaluate(() => { + window.Turbo.renderStreamMessage(` + + + + `) + }) + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + test("test preserves input values", async ({ page }) => { await page.fill("#text-input", "test") await page.click("#checkbox-input")