Skip to content

Commit 456b93e

Browse files
committed
Extract and re-use element morphing logic
Follow-up to [hotwired#1185][] Related to [hotwired#1192][] The `morphElements` function --- Introduce a new `src/core/morphing` module to expose a centralized and re-usable `morphElements(currentElement, newElement, delegate)` function to be invoked across the various morphing contexts. Next, move the logic from the `MorphRenderer` into a module-private `IdomorphDelegate` class. The `IdomorphDelegate` 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 `IdomorphDelegate` class, including checks for `[data-turbo-permanent]`. To serve as a seam for integration, the class retains a reference to a delegate responsible for: * providing options for the `Idiomorph` * determining whether or not a node should be skipped while morphing The `PageMorphRenderer` skips `<turbo-frame refresh="morph">` elements so that it can override their rendering to use morphing. Similarly, the `FrameMorphRenderer` provides the `morphStyle: "innerHTML"` option to morph its children. Changes to the renderers --- To integrate with the new module, first rename the `MorphRenderer` to `PageMorphRenderer` 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 `PageMorphRenderer.renderElement` to mirror the other existing renderer static functions (like [PageRenderer.renderElement][], [ErrorRenderer.renderElement][], and [FrameRenderer.renderElement][]). This integrates with the changes proposed in [hotwired#1028][]. Next, modify the rest of the `PageMorphRenderer` 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 `FrameMorphRenderer` class to define the `FrameMorphRenderer.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][hotwired#1177] by overriding the rendering mechanism in `turbo:before-render`: ```js addEventListener("turbo:before-render", (event) => { const someCriteriaForMorphing = ... if (someCriteriaForMorphing) { event.detail.render = (currentElement, newElement) => { window.Turbo.morphElements(currentElement, newElement, { ... }) } } }) ``` [hotwired#1185]: hotwired#1185 (comment) [hotwired#1192]: hotwired#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 [hotwired#1028]: hotwired#1028 [hotwired#1177]: hotwired#1177
1 parent 9fb05e3 commit 456b93e

File tree

7 files changed

+126
-188
lines changed

7 files changed

+126
-188
lines changed

src/core/drive/morph_renderer.js

-118
This file was deleted.

src/core/drive/page_morph_renderer.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { morphRefresh } from "../morphing"
2+
import { PageRenderer } from "./page_renderer"
3+
4+
export class PageMorphRenderer extends PageRenderer {
5+
static renderElement(currentElement, newElement) {
6+
morphRefresh(currentElement, newElement)
7+
}
8+
9+
async preservingPermanentElements(callback) {
10+
return await callback()
11+
}
12+
13+
get renderMethod() {
14+
return "morph"
15+
}
16+
}

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 { PageMorphRenderer } from "./page_morph_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 ? PageMorphRenderer : 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
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { FrameRenderer } from "./frame_renderer"
2+
import { morphFrames } from "../morphing"
3+
4+
export class FrameMorphRenderer extends FrameRenderer {
5+
static renderElement(currentElement, newElement) {
6+
morphFrames(currentElement, newElement)
7+
}
8+
}

src/core/morphing.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
2+
import { FrameElement } from "../elements/frame_element"
3+
import { dispatch } from "../util"
4+
5+
export function morphRefresh(currentElement, newElement) {
6+
idiomorph(currentElement, newElement, {
7+
willMorphElement: element => !canRefreshFrame(element)
8+
})
9+
10+
for (const frame of document.querySelectorAll("turbo-frame")) {
11+
if (canRefreshFrame(frame)) refreshFrame(frame)
12+
}
13+
14+
dispatch("turbo:morph", { detail: { currentElement, newElement } })
15+
}
16+
17+
export function morphFrames(currentElement, newElement) {
18+
dispatch("turbo:before-frame-morph", {
19+
target: currentElement,
20+
detail: { currentElement, newElement }
21+
})
22+
23+
morphChildren(currentElement, newElement)
24+
}
25+
26+
export function morphChildren(currentElement, newElement) {
27+
idiomorph(currentElement, newElement.children, {
28+
morphStyle: "innerHTML"
29+
})
30+
}
31+
32+
export function morphElements(currentElement, newElement) {
33+
idiomorph(currentElement, newElement)
34+
}
35+
36+
function idiomorph(currentElement, newElement, { willMorphElement, ...options } = {}) {
37+
const callbacks = new IdiomorphCallbacks(willMorphElement)
38+
39+
Idiomorph.morph(currentElement, newElement, { ...options, callbacks })
40+
}
41+
42+
class IdiomorphCallbacks {
43+
constructor(willMorphElement) {
44+
this.willMorphElement = willMorphElement || ((element) => true)
45+
}
46+
47+
beforeNodeAdded = (node) => {
48+
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
49+
}
50+
51+
beforeNodeMorphed = (target, newElement) => {
52+
if (target instanceof HTMLElement) {
53+
if (!target.hasAttribute("data-turbo-permanent") && this.willMorphElement(target)) {
54+
const event = dispatch("turbo:before-morph-element", { cancelable: true, target, detail: { newElement } })
55+
56+
return !event.defaultPrevented
57+
} else {
58+
return false
59+
}
60+
}
61+
}
62+
63+
beforeAttributeUpdated = (attributeName, target, mutationType) => {
64+
const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } })
65+
66+
return !event.defaultPrevented
67+
}
68+
69+
beforeNodeRemoved = (node) => {
70+
return this.beforeNodeMorphed(node)
71+
}
72+
73+
afterNodeMorphed = (target, newNode) => {
74+
if (newNode instanceof HTMLElement) {
75+
dispatch("turbo:morph-element", { target, detail: { newElement: newNode } })
76+
}
77+
}
78+
}
79+
80+
function canRefreshFrame(frame) {
81+
return frame instanceof FrameElement &&
82+
frame.src &&
83+
frame.refresh === "morph" &&
84+
!frame.closest("[data-turbo-permanent]")
85+
}
86+
87+
function refreshFrame(frame) {
88+
frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
89+
detail.render = morphFrames
90+
}, { once: true })
91+
92+
frame.reload()
93+
}

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)