Skip to content

Commit 8162495

Browse files
committed
Re-structure turbo-stream[action=morph] support
This commit re-structures the new support for `turbo-stream[action="morph"]` elements introduced in [#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. [#1185]: #1185
1 parent 14284e6 commit 8162495

File tree

7 files changed

+93
-93
lines changed

7 files changed

+93
-93
lines changed

src/core/streams/actions/morph.js

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { Idiomorph } from "idiomorph/dist/idiomorph.esm"
22
import { dispatch } from "../../../util"
33

4-
export default function morph(streamElement) {
5-
const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML"
6-
streamElement.targetElements.forEach((element) => {
7-
Idiomorph.morph(element, streamElement.templateContent, {
8-
morphStyle: morphStyle,
9-
callbacks: {
10-
beforeNodeAdded,
11-
beforeNodeMorphed,
12-
beforeAttributeUpdated,
13-
beforeNodeRemoved,
14-
afterNodeMorphed
15-
}
16-
})
4+
export function morphElement(target, element) {
5+
idiomorph(target, element, { morphStyle: "outerHTML" })
6+
}
7+
8+
export function morphChildren(target, childElements) {
9+
idiomorph(target, childElements, { morphStyle: "innerHTML" })
10+
}
11+
12+
function idiomorph(target, element, options = {}) {
13+
Idiomorph.morph(target, element, {
14+
...options,
15+
callbacks: {
16+
beforeNodeAdded,
17+
beforeNodeMorphed,
18+
beforeAttributeUpdated,
19+
beforeNodeRemoved,
20+
afterNodeMorphed
21+
}
1722
})
1823
}
1924

src/core/streams/stream_actions.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { session } from "../"
2-
import morph from "./actions/morph"
2+
import { morphChildren, morphElement } from "./actions/morph"
33

44
export const StreamActions = {
55
after() {
@@ -25,21 +25,31 @@ export const StreamActions = {
2525
},
2626

2727
replace() {
28-
this.targetElements.forEach((e) => e.replaceWith(this.templateContent))
28+
const method = this.getAttribute("method")
29+
30+
this.targetElements.forEach((targetElement) => {
31+
if (method === "morph") {
32+
morphElement(targetElement, this.templateContent)
33+
} else {
34+
targetElement.replaceWith(this.templateContent)
35+
}
36+
})
2937
},
3038

3139
update() {
40+
const method = this.getAttribute("method")
41+
3242
this.targetElements.forEach((targetElement) => {
33-
targetElement.innerHTML = ""
34-
targetElement.append(this.templateContent)
43+
if (method === "morph") {
44+
morphChildren(targetElement, this.templateContent)
45+
} else {
46+
targetElement.innerHTML = ""
47+
targetElement.append(this.templateContent)
48+
}
3549
})
3650
},
3751

3852
refresh() {
3953
session.refresh(this.baseURI, this.requestId)
40-
},
41-
42-
morph() {
43-
morph(this)
4454
}
4555
}

src/tests/fixtures/morph_stream_action.html

-16
This file was deleted.

src/tests/fixtures/stream.html

+4
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,9 @@
3939
<div id="container">
4040
<input id="container-element">
4141
</div>
42+
43+
<div id="message_1">
44+
<div>Morph me</div>
45+
</div>
4246
</body>
4347
</html>

src/tests/functional/morph_stream_action_tests.js

-48
This file was deleted.

src/tests/functional/stream_tests.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { test } from "@playwright/test"
1+
import { expect, test } from "@playwright/test"
22
import { assert } from "chai"
33
import {
44
hasSelector,
55
nextBeat,
66
nextEventNamed,
7+
nextEventOnTarget,
8+
noNextEventOnTarget,
79
readEventLogs,
810
waitUntilNoSelector,
911
waitUntilText
@@ -182,6 +184,48 @@ test("receiving a remove stream message preserves focus blurs the activeElement"
182184
assert.notOk(await hasSelector(page, ":focus"))
183185
})
184186

187+
test("dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action", async ({ page }) => {
188+
await page.evaluate(() => {
189+
window.Turbo.renderStreamMessage(`
190+
<turbo-stream action="replace" method="morph" target="message_1">
191+
<template>
192+
<div id="message_1">
193+
<h1>Morphed</h1>
194+
</div>
195+
</template>
196+
</turbo-stream>
197+
`)
198+
})
199+
200+
await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
201+
await nextEventOnTarget(page, "message_1", "turbo:morph-element")
202+
await expect(page.locator("#message_1")).toHaveText("Morphed")
203+
})
204+
205+
test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => {
206+
await page.evaluate(() => {
207+
addEventListener("turbo:before-morph-element", (event) => {
208+
event.preventDefault()
209+
})
210+
})
211+
212+
await page.evaluate(() => {
213+
window.Turbo.renderStreamMessage(`
214+
<turbo-stream action="replace" method="morph" target="message_1">
215+
<template>
216+
<div id="message_1">
217+
<h1>Morphed</h1>
218+
</div>
219+
</template>
220+
</turbo-stream>
221+
`)
222+
})
223+
224+
await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
225+
await noNextEventOnTarget(page, "message_1", "turbo:morph-element")
226+
await expect(page.locator("#message_1")).toHaveText("Morph me")
227+
})
228+
185229
async function getReadyState(page, id) {
186230
return page.evaluate((id) => {
187231
const element = document.getElementById(id)

src/tests/unit/stream_element_tests.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { assert } from "@open-wc/testing"
55
import { sleep } from "../helpers/page"
66
import * as Turbo from "../../index"
77

8-
function createStreamElement(action, target, templateElement) {
8+
function createStreamElement(action, target, templateElement, attributes = {}) {
99
const element = new StreamElement()
1010
if (action) element.setAttribute("action", action)
1111
if (target) element.setAttribute("target", target)
1212
if (templateElement) element.appendChild(templateElement)
13+
Object.entries(attributes).forEach((attribute) => element.setAttribute(...attribute))
1314
return element
1415
}
1516

@@ -197,9 +198,9 @@ test("test action=refresh discarded when matching request id", async () => {
197198
assert.ok(document.body.hasAttribute("data-modified"))
198199
})
199200

200-
test("action=morph", async () => {
201+
test("action=replace method=morph", async () => {
201202
const templateElement = createTemplateElement(`<h1 id="hello">Hello Turbo Morphed</h1>`)
202-
const element = createStreamElement("morph", "hello", templateElement)
203+
const element = createStreamElement("replace", "hello", templateElement, { method: "morph" })
203204

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

@@ -210,9 +211,9 @@ test("action=morph", async () => {
210211
assert.equal(subject.find("h1#hello")?.textContent, "Hello Turbo Morphed")
211212
})
212213

213-
test("action=morph with text content change", async () => {
214+
test("action=replace method=morph with text content change", async () => {
214215
const templateElement = createTemplateElement(`<div id="hello">Hello Turbo Morphed</div>`)
215-
const element = createStreamElement("morph", "hello", templateElement)
216+
const element = createStreamElement("replace", "hello", templateElement, { method: "morph" })
216217

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

@@ -223,9 +224,9 @@ test("action=morph with text content change", async () => {
223224
assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo Morphed")
224225
})
225226

226-
test("action=morph children-only", async () => {
227+
test("action=update method=morph", async () => {
227228
const templateElement = createTemplateElement(`<h1 id="hello-child-element">Hello Turbo Morphed</h1>`)
228-
const element = createStreamElement("morph", "hello", templateElement)
229+
const element = createStreamElement("update", "hello", templateElement, { method: "morph" })
229230
const target = subject.find("div#hello")
230231
assert.equal(target?.textContent, "Hello Turbo")
231232
element.setAttribute("children-only", true)

0 commit comments

Comments
 (0)