Skip to content

Commit 5e99865

Browse files
committed
Extract and re-use element morphing logic
Follow-up to [#1185][] Related to [#1192][] The `morph{Page,Frames,Elements}` functions --- Introduce a new `src/core/morphing` module to expose a centralized and re-usable `morphElements(currentElement, newElement)` function to be invoked across the various morphing contexts. Next, move the logic from the `MorphRenderer` into a module-private `IdomorphCallbacks` class. The `IdomorphCallbacks` class (like its `MorphRenderer` predecessor) wraps a call to `Idiomorph` based on its own set of callbacks. The bulk of the logic remains in the `IdomorphCallbacks` class, including checks for `[data-turbo-permanent]`. To serve as a seam for integration, the class retains a reference to a callback responsible for: * providing options for the `Idiomorph` * determining whether or not a node should be skipped while morphing The `MorphingPageRenderer` skips `<turbo-frame refresh="morph">` elements so that it can override their rendering to use morphing. Similarly, the `MorphingFrameRenderer` provides the `morphStyle: "innerHTML"` option to morph its children. Changes to the renderers --- To integrate with the new module, first rename the `MorphRenderer` to `MorphingPageRenderer` to set a new precedent that communicates the level of the document the morphing is scoped to. With that change in place, define the static `MorphingPageRenderer.renderElement` to mirror the other existing renderer static functions (like [PageRenderer.renderElement][], [ErrorRenderer.renderElement][], and [FrameRenderer.renderElement][]). This integrates with the changes proposed in [#1028][]. Next, modify the rest of the `MorphingPageRenderer` to integrate with its `PageRenderer` ancestor in a way that invokes the static `renderElement` function. This involves overriding the `preservingPermanentElements(callback)` method. In theory, morphing has implications on the concept of "permanence". In practice, morphing has the `[data-turbo-permanent]` attribute receive special treatment during morphing. Following the new precedent, introduce a new `MorphingFrameRenderer` class to define the `MorphingFrameRenderer.renderElement` function that invokes the `morphElements` function with `newElement.children` and `morphStyle: "innerHTML"`. Changes to the StreamActions --- The extraction of the `morphElements` function makes entirety of the `src/core/streams/actions/morph.js` module redundant. This commit removes that module and invokes `morphElements` directly within the `StreamActions.morph` function. Future possibilities --- In the future, additional changes could be made to expose the morphing capabilities as part of the `window.Turbo` interface. For example, applications could experiment with supporting [Page Refresh-style morphing for pages with different URL pathnames][#1177] by overriding the rendering mechanism in `turbo:before-render`: ```js addEventListener("turbo:before-render", (event) => { const someCriteriaForMorphing = ... if (someCriteriaForMorphing) { event.detail.render = Turbo.morphPage } }) addEventListener("turbo:before-frame-render", (event) => { const someCriteriaForMorphingAFrame = ... if (someCriteriaForMorphingAFrame) { event.detail.render = Turbo.morphFrames } }) ``` [#1185]: #1185 (comment) [#1192]: #1192 [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/9fb05e3ed3ebb15fe7b13f52941f25df425e3d15/src/core/drive/page_renderer.js#L5-L11 [ErrorRenderer.renderElement]: https://github.com/hotwired/turbo/blob/9fb05e3ed3ebb15fe7b13f52941f25df425e3d15/src/core/drive/error_renderer.js#L5-L9 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/9fb05e3ed3ebb15fe7b13f52941f25df425e3d15/src/core/frames/frame_renderer.js#L5-L16 [#1028]: #1028 [#1177]: #1177
1 parent 9fb05e3 commit 5e99865

File tree

7 files changed

+133
-188
lines changed

7 files changed

+133
-188
lines changed

src/core/drive/morph_renderer.js

-118
This file was deleted.
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { FrameElement } from "../../elements/frame_element"
2+
import { MorphingFrameRenderer } from "../frames/morphing_frame_renderer"
3+
import { PageRenderer } from "./page_renderer"
4+
import { dispatch } from "../../util"
5+
import { morphElements } from "../morphing"
6+
7+
export class MorphingPageRenderer extends PageRenderer {
8+
static renderElement(currentElement, newElement) {
9+
morphElements(currentElement, newElement, {
10+
callbacks: {
11+
beforeNodeMorphed: element => !canRefreshFrame(element)
12+
}
13+
})
14+
15+
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
16+
if (canRefreshFrame(frame)) refreshFrame(frame)
17+
}
18+
19+
dispatch("turbo:morph", { detail: { currentElement, newElement } })
20+
}
21+
22+
async preservingPermanentElements(callback) {
23+
return await callback()
24+
}
25+
26+
get renderMethod() {
27+
return "morph"
28+
}
29+
}
30+
31+
function canRefreshFrame(frame) {
32+
return frame instanceof FrameElement &&
33+
frame.src &&
34+
frame.refresh === "morph" &&
35+
!frame.closest("[data-turbo-permanent]")
36+
}
37+
38+
function refreshFrame(frame) {
39+
frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
40+
detail.render = MorphingFrameRenderer.renderElement
41+
}, { once: true })
42+
43+
frame.reload()
44+
}

