From ade7649c737d5d97fc7d028d22bed7d0cda71873 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sat, 2 Dec 2023 19:07:35 -0500 Subject: [PATCH] Introduce `turbo:before-morph` and re-purpose `turbo:morph` Follow-up to [9944490][] Related to [#1083] Related to [@hotwired/turbo-rails#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 `` element as part of `MorphRenderer`'s lifecycle. That event will still be dispatched, since `` 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]: https://github.com/hotwired/turbo/pull/1019/commits/9944490a3c8aec0c5060401125cc8932e93a32df [#1083]: https://github.com/hotwired/turbo/issues/1083 [@hotwired/turbo-rails#533]: https://github.com/hotwired/turbo-rails/issues/533 --- src/core/drive/morph_renderer.js | 42 +++++++++++++---- src/tests/fixtures/page_refresh.html | 46 +++++++++++++++++- src/tests/fixtures/test.js | 2 + src/tests/functional/page_refresh_tests.js | 55 +++++++++++++++++++++- 4 files changed, 133 insertions(+), 12 deletions(-) diff --git a/src/core/drive/morph_renderer.js b/src/core/drive/morph_renderer.js index beb94bfb1..fcf54d6ea 100644 --- a/src/core/drive/morph_renderer.js +++ b/src/core/drive/morph_renderer.js @@ -16,13 +16,6 @@ export class MorphRenderer extends Renderer { async #morphBody() { this.#morphElements(this.currentElement, this.newElement) this.#reloadRemoteFrames() - - dispatch("turbo:morph", { - detail: { - currentElement: this.currentElement, - newElement: this.newElement - } - }) } #morphElements(currentElement, newElement, morphStyle = "outerHTML") { @@ -33,7 +26,9 @@ export class MorphRenderer extends Renderer { callbacks: { beforeNodeAdded: this.#shouldAddElement, beforeNodeMorphed: this.#shouldMorphElement, - beforeNodeRemoved: this.#shouldRemoveElement + beforeAttributeUpdated: this.#shouldUpdateAttribute, + beforeNodeRemoved: this.#shouldRemoveElement, + afterNodeMorphed: this.#didMorphElement } }) } @@ -44,12 +39,41 @@ export class MorphRenderer extends Renderer { #shouldMorphElement = (oldNode, newNode) => { if (oldNode instanceof HTMLElement) { - return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode)) + if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) { + const event = dispatch("turbo:before-morph", { + cancelable: true, + target: oldNode, + detail: { + newElement: newNode + } + }) + + return !event.defaultPrevented + } else { + return false + } } else { return true } } + #shouldUpdateAttribute = (attributeName, target, mutationType) => { + const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } }) + + return !event.defaultPrevented + } + + #didMorphElement = (oldNode, newNode) => { + if (newNode instanceof HTMLElement) { + dispatch("turbo:morph", { + target: oldNode, + detail: { + newElement: newNode + } + }) + } + } + #shouldRemoveElement = (node) => { return this.#shouldMorphElement(node) } diff --git a/src/tests/fixtures/page_refresh.html b/src/tests/fixtures/page_refresh.html index d9523c6b1..42e8153c4 100644 --- a/src/tests/fixtures/page_refresh.html +++ b/src/tests/fixtures/page_refresh.html @@ -9,6 +9,45 @@ +