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
Morphing: Changes from JS libs that mutate the DOM are lost #1083
Comments
What happens if you listen to the turbo:morph event to re-initialize it? |
Hmm, it does! The solution looks like: <select
data-controller="tom-select"
data-action="turbo:morph@window->tom-select#reconnect"
> Where A) Unless I'm crazy (but I checked pretty closely), B) It may be trick, but it would be more useful the Thanks! |
This is because it is rendering the cached preview and then the response coming from the server. That's why the first time you only see one event and the subsequent times two.
From the Turbo side, dispatching an event from the element that has been morphed is entirely possible, I don't think it is the responsibility of Stimulus. I guess we would have to know the opinion of the maintainers. In the meantime, you could have an observer which do the job, something like: // app/javascript/controllers/tom_select_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
initialize() {
this.mutationObserver = new MutationObserver(this.handleMutation.bind(this))
}
connect() {
this.mutationObserver.observe(this.element, { childList: true, subtree: true })
}
disconnect() {
this.mutationObserver.disconnect()
}
handleMutation(mutations, observer) {
for (const mutation of mutations) {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
this.reconnect()
}
}
}
reconnect() {
console.log('Element is changed. Reconnecting...')
// Add your reconnect logic here
}
} |
Follow-up to [9944490][] Related to [hotwired#1083] Related to [@hotwired/turbo-railshotwired#533][] The problem --- Some client-side plugins are losing their state when elements are morphed. Without resorting to `MutationObserver` instances to determine when a node is morphed, uses of those plugins don't have the ability to prevent (without `[data-turbo-permanent]`) or respond to the morphing. The proposal --- This commit introduces a `turbo:before-morph` event that'll dispatch as part of the Idiomorph `beforeNodeMorphed` callback. It'll give interested parties access to the nodes before and after a morph. If that event is cancelled via `event.preventDefault()`, it'll skip the morph as if the element were marked with `[data-turbo-permanent]`. Similarly, this commit re-purposes the new `turbo:morph` event to be dispatched for every morphed node (via Idiomorph's `afterNodeMorphed` callback). The original implementation dispatched the event for the `<body>` element as part of `MorphRenderer`'s lifecycle. That event will still be dispatched, since `<body>` is the first element the callback will fire for. In addition to that event, each individual morphed node will dispatch one. This commit re-introduced test coverage for a Stimulus controller to demonstrate how an interested party might respond. It isn't immediately clear with that code should live, but once we iron out the details, it could be part of a `@hotwired/turbo/stimulus` package, or a `@hotwired/stimulus/turbo` package that users (or `@hotwired/turbo-rails`) could opt-into. [9944490]: hotwired@9944490 [hotwired#1083]: hotwired#1083 [@hotwired/turbo-railshotwired#533]: hotwired/turbo-rails#533
Come to think of it, @seanpdoyle did a PR to Stimulus where he added callbacks when an element and targets changed. I think that might be something we should rescue, more so now with morphing. It is very common to use observers to execute actions and this could simplify it a lot. The example above is the best proof of it. What do you think? I would love to see that PR merged. |
Follow-up to [9944490][] Related to [hotwired#1083] Related to [@hotwired/turbo-railshotwired#533][] The problem --- Some client-side plugins are losing their state when elements are morphed. Without resorting to `MutationObserver` instances to determine when a node is morphed, uses of those plugins don't have the ability to prevent (without `[data-turbo-permanent]`) or respond to the morphing. The proposal --- This commit introduces a `turbo:before-morph` event that'll dispatch as part of the Idiomorph `beforeNodeMorphed` callback. It'll give interested parties access to the nodes before and after a morph. If that event is cancelled via `event.preventDefault()`, it'll skip the morph as if the element were marked with `[data-turbo-permanent]`. Similarly, this commit re-purposes the new `turbo:morph` event to be dispatched for every morphed node (via Idiomorph's `afterNodeMorphed` callback). The original implementation dispatched the event for the `<body>` element as part of `MorphRenderer`'s lifecycle. That event will still be dispatched, since `<body>` is the first element the callback will fire for. In addition to that event, each individual morphed node will dispatch one. This commit re-introduced test coverage for a Stimulus controller to demonstrate how an interested party might respond. It isn't immediately clear with that code should live, but once we iron out the details, it could be part of a `@hotwired/turbo/stimulus` package, or a `@hotwired/stimulus/turbo` package that users (or `@hotwired/turbo-rails`) could opt-into. [9944490]: hotwired@9944490 [hotwired#1083]: hotwired#1083 [@hotwired/turbo-railshotwired#533]: hotwired/turbo-rails#533
I ran into a similar issue where I was showing/hiding form fields based on a selected radio button value. I relied on the stimulus To resolve this, I did as the above posts recommended:
This works, and I could probably clean it up a bit by making a But I want to make sure there's not a more conventional/official solution that the hotwire/turbo team is presenting? Is there a way to have per-page morphing? It would seem that forms that fail validation and re-render the form is a prime candidate for situations where the DOM has been manipulated client-side, and morphing from the server version will wipe those changes. My stimulus controllers have been written the past few years with the assumption that the But I'm extremely stoked to see morphing being added! I was close to implementing my own solution a few months back until I saw the team was baking this in. Very excited, just want to make sure all my cool toys play nicely together :) |
FYI @seanpdoyle's PR, #1097, should expose fine-grain controls that would give us better control and should fix this issue 👍 |
I'm curious what about script tags? Usually when a body is replaced script tags are re-evaluated, but with morph this does not happen. If a javascript library modifies the DOM, like say, a script injects the debugbar html, the bar is removed on morph but never re-added because the existing script tag is morphed but not re-evaluated. |
Follow-up to [9944490][] Related to [hotwired#1083] Related to [@hotwired/turbo-railshotwired#533][] The problem --- Some client-side plugins are losing their state when elements are morphed. Without resorting to `MutationObserver` instances to determine when a node is morphed, uses of those plugins don't have the ability to prevent (without `[data-turbo-permanent]`) or respond to the morphing. The proposal --- This commit introduces a `turbo:before-morph` event that'll dispatch as part of the Idiomorph `beforeNodeMorphed` callback. It'll give interested parties access to the nodes before and after a morph. If that event is cancelled via `event.preventDefault()`, it'll skip the morph as if the element were marked with `[data-turbo-permanent]`. Along with `turbo:before-morph`, this commit also introduces a `turbo:before-morph-attribute` to correspond to the `beforeAttributeUpdated` callback that Idiomorph provides. When listeners (like an `HTMLDetailsElement`, an `HTMLDialogElement`, or a Stimulus controller) want to preserve the state of an attribute, they can cancel the `turbo:before-morph-attribute` event that corresponds with the attribute name (through `event.detail.attributeName`). Similarly, this commit re-purposes the new `turbo:morph` event to be dispatched for every morphed node (via Idiomorph's `afterNodeMorphed` callback). The original implementation dispatched the event for the `<body>` element as part of `MorphRenderer`'s lifecycle. That event will still be dispatched, since `<body>` is the first element the callback will fire for. In addition to that event, each individual morphed node will dispatch one. This commit re-introduced test coverage for a Stimulus controller to demonstrate how an interested party might respond. It isn't immediately clear with that code should live, but once we iron out the details, it could be part of a `@hotwired/turbo/stimulus` package, or a `@hotwired/stimulus/turbo` package that users (or `@hotwired/turbo-rails`) could opt-into. [9944490]: hotwired@9944490 [hotwired#1083]: hotwired#1083 [@hotwired/turbo-railshotwired#533]: hotwired/turbo-rails#533
Follow-up to [9944490][] Related to [hotwired#1083] Related to [@hotwired/turbo-railshotwired#533][] The problem --- Some client-side plugins are losing their state when elements are morphed. Without resorting to `MutationObserver` instances to determine when a node is morphed, uses of those plugins don't have the ability to prevent (without `[data-turbo-permanent]`) or respond to the morphing. The proposal --- This commit introduces a `turbo:before-morph-element` event that'll dispatch as part of the Idiomorph `beforeNodeMorphed` callback. It'll give interested parties access to the nodes before and after a morph. If that event is cancelled via `event.preventDefault()`, it'll skip the morph as if the element were marked with `[data-turbo-permanent]`. Along with `turbo:before-morph-element`, this commit also introduces a `turbo:before-morph-attribute` to correspond to the `beforeAttributeUpdated` callback that Idiomorph provides. When listeners (like an `HTMLDetailsElement`, an `HTMLDialogElement`, or a Stimulus controller) want to preserve the state of an attribute, they can cancel the `turbo:before-morph-attribute` event that corresponds with the attribute name (through `event.detail.attributeName`). Similarly, this commit adds a new `turbo:morph-element` event to be dispatched for every morphed node (via Idiomorph's `afterNodeMorphed` callback). The original implementation dispatched the event for the `<body>` element as part of `MorphRenderer`'s lifecycle. That event will still be dispatched, since `<body>` is the first element the callback will fire for. In addition to that event, each individual morphed node will dispatch one. This commit re-introduced test coverage for a Stimulus controller to demonstrate how an interested party might respond. It isn't immediately clear with that code should live, but once we iron out the details, it could be part of a `@hotwired/turbo/stimulus` package, or a `@hotwired/stimulus/turbo` package that users (or `@hotwired/turbo-rails`) could opt-into. [9944490]: hotwired@9944490 [hotwired#1083]: hotwired#1083 [@hotwired/turbo-railshotwired#533]: hotwired/turbo-rails#533
Hi!
Thank you for adding morphing! ❤️. I maintain the Symfony's LiveComponents package where we also morph. We frequently hit a problem with morphing + Stimulus that Turbo morphing also hits.
Reproducer: https://github.com/weaverryan/turbo-morph-tom-select-reproducer
tl;dr
The result is that the rich TomSelect widget is lost. This is because TomSelect works by modifying the
select
(e.g. adding classes) and adding an entirely new<div>
element after theselect
:So, naturally, when the morph happens, the
select
element is reverted and the new<div>
is lost entirely. However, because the<select data-controller="tom-select">
element was not removed and re-added to the page,disconnect()
andconnect()
are not called again. Also, no Stimulus values were changed and no targets (if we added a target to theselect
) are added/removed.The result is that we lose the TomSelect widget and there's no hook to reinitialize it.
Possible Solutions
Is there a solution for this? Or any thoughts? In Symfony LiveComponents, we have a complex MutationObserver system to track changes. But the correct solution would be something much simpler. Ideas:
A) Do we need to switch to a paradigm where we avoid JavaScript packages that mutate the DOM?
B) I know it #1019, there was temporarily a way for a Stimulus controller to "reconnect" when morphed. Is that needed?
C) Or, do we need some way to be able to mark an element to NOT be morphed (permanent)? This is actually not a great solution, as, in this case, you need to be aware of and find the new elements added by TomSelect and manually add some attribute (e.g.
data-turbo-permanent
). And if a Stimulus value did change and we did want to reinitialize TomSelect after a morph, we might need to manually destroy the<div>
adde by TomSelect.Note: TomSelect is an especially annoying/messy library. But from experience, this "JavaScript mutated the DOM and that was lost after morphing" will be a common issue.
Thanks and apologies if I've missed some thoughts on this already!
The text was updated successfully, but these errors were encountered: