Skip to content

Instantly share code, notes, and snippets.

@pfeiffer
Last active February 26, 2024 16:34
Show Gist options
  • Save pfeiffer/54e3bf929637cad42d534c84f65b1e99 to your computer and use it in GitHub Desktop.
Save pfeiffer/54e3bf929637cad42d534c84f65b1e99 to your computer and use it in GitHub Desktop.
Turbo MorphRenderer
import { morphElement } from '../../helpers'
import MorphableSnapshot from './morphable_snapshot'
// A Renderer class that will extract morphable elements tagged with [data-turbo-morph] before
// rendering, and morph them back into the new snapshot after rendering, similar to how permanent
// elements are handled by Turbo. Useful for persisting horizontal scroll etc.
//
// Note: A morphable element must have an [id] attribute.
class MorphRenderer {
constructor(existingBody, newBody) {
this.existingSnapshot = new MorphableSnapshot(existingBody)
this.newSnapshot = new MorphableSnapshot(newBody)
}
render(originalRender) {
const morphableElementsMap = this.existingSnapshot.getMorphableElementsMapForSnapshot(this.newSnapshot)
this.moveExistingElementToNewSnapshot(morphableElementsMap)
originalRender(this.existingSnapshot.element, this.newSnapshot.element)
this.morphNewElementsAfterRender(morphableElementsMap)
}
moveExistingElementToNewSnapshot(morphableElementsMap) {
for (const [_, { currentElement, newElement }] of Object.entries(morphableElementsMap)) {
const clone = currentElement.cloneNode(true)
currentElement.replaceWith(clone)
newElement.replaceWith(currentElement)
}
}
morphNewElementsAfterRender(morphableElementsMap) {
for (const [_, { currentElement, newElementHTML }] of Object.entries(morphableElementsMap)) {
morphElement(currentElement, newElementHTML)
}
}
}
export default MorphRenderer
// A Snapshot class that will extract morphable elements tagged with [data-turbo-morph] and [id]
// and allow generating a map of elements that are morphable between snapshots.
class MorphableSnapshot {
constructor(element) {
this.element = element
}
get morphableElements() {
return [...this.element.querySelectorAll("[id][data-turbo-morph]")]
}
getMorphableElementById(id) {
return this.element.querySelector(`#${id}[data-turbo-morph]`)
}
getMorphableElementsMapForSnapshot(snapshot) {
const morphableElementsMap = {}
for (const morphableCurrentElement of this.morphableElements) {
const { id } = morphableCurrentElement
const newMorphableElement = snapshot.getMorphableElementById(id)
if (newMorphableElement) {
morphableElementsMap[id] = {
currentElement: morphableCurrentElement,
newElement: newMorphableElement,
newElementHTML: newMorphableElement.outerHTML,
}
}
}
return morphableElementsMap
}
}
export default MorphableSnapshot
import MorphRenderer from "../lib/turbo/morph_renderer"
addEventListener("turbo:before-render", ({ detail }) => {
const originalRender = detail.render
detail.render = async (existingBody, newBody) => {
const renderer = new MorphRenderer(existingBody, newBody)
renderer.render(originalRender)
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment