Custom Actions: Export StreamActions module - #479
Conversation
|
This is a draft because a feature like this is not entirely free. By opening up the innards of Turbo's support for Similarly, since the On the plus side, this grants applications more fine-grained control to expand the suite of Stream capabilities, or change existing actions to better suit their needs. This might also close issues like #474, since applications can fire whatever events they desire from within the actions. |
cafc827 to
f2aceb1
Compare
|
Instead of exporting the module, a different variation of this change could involve dispatching the That would align with the pattern that #431 is aiming to establish. |
I like the idea of having the stream actions being exported so that we can have additional stream actions defined inside our applications. Then it would be okay to use |
That's good feedback! I'm thinking about the developer experience in terms of when would be best to intervene. I'll share some pseudo-code to demonstrate the two styles. First, consider manipulating the import { Turbo } from "@hotwired/turbo"
const { update: originalUpdateAction } = Turbo.StreamActions
const customActions = {
// override the default `update`
update() {
doSomethingBeforeCallingUpdate(this)
originalUpdateAction()
},
myCustomAction() {
// ...
}
}
Turbo.StreamActions = { ...Turbo.StreamActions, ...customActions }Next, consider doing so within an event listener. This example assumes that addEventListener("turbo:before-stream-render", ({ relatedTarget, detail }) => {
const { render: originalRender } = detail
const customActions = {
update() {
doSomethingBeforeUpdate(this)
originalRender()
},
myCustomAction() {
callAnotherLibraryWith(this)
}
}
event.detail.render = customActions[target.action] || function() { console.error("Failed to handle action", this) }
})Just spitballing here. The event listener version is more verbose, but grants more of an opportunity to incorporate contextual information like the state of the document, the It would still require committing to support some form of a
Regardless of when the client-side extension happens, changes to support something like As far as I know, there isn't any coercion or validation happening at the |
|
I think the first option would be better for defining custom stream actions to do something like #477 |
|
It's currently possible, without any internal or external changes to Turbo, to implement custom Turbo Stream actions. As an alternative to extending Turbo Stream elements expect their first child to be a If that content were itself a Consider the following example code (available as a JSFilddle: https://jsfiddle.net/o1kLsq6p/) <html>
<head>
<script type="module">
import "https://unpkg.com/@hotwired/turbo@7.1.0"
import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus@3.0.1/dist/stimulus.js"
const Stimulus = Application.start()
Stimulus.register("custom-stream", class extends Controller {
static get values() { return { action: String } }
connect() {
const { content, parentElement } = this.element
switch (this.actionValue) {
case "append": {
parentElement.append(content)
parentElement.style.color = "red"
break
}
default: console.error(`Don't know how to handle ${this.actionValue}!`)
}
}
}
})
</script>
</head>
<body>
<ul id="my_list">
<li>First Element</li>
</ul>
</body>
</html>Consider receiving the following <turbo-stream action="append" target="my_list">
<template>
<template data-controller="custom-stream" data-custom-stream-action-value="append">
<li>Another Element</li>
</template>
</template>
</turbo-stream>When executed, the The The The |
|
This proves that stimulus + the default stream actions are enough to build any functionality that we desire even having custom dom actions. |
|
What remains to be done to finish this PR? |
|
@seb-jean this PR is pretty much done - at least code-wise. It's more a question about the philosophy around it. I agree, it's a sharp knife, as with most of the things in the Rails ecosystem. But I think, that in the end it's probably worth the flexibility and the endless possibilities it provides. |
|
@marcoroth thanks for your answer |
|
After letting this marinate for a while, I like the simplicity of exporting the stream actions, and allowing the app to extend as they see fit. We should document it as a sharp knife. But I like sharp knives 😄. |
f2aceb1 to
310658e
Compare
|
Should add a doc PR to explain how to actually extend the actions 👍 |
@dcyoung-dev @enoch-tamulonis since your original issues were the inspiration for this change, would either of you be interested in opening a pull request against https://github.com/hotwired/turbo-site/ outlining a use case or two that would motivate a customization? @marcoroth maybe you're interested as well? The how-to is captured by the |
f90bc14 to
5020760
Compare
f2c5a3d to
09c4acc
Compare
|
Rebase needed? |
09c4acc to
5ff8281
Compare
Closes hotwired#478 Closes hotwired#477 As an alternative to continuing to introduce support for more varied and specialized `<turbo-stream action="...">` values, this commit exports the `StreamActions` module from the root-module in order to provide applications with direct access. Applications can add new actions or override existing actions, so long as they adhere to the `StreamAction` interface: a function whose `this` context is an instance of `StreamElement`.
5ff8281 to
abae079
Compare
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Follow-up to hotwired#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
* Test harness: Prevent infinite recusion When serializing `CustomEvent.detail` objects from the browser to the Playwright test harness, the current `serializeToChannel` implementation is prone to recurse infinitely if an object nests another object that refers to the outer object. To prevent that, this commit tracks which objects have been visited during the current serialization process, and avoid an infinitely recursing loop. Related to that change, this commit also add more descriptive types to the `EventLog` and `MutationLog` arrays to clarify which positional elements correspond to their serialized values. Finally, extend the test server's `<turbo-stream>` creation actions to support serializing an element with an `[id]` value, so that it can be serialized from the browser to the test harness, and serve as a `target.id` value. * Add `render` to `turbo:before-stream-render` event Follow-up to #479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
* Test harness: Prevent infinite recusion When serializing `CustomEvent.detail` objects from the browser to the Playwright test harness, the current `serializeToChannel` implementation is prone to recurse infinitely if an object nests another object that refers to the outer object. To prevent that, this commit tracks which objects have been visited during the current serialization process, and avoid an infinitely recursing loop. Related to that change, this commit also add more descriptive types to the `EventLog` and `MutationLog` arrays to clarify which positional elements correspond to their serialized values. Finally, extend the test server's `<turbo-stream>` creation actions to support serializing an element with an `[id]` value, so that it can be serialized from the browser to the test harness, and serve as a `target.id` value. * Add `render` to `turbo:before-stream-render` event Follow-up to hotwired/turbo#479 Related to hotwired/turbo-site#107 As an alternative to exposing an otherwise private and internal `StreamActions` "class", support custom `<turbo-stream action="...">` values the same way as custom `<turbo-frame>` rendering or `<body>` rendering: as part of its `turbo:before-render-stream` event. To change how Turbo renders the document during page rendering, client applications declare a `turbo:before-render` event listener that overrides its `CustomEvent.detail.render` function from its [default][PageRenderer.renderElement]. Similarly, to change how Turbo renders a frame, client applications declare a `turbo:before-frame-render` and override its `CustomEvent.detail.render` function from its [default][]. This commit introduces the `StreamElement.renderElement` function, and extends the existing `turbo:before-stream-render` event to support the same pattern with `StreamElement.renderElement` server as the default `CustomEvent.detail.render` value. With those changes in place, callers can declare a document-wide event listener and override based on the value of `StreamElement.action`: ```javascript const CustomActions = { customUpdate: (stream) => { /* ... */ } customReplace: (stream) => { /* ... */ } // ... } document.addEventListener("turbo:before-stream-render", (({ target, detail }) => { const defaultRender = detail.render detail.render = CustomActions[target.action] || defaultRender })) ``` [PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13 [FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
Closes #478
Closes #477
As an alternative to continuing to introduce support for more varied and
specialized
<turbo-stream action="...">values, this commit exportsthe
StreamActionsmodule from the root-module in order to provideapplications with direct access.
Applications can add new actions or override existing actions, so long
as they adhere to the
StreamActioninterface: a function whosethiscontext is an instance of
StreamElement.