Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

krschacht
Copy link

@krschacht krschacht commented Feb 19, 2024

Fixes Issue #1161

Background:

turbo-frame tags support an optional property of refresh="morph". It's used like this:

   <turbo-frame id="post" refresh="morph" src="/post/1">
      Loading... (preload contents)
   </turbo-frame>

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 from FrameRenderer. (Note: MorphRenderer is renamed to MorphPageRenderer for clarity.) And just like PageView decides when to use MorphPageRenderer, now FrameController makes the appropriate decision of when to use MorphFrameRenderer.

Because MorphFrame* and MorphPage* share so much logic this has been extracted into a utility class core/morph_elements.js

(I made a corresponding update to the turbo-frame docs here: hotwired/turbo-site#170)


// Private
// Static methods — these helper methods are all static because morphing is done by turbo frames, which is outside of a PageView
Copy link
Author

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.

#reloadRemoteFrames() {
this.#remoteFrames().forEach((frame) => {
frame.reload()
})
Copy link
Author

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>
Copy link
Author

@krschacht krschacht Feb 19, 2024

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")
Copy link
Author

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.

@krschacht krschacht changed the title Lift the frame morphing logic up to FrameElement.reload Lift the frame morphing logic up to FrameController.reload Feb 19, 2024
@krschacht
Copy link
Author

@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.

Copy link
Member

@jorgemanrubia jorgemanrubia left a 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.

@krschacht
Copy link
Author

krschacht commented Feb 24, 2024

@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:

  • Rename MorphRenderer to MorphPageRenderer
  • Create a new MorphFrameRenderer which inherits from FrameRenderer
  • Create a utility class which contains the core morphing logic so that these two Renderers don't duplicate so much code. Maybe src/core/morph_elements.js so that both Renders will be able to call MorphElements.morph(currentElement, newElement, morphStyle = "outerHTML")

@krschacht
Copy link
Author

@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

import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
import { dispatch } from "../util"

export class MorphElements {
Copy link
Contributor

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?

Copy link
Author

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?

Copy link
Contributor

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.

Copy link
Author

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

Copy link
Author

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.

Copy link
Contributor

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.

Copy link
Author

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.

src/tests/server.mjs Outdated Show resolved Hide resolved
static renderElement(currentElement, newParentElement) {
const newElement = newParentElement.children

dispatch("turbo:before-frame-morph", {
Copy link
Contributor

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?

Copy link
Author

@krschacht krschacht Feb 27, 2024

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

Copy link
Contributor

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.

Copy link
Contributor

@brunoprietog brunoprietog left a 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/core/frames/frame_controller.js Outdated Show resolved Hide resolved
src/core/morph_elements.js Outdated Show resolved Hide resolved
@@ -82,6 +82,10 @@ export class FrameElement extends HTMLElement {
return this.getAttribute("refresh")
}

get shouldReloadWithMorph() {
this.src && this.refresh === "morph"
Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Author

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).

@rbclark
Copy link

rbclark commented Mar 13, 2024

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.

seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Mar 30, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Mar 30, 2024
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
@seanpdoyle
Copy link
Contributor

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 turbo-frame[refresh="morph"].

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.

seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Mar 30, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Mar 30, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Mar 30, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 3, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 8, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 8, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 13, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 15, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 15, 2024
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 15, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants