-
Notifications
You must be signed in to change notification settings - Fork 436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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/[email protected]"
import { Application, Controller } from "https://unpkg.com/@hotwired/[email protected]/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
Closes #478
Closes #477
As an alternative to continuing to introduce support for more varied and
specialized
<turbo-stream action="...">
values, this commit exportsthe
StreamActions
module 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
StreamAction
interface: a function whosethis
context is an instance of
StreamElement
.