Skip to content

Commit

Permalink
Merge pull request hotwired#1185 from omarluq/omarluq/turbo-stream-mo…
Browse files Browse the repository at this point in the history
…rph-action

Add Turbo stream morph action
  • Loading branch information
jorgemanrubia authored Mar 14, 2024
2 parents 1cd6271 + f02bfb2 commit 600203e
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/core/streams/actions/morph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Idiomorph } from "idiomorph/dist/idiomorph.esm"
import { dispatch } from "../../../util"

export default function morph(streamElement) {
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
}
})
}
}
5 changes: 5 additions & 0 deletions src/core/streams/stream_actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { session } from "../"
import morph from "./actions/morph"

export const StreamActions = {
after() {
Expand Down Expand Up @@ -36,5 +37,9 @@ export const StreamActions = {

refresh() {
session.refresh(this.baseURI, this.requestId)
},

morph() {
morph(this)
}
}
16 changes: 16 additions & 0 deletions src/tests/fixtures/morph_stream_action.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html id="html">
<head>
<meta charset="utf-8">
<title>Morph Stream Action</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
<meta name="turbo-refresh-method" content="replace">
</head>

<body>
<div id="message_1">
<div>Morph me</div>
</div>
</body>
</html>
48 changes: 48 additions & 0 deletions src/tests/functional/morph_stream_action_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test, expect } from "@playwright/test"
import { nextEventOnTarget, noNextEventOnTarget } from "../helpers/page"

test("dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action", async ({ page }) => {
await page.goto("/src/tests/fixtures/morph_stream_action.html")

await page.evaluate(() => {
window.Turbo.renderStreamMessage(`
<turbo-stream action="morph" target="message_1">
<template>
<div id="message_1">
<h1>Morphed</h1>
</div>
</template>
</turbo-stream>
`)
})

await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
await nextEventOnTarget(page, "message_1", "turbo:morph-element")
await expect(page.locator("#message_1")).toHaveText("Morphed")
})

test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => {
await page.goto("/src/tests/fixtures/morph_stream_action.html")

await page.evaluate(() => {
addEventListener("turbo:before-morph-element", (event) => {
event.preventDefault()
})
})

await page.evaluate(() => {
window.Turbo.renderStreamMessage(`
<turbo-stream action="morph" target="message_1">
<template>
<div id="message_1">
<h1>Morphed</h1>
</div>
</template>
</turbo-stream>
`)
})

await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
await noNextEventOnTarget(page, "message_1", "turbo:morph-element")
await expect(page.locator("#message_1")).toHaveText("Morph me")
})
29 changes: 29 additions & 0 deletions src/tests/unit/stream_element_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,32 @@ test("test action=refresh discarded when matching request id", async () => {

assert.ok(document.body.hasAttribute("data-modified"))
})

test("action=morph", async () => {
const templateElement = createTemplateElement(`<h1 id="hello">Hello Turbo Morphed</h1>`)
const element = createStreamElement("morph", "hello", templateElement)

assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo")

subject.append(element)
await nextAnimationFrame()

assert.notOk(subject.find("div#hello"))
assert.equal(subject.find("h1#hello")?.textContent, "Hello Turbo Morphed")
})

test("action=morph children-only", async () => {
const templateElement = createTemplateElement(`<h1 id="hello-child-element">Hello Turbo Morphed</h1>`)
const element = createStreamElement("morph", "hello", templateElement)
const target = subject.find("div#hello")
assert.equal(target?.textContent, "Hello Turbo")
element.setAttribute("children-only", true)

subject.append(element)

await nextAnimationFrame()

assert.ok(subject.find("div#hello"))
assert.ok(subject.find("div#hello > h1#hello-child-element"))
assert.equal(subject.find("div#hello > h1#hello-child-element").textContent, "Hello Turbo Morphed")
})

0 comments on commit 600203e

Please sign in to comment.