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

Add Turbo stream morph action #1185

Merged

Conversation

omarluq
Copy link
Contributor

@omarluq omarluq commented Feb 15, 2024

This PR introduces a morph Turbo Stream action. The motivation for this addition stems from the recent discussions and suggestions in issue #1163. There is a clear interest in providing users with more flexible options for DOM manipulation, especially for those not ready or looking to fully transition to using Turbo's full page morphing capabilities.

turbo-rails PR hotwired/turbo-rails#583
documentation PR hotwired/turbo-site#169

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.

Hey @omarluq, thanks for the PR:

I think a morph stream action makes sense but I am not sure about those data- attributes. It's quite a new API the one we would be introducing. I'd aim to start with something simple, like what turbo-morph does, where it only supports a "children only" option via a regular attribute in the stream action, which matches morphdom. I'd start without any option here.

This would also need to be consistent with the events we dispatch during morphing, both in the events it emits and in how they let you cancel the operation. We should probably extract the morphing operation out to a common place we can reuse.

Finally, this would need its counterpart helper in turbo-rails and documentation in turbo-site.

src/core/drive/morph_renderer.js Outdated Show resolved Hide resolved
@omarluq omarluq force-pushed the omarluq/turbo-stream-morph-action branch from c3257f4 to 6a5743c Compare February 15, 2024 06:46
@omarluq
Copy link
Contributor Author

omarluq commented Feb 15, 2024

Hey @jorgemanrubia, thank you for the speedy review sir! I've dropped the cosmetic change 🫡.
Extracting the morph operation into a common place sound like a great idea; how about a helper function in util.js? open to other suggestions as well.
Also, I opened a related PR in turbo-rails, but there are a few specs that aren't passing. I plan to tackle those tomorrow along with the changes you suggested to simplify the API! Ty much 🙏🏻

@jorgemanrubia
Copy link
Member

Extracting the morph operation into a common place sound like a great idea; how about a helper function in util.js? open to other suggestions as well.

Checking the code, this might not be the best path, because the current renderer has some parts that are tied to a Turbo page rendering operation. I'd see how the code looks without doing this first.

@omarluq
Copy link
Contributor Author

omarluq commented Feb 15, 2024

Extracting the morph operation into a common place sound like a great idea; how about a helper function in util.js? open to other suggestions as well.

Checking the code, this might not be the best path, because the current renderer has some parts that are tied to a Turbo page rendering operation. I'd see how the code looks without doing this first.

Just to make sure I'm getting this correctly, you are suggesting we implement the entire thing in stream_action.js and not worry about duplicate logic?

@omarluq omarluq force-pushed the omarluq/turbo-stream-morph-action branch 7 times, most recently from 48ce6ca to b8d1d8b Compare February 17, 2024 18:45
@omarluq
Copy link
Contributor Author

omarluq commented Feb 17, 2024

Hey @jorgemanrubia, I believe I've addressed all of your feedback. Additionally, I added tests for the data-turbo-morph-style="innerHTML" option. I would appreciate a review. 🫡

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.

Thanks for your work here @omarluq. This is looking better. Please, consider the suggestions below.

src/core/streams/stream_actions.js Outdated Show resolved Hide resolved
src/core/streams/stream_actions.js Outdated Show resolved Hide resolved
src/core/streams/stream_actions.js Outdated Show resolved Hide resolved
src/tests/unit/stream_element_tests.js Show resolved Hide resolved
@omarluq omarluq force-pushed the omarluq/turbo-stream-morph-action branch 3 times, most recently from d52b41a to 1e6e771 Compare February 20, 2024 16:39
@omarluq omarluq force-pushed the omarluq/turbo-stream-morph-action branch from 1e6e771 to 276ee38 Compare March 11, 2024 20:50
Copy link
Collaborator

@afcapel afcapel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Just a minor formatting issue.

@omarluq omarluq force-pushed the omarluq/turbo-stream-morph-action branch from f320a28 to f02bfb2 Compare March 14, 2024 14:24
@omarluq omarluq requested a review from afcapel March 14, 2024 14:26
@jorgemanrubia jorgemanrubia merged commit 600203e into hotwired:main Mar 14, 2024
1 check passed
@jorgemanrubia
Copy link
Member

Thanks @omarluq, very nice addition!

Comment on lines +5 to +62
const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML"
streamElement.targetElements.forEach((element) => {
Idiomorph.morph(element, streamElement.templateContent, {
morphStyle: morphStyle,
callbacks: {
beforeNodeAdded,
beforeNodeMorphed,
beforeAttributeUpdated,
beforeNodeRemoved,
afterNodeMorphed
}
})
})
}

function beforeNodeAdded(node) {
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
}

function beforeNodeRemoved(node) {
return beforeNodeAdded(node)
}

function beforeNodeMorphed(target, newElement) {
if (target instanceof HTMLElement && !target.hasAttribute("data-turbo-permanent")) {
const event = dispatch("turbo:before-morph-element", {
cancelable: true,
target,
detail: {
newElement
}
})
return !event.defaultPrevented
}
return false
}

function beforeAttributeUpdated(attributeName, target, mutationType) {
const event = dispatch("turbo:before-morph-attribute", {
cancelable: true,
target,
detail: {
attributeName,
mutationType
}
})
return !event.defaultPrevented
}

function afterNodeMorphed(target, newElement) {
if (newElement instanceof HTMLElement) {
dispatch("turbo:morph-element", {
target,
detail: {
newElement
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorgemanrubia duplicating the MorphRenderer logic feels like a big risk. Would generalizing the details of how we invoke Idiomorph (through something like #1192) help here?

Copy link
Contributor Author

@omarluq omarluq Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lagree with @seanpdoyle I think morph_elements.js can be a drop in replacement for the stream action morph implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanpdoyle the problem is that the current implementation of the page renderer has logic interweaved to deal with morphing frames. I think we need to untangle that in #1192, but I still don't see a clear separation there, and I haven't had proper time for looking into that yet. Agree that, ultimately, we should invoke the same morphing logic from everywhere, but we need to do some deep refactor to accommodate that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to deal with morphing frames

How does the exclusion of the Frame handling logic from the MorphRenderer impact the Stream's behavior?

Are there downsides to handling Frames consistently across Drive navigation morphs and Stream morphs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorgemanrubia In response to #1185 (comment), I've opened #1234 to share the bulk of the morphing logic across Pages, Frames, and Streams.

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 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 4, 2024
This commit re-structures the new support for
`turbo-stream[action="morph"]` elements introduced in [hotwired#1185][].

As an alternative to introduce a new Stream Action, this commit changes
existing actions to be more flexible.

For example, the `<turbo-stream method="morph">` element behaves like a
specialized version of a `<turbo-stream method="replace">`, since it
operates on the target element's `outerHTML` property.

Similarly, the `<turbo-stream method="morph" children-only>` element
behaves like a specialized version of a `<turbo-stream
method="update">`, since it operates on the target element's `innerHTML`
property.

This commit removes the `[action="morph"]` support entirely, and
re-implements it in terms of the `[action="replace"]` and
`[action="update"]` support.

By consolidating concepts, the "scope" of the modifications is more
clearly communicated to callers that are familiar with the underlying
DOM interfaces (`Element.replaceWith` and `Element.innerHTML`) that are
invoked by the conventionally established Replace and Update actions.

This proposal also aims to reinforce the "method" terminology introduced
by the Page Refresh `<meta name="refresh-method" content="morph">`
element.

[hotwired#1185]: hotwired#1185
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Apr 4, 2024
This commit re-structures the new support for
`turbo-stream[action="morph"]` elements introduced in [hotwired#1185][].

Since the `<turbo-stream action="morph">` hasn't yet been part of a
release, there's an opportunity to rename it without being considerate
of backwards compatibility.

As an alternative to introduce a new Stream Action, this commit changes
existing actions to be more flexible.

For example, the `<turbo-stream method="morph">` element behaves like a
specialized version of a `<turbo-stream method="replace">`, since it
operates on the target element's `outerHTML` property.

Similarly, the `<turbo-stream method="morph" children-only>` element
behaves like a specialized version of a `<turbo-stream
method="update">`, since it operates on the target element's `innerHTML`
property.

```diff
-<turbo-stream action="morph">
+<turbo-stream action="replace" method="morph">
   <template>Replace me with morphing</template>
 </turbo-stream>

-<turbo-stream action="morph" children-only>
+<turbo-stream action="update" method="morph">
   <template>Update me with morphing</template>
 </turbo-stream>
```

This commit removes the `[action="morph"]` support entirely, and
re-implements it in terms of the `[action="replace"]` and
`[action="update"]` support.

By consolidating concepts, the "scope" of the modifications is more
clearly communicated to callers that are familiar with the underlying
DOM interfaces (`Element.replaceWith` and `Element.innerHTML`) that are
invoked by the conventionally established Replace and Update actions.

This proposal also aims to reinforce the "method" terminology introduced
by the Page Refresh `<meta name="refresh-method" content="morph">`
element.

[hotwired#1185]: hotwired#1185
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Jul 3, 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 Jul 3, 2024
This commit re-structures the new support for
`turbo-stream[action="morph"]` elements introduced in [hotwired#1185][].

Since the `<turbo-stream action="morph">` hasn't yet been part of a
release, there's an opportunity to rename it without being considerate
of backwards compatibility.

As an alternative to introduce a new Stream Action, this commit changes
existing actions to be more flexible.

For example, the `<turbo-stream method="morph">` element behaves like a
specialized version of a `<turbo-stream method="replace">`, since it
operates on the target element's `outerHTML` property.

Similarly, the `<turbo-stream method="morph" children-only>` element
behaves like a specialized version of a `<turbo-stream
method="update">`, since it operates on the target element's `innerHTML`
property.

```diff
-<turbo-stream action="morph">
+<turbo-stream action="replace" method="morph">
   <template>Replace me with morphing</template>
 </turbo-stream>

-<turbo-stream action="morph" children-only>
+<turbo-stream action="update" method="morph">
   <template>Update me with morphing</template>
 </turbo-stream>
```

This commit removes the `[action="morph"]` support entirely, and
re-implements it in terms of the `[action="replace"]` and
`[action="update"]` support.

By consolidating concepts, the "scope" of the modifications is more
clearly communicated to callers that are familiar with the underlying
DOM interfaces (`Element.replaceWith` and `Element.innerHTML`) that are
invoked by the conventionally established Replace and Update actions.

This proposal also aims to reinforce the "method" terminology introduced
by the Page Refresh `<meta name="refresh-method" content="morph">`
element.

[hotwired#1185]: hotwired#1185
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Jul 12, 2024
This commit re-structures the new support for
`turbo-stream[action="morph"]` elements introduced in [hotwired#1185][].

Since the `<turbo-stream action="morph">` hasn't yet been part of a
release, there's an opportunity to rename it without being considerate
of backwards compatibility.

As an alternative to introduce a new Stream Action, this commit changes
existing actions to be more flexible.

For example, the `<turbo-stream method="morph">` element behaves like a
specialized version of a `<turbo-stream method="replace">`, since it
operates on the target element's `outerHTML` property.

Similarly, the `<turbo-stream method="morph" children-only>` element
behaves like a specialized version of a `<turbo-stream
method="update">`, since it operates on the target element's `innerHTML`
property.

```diff
-<turbo-stream action="morph">
+<turbo-stream action="replace" method="morph">
   <template>Replace me with morphing</template>
 </turbo-stream>

-<turbo-stream action="morph" children-only>
+<turbo-stream action="update" method="morph">
   <template>Update me with morphing</template>
 </turbo-stream>
```

This commit removes the `[action="morph"]` support entirely, and
re-implements it in terms of the `[action="replace"]` and
`[action="update"]` support.

By consolidating concepts, the "scope" of the modifications is more
clearly communicated to callers that are familiar with the underlying
DOM interfaces (`Element.replaceWith` and `Element.innerHTML`) that are
invoked by the conventionally established Replace and Update actions.

This proposal also aims to reinforce the "method" terminology introduced
by the Page Refresh `<meta name="refresh-method" content="morph">`
element.

[hotwired#1185]: hotwired#1185
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.

5 participants