New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lift the frame morphing logic up to FrameController.reload #1192
base: main
Are you sure you want to change the base?
Conversation
src/core/drive/morph_renderer.js
Outdated
|
||
// Private | ||
// Static methods — these helper methods are all static because morphing is done by turbo frames, which is outside of a PageView |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no change to the logic of any of these methods. They're simply converted to static methods so that they can be used by FrameController.
src/core/drive/morph_renderer.js
Outdated
#reloadRemoteFrames() { | ||
this.#remoteFrames().forEach((frame) => { | ||
frame.reload() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class no longer needs to check if the frame has morph set or not. It can blindly trigger a reload()
on each frame and trust that the frame will be smart — it knows if it needs to morph itself.
@@ -1,3 +1,4 @@ | |||
<turbo-frame id="refresh-morph"> | |||
<h2>Loaded morphed frame</h2> | |||
<h3>Does not change</h3> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a piece of HTML which does not change made it easier for me to observe whether morphing is occurring or not within chrome inspector
@@ -654,7 +694,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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every other test was referencing click("#hello a")
so I updated this one for consistency.
@seanpdoyle You reviewed my documentation PR yesterday, related to this change. I wanted to ping on this one in case you could also look at the bug fix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@krschacht thanks for working on this.
Instead of reusing MorphRenderer
, which is a PageRenderer
for doing this, I'd consider adding a specialized FrameRenderer
that uses morphing. It could kick in when [refresh="morph"]
. I think this would also remove some clunkiness in the current MorphRenderer
, where it relies on an event to use morphing to refresh frames.
@jorgemanrubia Sure thing! I'm still trying to fully understand the architecture, but I think I follow what you're saying. That event logic was clunky. I'll get this cleaned up so we can get it merged in. Just to preview my plan in case you want to weigh in. I will plan to:
|
e429db7
to
e1fde68
Compare
@jorgemanrubia This PR is ready to go. I confirmed that all tests are passing and added good test coverage for the new cases. I also made a corresponding update to the turbo-frame docs here: hotwired/turbo-site#170 |
src/core/morph_elements.js
Outdated
import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js" | ||
import { dispatch } from "../util" | ||
|
||
export class MorphElements { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these are all static
methods, is there any benefit to defining exporting a class
? Would a collection of exported functions serve the same purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle Unlike some of the other utility files, I did it as a class because they're tightly coupled methods so on any use you'd need to import all methods. None of them stand alone. In this way, it's similar to the Cache utility class. But there is no actual state that is being instantiated so I did them static methods.
But I'm happy to change them to independent functions which are each exported. Would you like me to make that change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it as a class because they're tightly coupled methods so on any use you'd need to import all methods
Reading through the diff in its current state, the MorphElements
class is only ever interacted with by importing the class and invoking MorphElements.morph
directly:
import { MorphElements } from "../morph_elements"
// ...
MorphElements.morph(...)
Since its consistently invoked with a single method, the surface area of the module's interface could be reduced to only include morph
:
import { morph } from "../morph_elements"
morph(...)
Then, the module could be simplified to be a collection of module-private functions supporting a single exported morph
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry, you're right!
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle As I was doing some additional cleanup, I discovered that this change broke a random test in the test suite. Specifically, moving from a class to a collection of methods.
I spent a little while trying to track down the source of the bug and the only way I could solve it was to bring the class back.
The issue has something to do with this
context as shared between methods. The morph()
method sets this.isMorphingTurboFrame
which is later referenced by shouldMorphElement()
, but this is intentionally an arrow function because it's provided as a callback to Idiomorph. As I was banging my head against the wall trying to figure out another way around this or threading the context through as an argument, it occurred to me: this is the problem that classes solve! :) I just mean: sharing context between discrete method calls.
For now I reverted back to the class with static methods. The full test suite passes again. If you really think it shouldn't be a class, the solution needs to involve eliminating this shared state and somehow threading it through, but I don't understand the innerworkings of Idiomorph to easily do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the morphing is stateful (uses this
), you're correct: separate functions won't be suitable.
The original feedback was around the over-use of the static
keyword. A class that's composed of all static
keywords is functionally equivalent to a group of functions. Any "state" preserved through assigning to a this
will be global. That's equivalent to a module-local variable.
Sharing a model-local variable across all morphing will lead to sharing that state across potentially concurrent morphs. For example, if this.isMorphingTurboFrame
is true for a frame but false for a page-wide morph, that contention could cause unexpected behavior.
As a balance, what do you think of this set of changes:
diff --git a/src/core/drive/morph_page_renderer.js b/src/core/drive/morph_page_renderer.js
index 3e2dbf8..9418f44 100644
--- a/src/core/drive/morph_page_renderer.js
+++ b/src/core/drive/morph_page_renderer.js
@@ -1,6 +1,6 @@
import { dispatch } from "../../util"
import { PageRenderer } from "./page_renderer"
-import { MorphElements } from "../morph_elements"
+import { morphElements } from "../morph_elements"
export class MorphPageRenderer extends PageRenderer {
async render() {
@@ -14,7 +14,7 @@ export class MorphPageRenderer extends PageRenderer {
// Private
async #morphBody() {
- MorphElements.morph(this.currentElement, this.newElement)
+ morphElements(this.currentElement, this.newElement)
this.#reloadRemoteFrames()
dispatch("turbo:morph", {
diff --git a/src/core/frames/morph_frame_renderer.js b/src/core/frames/morph_frame_renderer.js
index 83d8bf1..b8404e2 100644
--- a/src/core/frames/morph_frame_renderer.js
+++ b/src/core/frames/morph_frame_renderer.js
@@ -1,5 +1,5 @@
import { FrameRenderer } from "./frame_renderer"
-import { MorphElements } from "../morph_elements"
+import { morphElements } from "../morph_elements"
import { dispatch } from "../../util"
export class MorphFrameRenderer extends FrameRenderer {
@@ -11,6 +11,6 @@ export class MorphFrameRenderer extends FrameRenderer {
detail: { currentElement, newElement }
})
- MorphElements.morph(currentElement, newElement, "innerHTML")
+ morphElements(currentElement, newElement, "innerHTML")
}
}
diff --git a/src/core/morph_elements.js b/src/core/morph_elements.js
index 0761486..7b74248 100644
--- a/src/core/morph_elements.js
+++ b/src/core/morph_elements.js
@@ -2,8 +2,14 @@ import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
import { dispatch } from "../util"
import { FrameElement } from "../elements/frame_element"
-export class MorphElements {
- static morph(currentElement, newElement, morphStyle = "outerHTML") {
+export function morphElements(currentElement, newElement, morphStyle = "outerHTML") {
+ const renderer = new Renderer()
+
+ renderer.morph(currentElement, newElement, morphStyle)
+}
+
+class Renderer {
+ morph(currentElement, newElement, morphStyle) {
this.isMorphingTurboFrame = this.isFrameReloadedWithMorph(currentElement)
Idiomorph.morph(currentElement, newElement, {
@@ -18,15 +24,15 @@ export class MorphElements {
})
}
- static isFrameReloadedWithMorph(element) {
+ isFrameReloadedWithMorph(element) {
return (element instanceof FrameElement) && element.shouldReloadWithMorph
}
- static shouldAddElement = (node) => {
+ shouldAddElement = (node) => {
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
}
- static shouldMorphElement = (oldNode, newNode) => {
+ shouldMorphElement = (oldNode, newNode) => {
if (!(oldNode instanceof HTMLElement)) return
if (oldNode.hasAttribute("data-turbo-permanent")) return false
@@ -44,17 +50,17 @@ export class MorphElements {
return !event.defaultPrevented
}
- static shouldUpdateAttribute = (attributeName, target, mutationType) => {
+ shouldUpdateAttribute = (attributeName, target, mutationType) => {
const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } })
return !event.defaultPrevented
}
- static shouldRemoveElement = (node) => {
+ shouldRemoveElement = (node) => {
return this.shouldMorphElement(node)
}
- static didMorphElement = (oldNode, newNode) => {
+ didMorphElement = (oldNode, newNode) => {
if (newNode instanceof HTMLElement) {
dispatch("turbo:morph-element", {
target: oldNode,
That diff removes the static
lines in favor of a bonafide Class with instances. It also exports a single morphElements
function for callers to use without any knowledge that it's implemented with a class instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle Good call, I made this change. I confirmed that the tests all still pass.
My recommendation would be that we merge in this PR and circle back to the open "turbo:before-frame-morph" element later in a follow-up since this PR is not making any changes to events.
static renderElement(currentElement, newParentElement) { | ||
const newElement = newParentElement.children | ||
|
||
dispatch("turbo:before-frame-morph", { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are we hoping to achieve by introducing a new event that exists separately from the other Morph events? What differs between this event and a turbo:before-morph-element
event that references the FrameElement
instance with its target
and currentElement
properties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle That's existing behavior in main. I preserved it so as not to break anyone's usage. You can see it here in the old morph_renderer.js:
https://github.com/hotwired/turbo/pull/1192/files#diff-04bb702ad80fc80a9ba0ddee175f0ad2527604ef65f45c78da9c379e5f63035eL102
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for clarifying @krschacht, I'd missed that.
@jorgemanrubia currently the turbo:before-frame-morph
event is not yet publicly documented.
Are we willing to omit it from the public collection of events, and instead encourage turbo:before-morph-element
events? I think it pre-dated the introduction of those events in #1097.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, it would help with quite a few things! I left minor comments.
src/elements/frame_element.js
Outdated
@@ -82,6 +82,10 @@ export class FrameElement extends HTMLElement { | |||
return this.getAttribute("refresh") | |||
} | |||
|
|||
get shouldReloadWithMorph() { | |||
this.src && this.refresh === "morph" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this have a return
statement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, is it important to exclude initial <turbo-frame>
elements that don't have a [src]
attribute? Wouldn't morphing an initial HTTP request have a benefit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're too fast for me! :) I actually mistakenly pushed before I was done. I'm doing a little more cleanup and making sure all tests pass before I went ahead and tagged anyone.
However, I don't understand your second comment. My mental model was that morphing for turbo frames only applied when there was a src. And I didn't introduce that extra condition, that's currently present within main.
When there is no src, then the full contents of the HTML must be supplied within the tags so that means it's part of the overall page. And so if the overall page morphs, then there is no special handling for turbo-frames — the contents within them just morphs along with all the rest of the HTML of that page. But I could be misunderstanding that.
That was my read of this original logic:
https://github.com/hotwired/turbo/blob/main/src/core/drive/morph_renderer.js#L95
The event binding hack was only being added when there was a src so I assume without that it's just regular HTML morphing of the page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're good with that, this PR should be ready. I just did the additional refactor Bruno suggested and I know you still want Jorge to weigh in on removing that event (or maybe you meant keep the event but just omit it from public documentation, I'm good either way).
c237cb2
to
87c9553
Compare
This is probably the Turbo 8 feature I'm looking forward to the most at the moment, thank you @krschacht. I just wanted to say I've been testing this locally for the past week or so and it works very well, I haven't run into any gotchas or bugs. |
Follow-up to [][] 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 `Morph` class. The `Morph` 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 `Morph` 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][] 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
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 `Morph` class. The `Morph` 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 `Morph` 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][] 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
I've opened #1234, which overlaps with the extractive portion of this PR. It doesn't incorporate the rest of the behavior change that accounts for If #1234 merges ahead of this, you'll likely need to rebase. I'm happy to assist with that effort if that ends up being the case. |
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][] 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
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
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
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
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
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
Follow-up to [hotwired#1185][] Related to [hotwired#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 [hotwired#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][hotwired#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 } }) ``` [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
Follow-up to [hotwired#1185][] Related to [hotwired#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 [hotwired#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][hotwired#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 } }) ``` [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
Follow-up to [hotwired#1185][] Related to [hotwired#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 [hotwired#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][hotwired#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 } }) ``` [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
Follow-up to [hotwired#1185][] Related to [hotwired#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 [hotwired#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][hotwired#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 } }) ``` [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
Fixes Issue #1161
Background:
turbo-frame
tags support an optional property ofrefresh="morph"
. It's used like this:When the full page morphs, this turbo-frame src is reloaded, and the presence of
morph
ensures the contents is morphed rather than replaced.Problem:
However, if you trigger an update of this frame by another means such as
document.getElementById('post').reload()
then the contents of the frame is replaced without morphing. @jorgemanrubia acknowledged in this comment that it would be nice for this behavior to be consistent.Solution within this PR:
This PR creates a new
MorphFrameRenderer
which inherits fromFrameRenderer
. (Note:MorphRenderer
is renamed toMorphPageRenderer
for clarity.) And just likePageView
decides when to useMorphPageRenderer
, nowFrameController
makes the appropriate decision of when to useMorphFrameRenderer
.Because
MorphFrame*
andMorphPage*
share so much logic this has been extracted into a utility classcore/morph_elements.js
(I made a corresponding update to the turbo-frame docs here: hotwired/turbo-site#170)