Skip to content

Commit

Permalink
Merge pull request #1097 from seanpdoyle/before-morph-and-morph-events
Browse files Browse the repository at this point in the history
Introduce `turbo:{before-,}morph-{element,attribute}` events
  • Loading branch information
jorgemanrubia committed Jan 29, 2024
2 parents 617e6d0 + abab1cf commit 67a191e
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 9 deletions.
37 changes: 33 additions & 4 deletions src/core/drive/morph_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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
}
})
}
Expand All @@ -44,9 +46,36 @@ export class MorphRenderer extends Renderer {

#shouldMorphElement = (oldNode, newNode) => {
if (oldNode instanceof HTMLElement) {
return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
} else {
return true
if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
const event = dispatch("turbo:before-morph-element", {
cancelable: true,
target: oldNode,
detail: {
newElement: newNode
}
})

return !event.defaultPrevented
} else {
return false
}
}
}

#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-element", {
target: oldNode,
detail: {
newElement: newNode
}
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class View {
await this.prepareToRenderSnapshot(renderer)

const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve))
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement }
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod }
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options)
if (!immediateRender) await renderInterception

Expand Down
46 changes: 44 additions & 2 deletions src/tests/fixtures/page_refresh.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
<script type="module">
import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"

const application = Application.start()

addEventListener("turbo:morph-element", ({ target }) => {
for (const { element, context } of application.controllers) {
if (element === target) {
context.disconnect()
context.connect()
}
}
})

addEventListener("turbo:before-morph-attribute", (event) => {
const { target, detail: { attributeName, mutationType } } = event

for (const { element, context } of application.controllers) {
const pattern = new RegExp(`data-${context.identifier}-\\w+-value`)

if (element === target) {
event.preventDefault()
}
}
})

application.register("test", class extends Controller {
static targets = ["output"]
static values = { state: String }

capture({ target }) {
this.stateValue = target.value
}

outputTargetConnected(target) {
target.textContent = "connected"
}
})
</script>

<style>
body {
Expand Down Expand Up @@ -42,15 +81,18 @@ <h2>Frame to be preserved</h2>
</turbo-frame>
</div>

<div id="stimulus-controller" data-controller="test">
<div id="stimulus-controller" data-controller="test" data-action="input->test#capture">
<h3>Element with Stimulus controller</h3>

<div id="test-output" data-test-target="output">reset</div>
<input>
</div>

<p><a id="replace-link" data-turbo-action="replace" href="/src/tests/fixtures/page_refresh.html?param=something">Link with params to refresh the page</a></p>
<p><a id="link" href="/src/tests/fixtures/one.html">Link to another page</a></p>

<form id="form" action="/__turbo/refresh" method="post" class="redirect">
<input type="text" name="text" value="">
<input id="form-text" type="text" name="text" value="">
<input type="hidden" name="path" value="/src/tests/fixtures/page_refresh.html">
<input type="hidden" name="sleep" value="50">
<input id="form-submit" type="submit" value="form[method=post]">
Expand Down
4 changes: 4 additions & 0 deletions src/tests/fixtures/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"turbo:frame-render",
"turbo:frame-missing",
"turbo:before-frame-morph",
"turbo:morph",
"turbo:before-morph-element",
"turbo:morph-element",
"turbo:before-morph-attribute",
"turbo:reload"
])

Expand Down
59 changes: 58 additions & 1 deletion src/tests/functional/page_refresh_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,67 @@ test("renders a page refresh with morphing", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

await page.click("#form-submit")
await nextEventNamed(page, "turbo:before-render", { renderMethod: "morph" })
await nextEventNamed(page, "turbo:render", { renderMethod: "morph" })
})

test("renders a page refresh with morphing when the paths are the same but search params are diferent", async ({ page }) => {
test("dispatches a turbo:before-morph-element and turbo:morph-element event for each morphed element", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")
await page.fill("#form-text", "Morph me")
await page.click("#form-submit")

await nextEventOnTarget(page, "form-text", "turbo:before-morph-element")
await nextEventOnTarget(page, "form-text", "turbo:morph-element")
})

test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => {
const input = await page.locator("#form-text")
const submit = await page.locator("#form-submit")

await page.goto("/src/tests/fixtures/page_refresh.html")
await input.evaluate((input) => input.addEventListener("turbo:before-morph-element", (event) => event.preventDefault()))
await input.fill("Morph me")
await submit.click()

await nextEventOnTarget(page, "form-text", "turbo:before-morph-element")
await noNextEventOnTarget(page, "form-text", "turbo:morph-element")

await expect(input).toHaveValue("Morph me")
})

test("turbo:morph-element Stimulus listeners can handle morphing", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

await expect(page.locator("#test-output")).toHaveText("connected")

await page.fill("#form-text", "Ignore me")
await page.click("#form-submit")

await expect(page.locator("#form-text")).toHaveValue("")
await expect(page.locator("#test-output")).toHaveText("connected")
})

test("turbo:before-morph-attribute Stimulus listeners can handle morphing attributes", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")
const controller = page.locator("#stimulus-controller")
const input = controller.locator("input")

await expect(page.locator("#test-output")).toHaveText("connected")

await input.fill("controller state")
await page.fill("#form-text", "Ignore me")
await page.click("#form-submit")

const { mutationType } = await nextEventOnTarget(page, "stimulus-controller", "turbo:before-morph-attribute", { attributeName: "data-test-state-value" })

await expect(mutationType).toEqual("update")
await expect(controller).toHaveAttribute("data-test-state-value", "controller state")
await expect(page.locator("#form-text")).toHaveValue("")
await expect(page.locator("#test-output")).toHaveText("connected")
})


test("renders a page refresh with morphing when the paths are the same but search params are different", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")

await page.click("#replace-link")
Expand Down
2 changes: 1 addition & 1 deletion src/tests/functional/rendering_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ test.beforeEach(async ({ page }) => {

test("triggers before-render and render events", async ({ page }) => {
await page.click("#same-origin-link")
const { newBody } = await nextEventNamed(page, "turbo:before-render")
const { newBody } = await nextEventNamed(page, "turbo:before-render", { renderMethod: "replace" })

assert.equal(await page.textContent("h1"), "One")

Expand Down

0 comments on commit 67a191e

Please sign in to comment.