src/core/drive/page_view.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { nextEventLoopTick } from "../../util"
22
import { View } from "../view"
33
import { ErrorRenderer } from "./error_renderer"
4-
import { MorphRenderer } from "./morph_renderer"
4+
import { MorphingPageRenderer } from "./morphing_page_renderer"
55
import { PageRenderer } from "./page_renderer"
66
import { PageSnapshot } from "./page_snapshot"
77
import { SnapshotCache } from "./snapshot_cache"
@@ -17,9 +17,9 @@ export class PageView extends View {
1717

1818
renderPage(snapshot, isPreview = false, willRender = true, visit) {
1919
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage
20-
const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer
20+
const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer
2121

22-
const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender)
22+
const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender)
2323

2424
if (!renderer.shouldRender) {
2525
this.forceReloaded = true
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { FrameRenderer } from "./frame_renderer"
2+
import { morphChildren } from "../morphing"
3+
import { dispatch } from "../../util"
4+
5+
export class MorphingFrameRenderer extends FrameRenderer {
6+
static renderElement(currentElement, newElement) {
7+
dispatch("turbo:before-frame-morph", {
8+
target: currentElement,
9+
detail: { currentElement, newElement }
10+
})
11+
12+
morphChildren(currentElement, newElement)
13+
}
14+
}

src/core/morphing.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
2+
import { dispatch } from "../util"
3+
4+
export function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {
5+
Idiomorph.morph(currentElement, newElement, {
6+
...options,
7+
callbacks: new IdiomorphCallbacks(callbacks)
8+
})
9+
}
10+
11+
export function morphChildren(currentElement, newElement) {
12+
morphElements(currentElement, newElement.children, {
13+
morphStyle: "innerHTML"
14+
})
15+
}
16+
17+
class IdiomorphCallbacks {
18+
#beforeNodeMorphed
19+
20+
constructor({ beforeNodeMorphed } = {}) {
21+
this.#beforeNodeMorphed = beforeNodeMorphed || (() => true)
22+
}
23+
24+
beforeNodeAdded = (node) => {
25+
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
26+
}
27+
28+
beforeNodeMorphed = (currentElement, newElement) => {
29+
if (currentElement instanceof Element) {
30+
if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
31+
const event = dispatch("turbo:before-morph-element", {
32+
cancelable: true,
33+
target: currentElement,
34+
detail: { currentElement, newElement }
35+
})
36+
37+
return !event.defaultPrevented
38+
} else {
39+
return false
40+
}
41+
}
42+
}
43+
44+
beforeAttributeUpdated = (attributeName, target, mutationType) => {
45+
const event = dispatch("turbo:before-morph-attribute", {
46+
cancelable: true,
47+
target,
48+
detail: { attributeName, mutationType }
49+
})
50+
51+
return !event.defaultPrevented
52+
}
53+
54+
beforeNodeRemoved = (node) => {
55+
return this.beforeNodeMorphed(node)
56+
}
57+
58+
afterNodeMorphed = (currentElement, newElement) => {
59+
if (currentElement instanceof Element) {
60+
dispatch("turbo:morph-element", {
61+
target: currentElement,
62+
detail: { currentElement, newElement }
63+
})
64+
}
65+
}
66+
}

src/core/streams/actions/morph.js

-65
This file was deleted.

src/core/streams/stream_actions.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { session } from "../"
2-
import morph from "./actions/morph"
2+
import { morphElements, morphChildren } from "../morphing"
33

44
export const StreamActions = {
55
after() {
@@ -40,6 +40,10 @@ export const StreamActions = {
4040
},
4141

4242
morph() {
43-
morph(this)
43+
const morph = this.hasAttribute("children-only") ?
44+
morphChildren :
45+
morphElements
46+
47+
this.targetElements.forEach((targetElement) => morph(targetElement, this.templateContent))
4448
}
4549
}

0 commit comments

Comments
 (0)